okengine 0.17.2 → 0.18.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -3
- package/README.md +14 -10
- package/manifest.v1.schema.json +61 -2
- package/package.json +19 -5
- package/site/content/docs/ai/index.mdx +1 -1
- package/site/content/docs/ai/mcp.mdx +10 -1
- package/site/content/docs/ai/skills.mdx +9 -9
- package/site/content/docs/elements/ai.mdx +1 -1
- package/site/content/docs/elements/clock.mdx +1 -1
- package/site/content/docs/elements/flow.mdx +25 -1
- package/site/content/docs/elements/gate.mdx +3 -2
- package/site/content/docs/elements/store.mdx +289 -341
- package/site/content/docs/elements/vault.mdx +5 -5
- package/site/content/docs/get-started/basic-usage.mdx +3 -10
- package/site/content/docs/get-started/index.mdx +1 -1
- package/site/content/docs/get-started/installation.mdx +2 -3
- package/site/content/docs/get-started/introduction.mdx +58 -121
- package/site/content/docs/get-started/meta.json +9 -1
- package/site/content/docs/get-started/project-structure.mdx +4 -11
- package/site/content/docs/get-started/testing.mdx +328 -0
- package/site/content/docs/get-started/why.mdx +93 -71
- package/site/content/docs/index.mdx +44 -11
- package/site/content/docs/meta.json +8 -5
- package/site/content/docs/plugins/apple.mdx +151 -0
- package/site/content/docs/plugins/discord.mdx +139 -0
- package/site/content/docs/plugins/facebook.mdx +134 -0
- package/site/content/docs/plugins/figma.mdx +138 -0
- package/site/content/docs/plugins/github.mdx +138 -0
- package/site/content/docs/plugins/google.mdx +153 -0
- package/site/content/docs/plugins/index.mdx +47 -1
- package/site/content/docs/plugins/meta.json +10 -0
- package/site/content/docs/plugins/microsoft.mdx +151 -0
- package/site/content/docs/plugins/oauth.mdx +188 -0
- package/site/content/docs/plugins/x.mdx +125 -0
- package/site/content/docs/providers/index.mdx +2 -0
- package/site/content/docs/recipes/index.mdx +2 -0
- package/site/content/docs/reference/cli.md +3 -2
- package/site/content/docs/reference/client.mdx +58 -1
- package/site/content/docs/reference/configuration.mdx +2 -4
- package/site/content/docs/reference/fx.mdx +3 -1
- package/site/content/docs/reference/index.mdx +0 -5
- package/site/content/docs/reference/meta.json +2 -2
- package/site/content/docs/reference/okid.mdx +137 -0
- package/src/auth/bindings.ts +1 -1
- package/src/auth/config.ts +9 -0
- package/src/auth/identity-sql.ts +314 -0
- package/src/auth/identity.ts +140 -2
- package/src/auth/index.ts +17 -1
- package/src/auth/method-context.ts +3 -0
- package/src/auth/oauth-as/cimd.ts +132 -0
- package/src/auth/oauth-as/crypto.test.ts +101 -0
- package/src/auth/oauth-as/crypto.ts +393 -0
- package/src/auth/oauth-as/errors.ts +68 -0
- package/src/auth/oauth-as/http.test.ts +419 -0
- package/src/auth/oauth-as/http.ts +842 -0
- package/src/auth/oauth-as/stores.ts +61 -0
- package/src/auth/oauth-as/tables.ts +142 -0
- package/src/auth/tables.ts +0 -11
- package/src/bench/README.md +83 -0
- package/src/bench/REPORT.md +176 -0
- package/src/bench/g01-rls-stamp.bench.ts +194 -0
- package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
- package/src/bench/g03-signal-once.bench.ts +157 -0
- package/src/bench/g03-signal-reconnect.bench.ts +254 -0
- package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
- package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
- package/src/bench/g05-sustained-full.bench.ts +265 -0
- package/src/bench/g06-mixed-load.bench.ts +260 -0
- package/src/bench/g07-vault-crypto.bench.ts +100 -0
- package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
- package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
- package/src/bench/g08-store-kv-durable.bench.ts +133 -0
- package/src/bench/g08-store-sql.bench.ts +178 -0
- package/src/bench/g09-journal-sustained.bench.ts +203 -0
- package/src/bench/g10-observability-contention.bench.ts +246 -0
- package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
- package/src/bench/g13-elements.bench.ts +427 -0
- package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
- package/src/bench/g15-postgres-degradation.bench.ts +264 -0
- package/src/bench/g16-live-query-fanout.bench.ts +206 -0
- package/src/bench/lib/event-loop-lag.ts +26 -0
- package/src/bench/lib/infra.ts +60 -0
- package/src/bench/lib/report.ts +52 -0
- package/src/bench/lib/rss-sampler.ts +61 -0
- package/src/bench/lib/signal-pg.ts +88 -0
- package/src/bench/load-app.ts +337 -0
- package/src/bench/load-child.ts +108 -0
- package/src/bench/smoke.bench.ts +43 -0
- package/src/cli/competitor-mention-removal.test.ts +28 -0
- package/src/cli/doctor-fd.ts +117 -0
- package/src/cli/doctor.test.ts +192 -0
- package/src/cli/doctor.ts +129 -1
- package/src/client/create.ts +95 -1
- package/src/client/index.ts +9 -2
- package/src/client/transport.ts +11 -4
- package/src/client/use-live-query.ts +154 -0
- package/src/client-react/index.ts +15 -1
- package/src/client-react/live-resource.ts +246 -0
- package/src/client-react/use-live-query.test.ts +475 -0
- package/src/client-react/use-live-query.ts +530 -0
- package/src/compiler/extract.test.ts +518 -0
- package/src/compiler/extract.ts +386 -19
- package/src/console/server/invoke-user-flow.ts +2 -1
- package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-De7Lc2JC.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-RGy7VEA_.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-_rgpdVzo.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-Ds6pcnh-.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
- package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-4rHOePuE.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-DISPgxLM.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
- package/src/drivers/cdc-outbox.ts +389 -0
- package/src/drivers/memory.ts +20 -0
- package/src/drivers/oauth-apple.ts +156 -0
- package/src/drivers/oauth-discord.ts +79 -0
- package/src/drivers/oauth-facebook.ts +80 -0
- package/src/drivers/oauth-figma.ts +116 -0
- package/src/drivers/oauth-github.ts +92 -0
- package/src/drivers/oauth-google.ts +142 -0
- package/src/drivers/oauth-microsoft.ts +174 -0
- package/src/drivers/oauth-oidc.ts +293 -0
- package/src/drivers/oauth-shared.ts +326 -0
- package/src/drivers/oauth-types.ts +159 -0
- package/src/drivers/oauth-x.ts +77 -0
- package/src/drivers/oauth2-common.ts +95 -0
- package/src/drivers/oauth2-token.ts +61 -0
- package/src/drivers/pg-rls-row-passes.ts +251 -0
- package/src/drivers/pg-rls.ts +2 -0
- package/src/drivers/postgres.ts +45 -2
- package/src/drivers/signal-postgres.ts +2 -1
- package/src/elements/channel/runtime.ts +29 -2
- package/src/elements/channel.test.ts +52 -0
- package/src/elements/gate/boot.ts +29 -2
- package/src/elements/store/emit-drizzle.ts +147 -14
- package/src/elements/store/field-ddl.test.ts +118 -0
- package/src/elements/store/field-types.test.ts +455 -0
- package/src/elements/store/list-query.golden.json +777 -0
- package/src/elements/store/list-query.parity.test.ts +396 -0
- package/src/elements/store/list-query.ts +792 -0
- package/src/elements/store/live-default.test.ts +136 -0
- package/src/elements/store/live-http.test.ts +160 -0
- package/src/elements/store/live-isolation.test.ts +291 -0
- package/src/elements/store/live-query-runtime.test.ts +323 -0
- package/src/elements/store/live-query-runtime.ts +403 -0
- package/src/elements/store/live-query-server.test.ts +377 -0
- package/src/elements/store/live-query-server.ts +102 -0
- package/src/elements/store/live-query.ts +97 -0
- package/src/elements/store/resource.ts +189 -680
- package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
- package/src/elements/store/schema-decl.ts +539 -41
- package/src/elements/store/sql-rls-stamp.test.ts +27 -0
- package/src/elements/store/sql-session.ts +297 -35
- package/src/elements/store/table.ts +102 -21
- package/src/elements/store.test.ts +3 -1
- package/src/elements/store.ts +12 -1
- package/src/elements/vault/chaos-child.ts +74 -1
- package/src/elements/vault/chaos.test.ts +4 -2
- package/src/elements/vault/storage.ts +4 -2
- package/src/index.ts +5 -2
- package/src/kernel/app-auth.ts +1 -0
- package/src/kernel/app.ts +116 -2
- package/src/kernel/auth-sharing.test.ts +196 -0
- package/src/kernel/boot.test.ts +3 -3
- package/src/kernel/errors.ts +8 -0
- package/src/kernel/fx.test.ts +1 -0
- package/src/kernel/fx.ts +14 -2
- package/src/kernel/horizontal-child.ts +2 -1
- package/src/kernel/http-resource.ts +33 -7
- package/src/kernel/identity-host-persist.test.ts +119 -0
- package/src/kernel/instance-id.ts +4 -2
- package/src/kernel/journal.ts +2 -1
- package/src/kernel/mcp-tool.test.ts +95 -0
- package/src/kernel/on.ts +9 -0
- package/src/kernel/realtime-bind.ts +326 -0
- package/src/kernel/resource-live.ts +117 -0
- package/src/kernel/triggers.ts +86 -4
- package/src/manifest/diff.ts +37 -0
- package/src/manifest/types.ts +64 -2
- package/src/okid.bench.test.ts +64 -0
- package/src/okid.test.ts +338 -0
- package/src/okid.ts +245 -0
- package/src/plugins/anonymous.ts +19 -1
- package/src/plugins/auth/shared.ts +15 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/magic-link.ts +10 -8
- package/src/plugins/mcp-oauth.ts +208 -0
- package/src/plugins/oauth/flow-store.ts +117 -0
- package/src/plugins/oauth/link.ts +69 -0
- package/src/plugins/oauth/shared.ts +108 -0
- package/src/plugins/oauth/token-vault.ts +100 -0
- package/src/plugins/oauth.security.test.ts +535 -0
- package/src/plugins/oauth.ts +532 -0
- package/src/plugins/otp.ts +48 -6
- package/src/plugins/passkey.ts +20 -1
- package/src/plugins/two-factor.ts +11 -0
- package/src/plugins/username.ts +40 -7
- package/src/release/build-lib.ts +7 -1
- package/src/release/measure.ts +1 -0
- package/src/release/official-plugins.ts +4 -1
- package/src/runs/collect.ts +2 -1
- package/src/runs/drivers/files.ts +2 -1
- package/src/test/create-test-app.ts +114 -5
- package/src/test/export-bundle.test.ts +33 -0
- package/src/test/live-signals.test.ts +83 -0
- package/src/test/tenant-isolation.test.ts +175 -0
- package/src/testing.ts +26 -0
- package/src/upgrade/codemods.ts +1 -1
- package/site/content/docs/reference/migrating-environments.mdx +0 -158
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Testing
|
|
3
|
+
description: Write deterministic tests for your Flows with createTestApp — freeze the clock, mock AI, drain signals, and prove side effects.
|
|
4
|
+
icon: FlaskConical
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
`createTestApp` boots the **same app** you deploy, but with memory drivers and a
|
|
8
|
+
frozen clock. You drive it through `t.api`, `t.auth`, `t.signals`, `t.clock`,
|
|
9
|
+
and `t.channels` — every assertion is deterministic.
|
|
10
|
+
|
|
11
|
+
<Callout title="The one rule">
|
|
12
|
+
Boot the same `app` your server boots. Assert only through the harness surfaces (`t.api`,
|
|
13
|
+
`t.auth`, `t.signals`, `t.clock`, `t.channels`, `t.ai`, `t.effects`) — never reach into internal
|
|
14
|
+
runtimes.
|
|
15
|
+
</Callout>
|
|
16
|
+
|
|
17
|
+
## Import
|
|
18
|
+
|
|
19
|
+
The harness ships as a separate subpath so production bundles stay small:
|
|
20
|
+
|
|
21
|
+
```typescript title="tests/orders.test.ts"
|
|
22
|
+
import { describe, expect, test } from "bun:test";
|
|
23
|
+
import { createTestApp } from "okengine/testing"; // or "okengine/test"
|
|
24
|
+
import { app } from "../src/app";
|
|
25
|
+
|
|
26
|
+
test("orders flow", async () => {
|
|
27
|
+
const t = await createTestApp(app);
|
|
28
|
+
// ...
|
|
29
|
+
await t.close();
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Both `okengine/testing` and `okengine/test` are aliases. Use `okengine/testing`
|
|
34
|
+
(→ `dist/testing.js`) for published packages and hoisted monorepos; `okengine/test`
|
|
35
|
+
is the source-only path older starters use.
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
<Steps>
|
|
40
|
+
|
|
41
|
+
<Step>
|
|
42
|
+
### Adopt your Flows
|
|
43
|
+
|
|
44
|
+
Every behavior on the app is reachable. No separate test database or server.
|
|
45
|
+
|
|
46
|
+
```typescript title="src/flows/orders/index.ts"
|
|
47
|
+
import { on, flow, http } from "okengine";
|
|
48
|
+
import { z } from "zod";
|
|
49
|
+
|
|
50
|
+
export const create = on(
|
|
51
|
+
http.post("/orders").gate(member),
|
|
52
|
+
flow("orders.create", {
|
|
53
|
+
in: z.object({ sku: z.string(), qty: z.number() }),
|
|
54
|
+
out: z.object({ id: z.string() }),
|
|
55
|
+
do: async (input, fx) => {
|
|
56
|
+
const id = fx.id();
|
|
57
|
+
await fx.emit(orderPlaced, { orderId: id, ...input });
|
|
58
|
+
return { id };
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
</Step>
|
|
65
|
+
|
|
66
|
+
<Step>
|
|
67
|
+
### Boot the harness
|
|
68
|
+
|
|
69
|
+
```typescript title="tests/orders.test.ts"
|
|
70
|
+
const t = await createTestApp(app, { gates: [member] });
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
</Step>
|
|
74
|
+
|
|
75
|
+
<Step>
|
|
76
|
+
### Login and call
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const u = await t.auth.loginAs({ scopes: ["order:create"] });
|
|
80
|
+
const { data, error } = await t.api.orders.create({ sku: "COFFEE", qty: 2 }, { as: u });
|
|
81
|
+
expect(error).toBeNull();
|
|
82
|
+
expect(data).toEqual({ id: expect.any(String) });
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
</Step>
|
|
86
|
+
|
|
87
|
+
<Step>
|
|
88
|
+
### Advance time and drain signals
|
|
89
|
+
|
|
90
|
+
Signal handlers and Clocks are processed deterministically:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
await t.signals.drain(); // run queued signal work
|
|
94
|
+
await t.clock.advance("2m"); // tick scheduled Clocks forward
|
|
95
|
+
expect(t.channels.sent()).toContainEqual(expect.objectContaining({ template: "order-confirmed" }));
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
</Step>
|
|
99
|
+
|
|
100
|
+
</Steps>
|
|
101
|
+
|
|
102
|
+
## Harness surface
|
|
103
|
+
|
|
104
|
+
| Property | What it does |
|
|
105
|
+
| ------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
106
|
+
| `t.api.<unit>.<flow>(input, opts?)` | Invoke an adopted Flow directly with a `{ as }` principal and optional `{ tenant }` |
|
|
107
|
+
| `t.auth.loginAs(options)` | Returns a verified `TestUser` with `id`, `scopes`, `verified`, and optional `tenantId` |
|
|
108
|
+
| `t.clock.advance(by)` / `t.clock.now()` | Freeze and move the harness clock (used by durable sleeps and Clocks) |
|
|
109
|
+
| `t.cron.run(name)` | Run a named Clock / `every()` interval now, still leader-elected |
|
|
110
|
+
| `t.signals.drain()` | Drain the signal bus until idle (deterministic queued work) |
|
|
111
|
+
| `t.signals.subscribeLive(signal, handlers)` | Subscribe to a `delivery: "live"` signal's event stream (deterministic) |
|
|
112
|
+
| `t.signals.waitForLive(signal, predicate)` | Resolve once a live event matching `predicate` arrives |
|
|
113
|
+
| `t.channels.sent()` | Receipts / inbox entries sent during the test |
|
|
114
|
+
| `t.ai.mock(prompt, output)` | Register a canned mock response for a Prompt |
|
|
115
|
+
| `t.ai.cost()` | Accumulated AI cost across `fx.ask` calls |
|
|
116
|
+
| `t.effects.of(runId)` | Effect ledger for a recorded run (reads / writes / emits / sends) |
|
|
117
|
+
| `t.runs()` | All wide events recorded during the test |
|
|
118
|
+
| `t.close()` | Close booted element runtimes |
|
|
119
|
+
|
|
120
|
+
## Multi-tenancy & RLS
|
|
121
|
+
|
|
122
|
+
With `gate.auth: { tenant: true }` you can test tenant isolation with real
|
|
123
|
+
row-level security.
|
|
124
|
+
|
|
125
|
+
```typescript title="src/db/documents.ts"
|
|
126
|
+
import { store, field } from "okengine";
|
|
127
|
+
|
|
128
|
+
export const documents = store.schema.table(
|
|
129
|
+
"documents",
|
|
130
|
+
{
|
|
131
|
+
id: field.text().primaryKey(),
|
|
132
|
+
tenant_id: field.text().notNull(),
|
|
133
|
+
title: field.text().notNull(),
|
|
134
|
+
},
|
|
135
|
+
[store.schema.policy.tenant("tenant_id", { for: "all" })],
|
|
136
|
+
);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Give each test user a tenant, and drive distinct tenants against the same
|
|
140
|
+
|
|
141
|
+
Flow:
|
|
142
|
+
|
|
143
|
+
```typescript title="tests/tenant.test.ts"
|
|
144
|
+
const t = await createTestApp(app, {
|
|
145
|
+
gates: [member],
|
|
146
|
+
boot: { config: { drivers: { store: { sql: { test: "pglite" } } } } },
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const alice = await t.auth.loginAs({ id: "alice", tenantId: "acme" });
|
|
150
|
+
const bob = await t.auth.loginAs({ id: "bob", tenantId: "globex" });
|
|
151
|
+
|
|
152
|
+
await t.api.docs.create({ title: "Acme Roadmap" }, { as: alice });
|
|
153
|
+
await t.api.docs.create({ title: "Globex Secrets" }, { as: bob });
|
|
154
|
+
|
|
155
|
+
const acmeDocs = await t.api.docs.list(undefined, { as: alice });
|
|
156
|
+
expect(acmeDocs.data).toHaveLength(1); // only Acme Roadmap
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Re-scope a single call to another tenant with `{ tenant }`, independent of the
|
|
160
|
+
principal's own tenant:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
const carol = await t.auth.loginAs({ id: "carol", tenantId: "acme" });
|
|
164
|
+
const { data } = await t.api.docs.create(
|
|
165
|
+
{ title: "Globex Carol" },
|
|
166
|
+
{ as: carol, tenant: "globex" },
|
|
167
|
+
);
|
|
168
|
+
expect(data.tenantId).toBe("globex");
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
RLS policies are row-level: tenant-scoped tables need `store.schema.policy.tenant`
|
|
172
|
+
(or `store.schema.unscoped()` for genuinely global tables). Harness boots do not
|
|
173
|
+
run migrations — in a real app `oke db push` installs those policies.
|
|
174
|
+
|
|
175
|
+
## Deterministic live signals
|
|
176
|
+
|
|
177
|
+
Signals with `delivery: "live"` back client feeds and SSE streams. The harness
|
|
178
|
+
exposes the retained event stream so you can assert both sides deterministically:
|
|
179
|
+
|
|
180
|
+
```typescript title="tests/live.test.ts"
|
|
181
|
+
const orderStatus = signal("order-status", {
|
|
182
|
+
delivery: "live",
|
|
183
|
+
retention: { maxCount: 10 },
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
on(
|
|
187
|
+
http.post("/orders/status").public(),
|
|
188
|
+
flow("orders.updateStatus", {
|
|
189
|
+
in: z.object({ orderId: z.string(), status: z.string() }),
|
|
190
|
+
effects: { emits: ["order-status"] },
|
|
191
|
+
do: async (input, fx) => {
|
|
192
|
+
await fx.emit(orderStatus, input);
|
|
193
|
+
return { ok: true };
|
|
194
|
+
},
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
const seen: Array<{ orderId: string; status: string }> = [];
|
|
201
|
+
|
|
202
|
+
const unsubscribe = t.signals.subscribeLive(orderStatus, {
|
|
203
|
+
onEvent: (event) => seen.push(event),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await t.api.orders.updateStatus({ orderId: "o1", status: "placed" });
|
|
207
|
+
await t.signals.drain();
|
|
208
|
+
expect(seen.map((e) => e.status)).toEqual(["placed"]);
|
|
209
|
+
|
|
210
|
+
// Resolve a promise the moment a matching event arrives.
|
|
211
|
+
const deliveredPromise = t.signals.waitForLive(
|
|
212
|
+
orderStatus,
|
|
213
|
+
(event) => event.status === "delivered",
|
|
214
|
+
);
|
|
215
|
+
await t.api.orders.updateStatus({ orderId: "o1", status: "delivered" });
|
|
216
|
+
await t.signals.drain();
|
|
217
|
+
const delivered = await deliveredPromise;
|
|
218
|
+
expect(delivered.status).toBe("delivered");
|
|
219
|
+
|
|
220
|
+
unsubscribe(); // no further events observed
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Call `t.signals.drain()` after each emit — live events are staged until the bus
|
|
224
|
+
drains. `retention` caps the replayed tape (`maxCount` / `maxAge`); it is only
|
|
225
|
+
valid on `delivery: "live"` signals.
|
|
226
|
+
|
|
227
|
+
## Time travel
|
|
228
|
+
|
|
229
|
+
`t.clock` freezes time. `t.clock.advance("2m")` moves the clock forward and
|
|
230
|
+
resumes any durable sleeps that become due. `t.cron.run("expire-stale")` runs a
|
|
231
|
+
named Clock now:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
await t.clock.advance("30s"); // durable sleeps resume
|
|
235
|
+
await t.cron.run("expire-stale"); // named Clock fires
|
|
236
|
+
expect(t.channels.sent()).toHaveLength(1); // side effect observed
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## AI & channels
|
|
240
|
+
|
|
241
|
+
`fx.ask` routes to a mock driver. Register canned outputs and read accumulated
|
|
242
|
+
cost:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
t.ai.mock(summarizePrompt, { title: "Mock summary" });
|
|
246
|
+
const { data } = await t.api.posts.summarize({ postId: "p_1" }, { as: u });
|
|
247
|
+
expect(data).toEqual({ title: "Mock summary" });
|
|
248
|
+
expect(t.ai.cost()).toBe(0);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Channels write to an inbox instead of a transport:
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
expect(t.channels.sent()).toContainEqual(
|
|
255
|
+
expect.objectContaining({ template: "order-confirmed", to: u.id }),
|
|
256
|
+
);
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Troubleshooting
|
|
260
|
+
|
|
261
|
+
<Accordions>
|
|
262
|
+
|
|
263
|
+
<Accordion title="createTestApp fails to boot — missing secrets or stores">
|
|
264
|
+
The starter patches test drivers and fallback secrets on the app options. If
|
|
265
|
+
your `oke({ ... })` references a Vault secret or Store that the template does
|
|
266
|
+
not define, pass them explicitly:
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
await createTestApp(app, {
|
|
270
|
+
secrets: [openAiKey],
|
|
271
|
+
vaultSecrets: { OPENAI_KEY: "sk-test" },
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
</Accordion>
|
|
276
|
+
|
|
277
|
+
<Accordion title="My live subscription never receives events">
|
|
278
|
+
- Live events are staged until `t.signals.drain()` — await `drain()` after
|
|
279
|
+
every emitting call.
|
|
280
|
+
- Register `subscribeLive` / `waitForLive` **before** you emit.
|
|
281
|
+
- A signal with a schema drops violating emits — assert `res.error`.
|
|
282
|
+
|
|
283
|
+
</Accordion>
|
|
284
|
+
|
|
285
|
+
<Accordion title="Cross-tenant reads return a stale cached row">
|
|
286
|
+
Tier-1 read caching keys by flow, input, and user id — tenant is a row-level
|
|
287
|
+
RLS scope, not a cache dimension. For reads under a `{ tenant }` override, use
|
|
288
|
+
a distinct call principal (or write first) so the cache key differs.
|
|
289
|
+
|
|
290
|
+
</Accordion>
|
|
291
|
+
|
|
292
|
+
<Accordion title="PGlite RLS policies aren't applied in the harness">
|
|
293
|
+
Test boots never run migrations — `oke db push` installs DDL and
|
|
294
|
+
`store.schema.policy.tenant` policies in real apps.
|
|
295
|
+
|
|
296
|
+
Assert real RLS by installing the policy + `oke.*` helpers on the booted
|
|
297
|
+
primary connection (as the repo's `sql-rls-isolation.test.ts` does).
|
|
298
|
+
|
|
299
|
+
</Accordion>
|
|
300
|
+
|
|
301
|
+
</Accordions>
|
|
302
|
+
|
|
303
|
+
## Learn more
|
|
304
|
+
|
|
305
|
+
- [Flow](/docs/elements/flow) — triggers, contracts, effects, and composition
|
|
306
|
+
- [Store](/docs/elements/store) — SQL, KV, files, index, and RLS policies
|
|
307
|
+
- [Signal](/docs/elements/signal) — delivery physics, retention, and live feeds
|
|
308
|
+
- [Client](/docs/reference/client) — the typed client for real HTTP calls
|
|
309
|
+
|
|
310
|
+
## Next
|
|
311
|
+
|
|
312
|
+
<Cards>
|
|
313
|
+
<Card
|
|
314
|
+
title="Project structure"
|
|
315
|
+
description="Folders are the URL; unit plus export is the name."
|
|
316
|
+
href="/docs/get-started/project-structure"
|
|
317
|
+
/>
|
|
318
|
+
<Card
|
|
319
|
+
title="Flow"
|
|
320
|
+
description="Triggers, contracts, effects, and composition."
|
|
321
|
+
href="/docs/elements/flow"
|
|
322
|
+
/>
|
|
323
|
+
<Card
|
|
324
|
+
title="Signal"
|
|
325
|
+
description="Delivery physics, retention, and deterministic live feeds."
|
|
326
|
+
href="/docs/elements/signal"
|
|
327
|
+
/>
|
|
328
|
+
</Cards>
|
|
@@ -1,120 +1,142 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Why OKE
|
|
3
|
-
description:
|
|
3
|
+
description: From forty tools to one species — the Manifest, the `fx` rule, and the eight closed elements.
|
|
4
4
|
source: docs/spec/unified-theory.md
|
|
5
5
|
icon: Compass
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
that serves last month's schema, the secret that only exists on your laptop,
|
|
10
|
-
the dashboard that has never heard of your new Flow.
|
|
8
|
+
## From forty tools to one species
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
knowledge, kept in places the compiler cannot check, updated by memory.
|
|
10
|
+
A booking API, a nightly cleanup job, a receipt email, a row-change hook — in most stacks these are **four frameworks**. In OKE they are **one species** with one shape. Learn the shape once; only the trigger changes.
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
> **The one rule**
|
|
13
|
+
> Every backend behavior is a **Flow**: `on(Trigger) → Effects`. There are no separate species called endpoints, handlers, consumers, jobs, or workflows.
|
|
14
|
+
|
|
15
|
+
<Callout title="A note on the count">
|
|
16
|
+
'Forty' is a representative illustration of the number of backend concerns developers commonly
|
|
17
|
+
assemble across modern stacks, not a literal count of backend technologies.
|
|
18
18
|
</Callout>
|
|
19
19
|
|
|
20
|
-
## The
|
|
20
|
+
## The pain we solve
|
|
21
|
+
|
|
22
|
+
Every TypeScript backend works on day one. The bill arrives later. None of these are router problems — they are **gaps** between what your code knows and what your tooling knows.
|
|
21
23
|
|
|
22
|
-
###
|
|
24
|
+
### Cache that lies
|
|
23
25
|
|
|
24
|
-
You add a column to `orders` and update three writers. The
|
|
25
|
-
key in `checkout` is not one of them. A customer finds it a week later.
|
|
26
|
+
You add a column to `orders` and update three writers. The cache key in `checkout` is not one of them. A customer finds it a week later.
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
follows the Flow — there is no separate key to remember.
|
|
28
|
+
OKE derives: reads and writes are recorded through `fx`, so cache invalidation follows the Flow — there is no separate key to remember.
|
|
29
29
|
|
|
30
|
-
###
|
|
30
|
+
### Secret that fails at 2am
|
|
31
31
|
|
|
32
|
-
`STRIPE_KEY` lives in your laptop's `.env`, a README, and a teammate's shell
|
|
33
|
-
history. The deploy boots fine — the first charge request dies at 2am.
|
|
32
|
+
`STRIPE_KEY` lives in your laptop's `.env`, a README, and a teammate's shell history. The deploy boots fine — the first charge request dies at 2am.
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
code; boot resolves every contract and fails loud with every gap listed —
|
|
37
|
-
never halfway.
|
|
34
|
+
OKE derives: [Vault](/docs/elements/vault) contracts declare the need in code; boot resolves every contract and **fails loud** with every gap listed — never halfway.
|
|
38
35
|
|
|
39
36
|
### The glue you rewrite
|
|
40
37
|
|
|
41
|
-
CORS rules, security headers, CSRF tokens, compression — copied from the last
|
|
42
|
-
repo, tweaked, and already drifting from whatever that repo does today.
|
|
38
|
+
CORS rules, security headers, CSRF tokens, compression — copied from the last repo, tweaked, and already drifting from whatever that repo does today.
|
|
43
39
|
|
|
44
|
-
|
|
45
|
-
shared lifecycle, optional live DB config, nothing to re-copy.
|
|
40
|
+
OKE derives: the official `okengine/plugins` set ships this glue once — shared lifecycle, optional live DB config, nothing to re-copy.
|
|
46
41
|
|
|
47
42
|
### The dashboard that doesn't know you
|
|
48
43
|
|
|
49
|
-
Your observability stack learned your routes from sampled traffic. The Flow
|
|
50
|
-
you deployed an hour ago is invisible until someone wires it by hand.
|
|
44
|
+
Your observability stack learned your routes from sampled traffic. The Flow you deployed an hour ago is invisible until someone wires it by hand.
|
|
51
45
|
|
|
52
|
-
|
|
53
|
-
flows, effects, traces, architecture — current on every save, in dev and prod
|
|
54
|
-
(`:6533`).
|
|
46
|
+
OKE derives: its operator interface reads the Manifest — flows, effects, traces, architecture — current on every save, in dev and prod (`:6533`).
|
|
55
47
|
|
|
56
|
-
###
|
|
48
|
+
### Permission check in the wrong place
|
|
57
49
|
|
|
58
|
-
`if (!user.isAdmin)` sits in handler forty-one of sixty. Which Flows touch
|
|
59
|
-
`payments`? grep answers slowly; review answers never.
|
|
50
|
+
`if (!user.isAdmin)` sits in handler forty-one of sixty. Which Flows touch `payments`? grep answers slowly; review answers never.
|
|
60
51
|
|
|
61
|
-
|
|
62
|
-
access appears in Manifest Diff, not in a diff nobody reads.
|
|
52
|
+
OKE derives: declared effects produce a **least-privilege matrix** — widening access appears in Manifest Diff, not in a diff nobody reads.
|
|
63
53
|
|
|
64
54
|
### Local works, prod doesn't
|
|
65
55
|
|
|
66
|
-
Local runs one vendor client, CI another, prod a third — three glue stories
|
|
67
|
-
|
|
56
|
+
Local runs one vendor client, CI another, prod a third — three glue stories for the same database.
|
|
57
|
+
|
|
58
|
+
OKE derives: drivers are named after protocols (`postgres`, `redis`, `s3`), the vendor lives in `images`, and `oke dev` runs the real stack locally.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
Every gap above is the same shape: knowledge the code already has, duplicated somewhere the compiler cannot check. **The tax is drift.** One change costs fifteen seams to update by hand; in OKE it always costs one.
|
|
63
|
+
|
|
64
|
+
## One Manifest feeds every surface
|
|
65
|
+
|
|
66
|
+
At build time OKE extracts a **Manifest** — the compiled, versioned contract between your backend model and everything derived from it. The source of truth is your model expressed in code (`on(Trigger) → Flow → fx → Effects`); the Manifest freezes that model into a machine-readable, diffable contract. You do not write or hand-maintain the Manifest:
|
|
67
|
+
|
|
68
|
+
| Surface | Port | You maintain? |
|
|
69
|
+
| --------------------------------- | ------------------ | ----------------------------- |
|
|
70
|
+
| Typed client (`okengine/client`) | your app code | No — derived from `Manifest` |
|
|
71
|
+
| Console panels, traces, explorers | `:6533` | No — reads the Manifest |
|
|
72
|
+
| MCP for agents | `:6535` | No — reads the Manifest |
|
|
73
|
+
| Architecture diagram | Console | No — it _is_ the effect graph |
|
|
74
|
+
| Capability matrix + cache keys | compiler / runtime | No — inferred from `fx` |
|
|
75
|
+
|
|
76
|
+
Because every surface reads the same Manifest contract, they **cannot disagree** — there is only one source of truth.
|
|
77
|
+
|
|
78
|
+
## The `fx` rule — one door to the world
|
|
79
|
+
|
|
80
|
+
**All world access goes through `fx`.** A Flow that imports `node:fs`, calls `fetch` directly, or uses `Date.now()` is a defect. Effects are **inferred from what a Flow touches through `fx`**, and that inference powers the Manifest, the Console, caching, and durability.
|
|
81
|
+
|
|
82
|
+
<Callout title="What `fx` records">
|
|
83
|
+
Every read, write, emit, send, ask, secret, and call is captured — so the Manifest knows exactly
|
|
84
|
+
which flows touch `orders` or send PII to a model, without you declaring it.
|
|
85
|
+
</Callout>
|
|
68
86
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
| Inferred from `fx` | What it gives you |
|
|
88
|
+
| ----------------------- | ------------------------------------ |
|
|
89
|
+
| Cache invalidation keys | Automatic — follows the Flow |
|
|
90
|
+
| Live queries | Built-in subscription model |
|
|
91
|
+
| Least-privilege tokens | Capability matrix from effects |
|
|
92
|
+
| Deterministic tests | Time, clock, and randomness injected |
|
|
93
|
+
| Manifest Diff | Exact change surface on every save |
|
|
72
94
|
|
|
73
|
-
##
|
|
95
|
+
## Eight elements — a closed set
|
|
74
96
|
|
|
75
|
-
|
|
76
|
-
code already has. Watch one change propagate both ways.
|
|
97
|
+
Forty infrastructure concerns collapse into eight elements — each kept only because it has **irreducible physics**. New infrastructure becomes a **driver** for an existing element, never a ninth.
|
|
77
98
|
|
|
78
|
-
|
|
99
|
+
| Element | Essence | Replaces the zoo |
|
|
100
|
+
| ----------- | ----------------------------- | --------------------------------------------------------- |
|
|
101
|
+
| **Flow** | Behavior | endpoint · handler · consumer · job · workflow · webhook |
|
|
102
|
+
| **Signal** | Data in motion | queue · pub/sub · stream · websocket · SSE · event bus |
|
|
103
|
+
| **Store** | Data at rest | database · cache · KV · file storage · search index |
|
|
104
|
+
| **Clock** | Time | cron · delay · timeout · durable sleep · TTL |
|
|
105
|
+
| **Gate** | Permission to act | auth · session · ABAC · rate limit · quota · feature flag |
|
|
106
|
+
| **Vault** | Protected knowledge | secrets · config · environment |
|
|
107
|
+
| **Channel** | Reaching humans | email · SMS · WhatsApp · push |
|
|
108
|
+
| **AI** | Reaching machine intelligence | model calls · prompts · embeddings · agents · RAG |
|
|
79
109
|
|
|
80
|
-
|
|
81
|
-
feeds five surfaces — they cannot disagree, because none of them is a copy.
|
|
110
|
+
Drivers are named after **protocols** (`postgres`, `redis`, `s3`) — never vendors. The vendor lives in `images`, and `oke dev` runs the real stack locally.
|
|
82
111
|
|
|
83
|
-
## The
|
|
112
|
+
## The shape of a Flow
|
|
84
113
|
|
|
85
|
-
|
|
86
|
-
because it has irreducible physics. One change costs up to fifteen seams in
|
|
87
|
-
the zoo; here it always costs two.
|
|
114
|
+
One pipeline. Only the trigger changes between an endpoint, a job, a consumer, and a row hook:
|
|
88
115
|
|
|
89
|
-
|
|
116
|
+
```ts
|
|
117
|
+
import { on, flow, http } from "okengine/http";
|
|
90
118
|
|
|
91
|
-
|
|
92
|
-
|
|
119
|
+
export const health = on(http.get().public(), flow({ do: () => ({ ok: true }) }));
|
|
120
|
+
```
|
|
93
121
|
|
|
94
|
-
|
|
122
|
+
| Piece | Role |
|
|
123
|
+
| ------------- | -------------------------------------------------------------- |
|
|
124
|
+
| **Trigger** | How work starts — `http`, a signal, `every`, a row change |
|
|
125
|
+
| **Contracts** | `in`, `out`, typed `errors` — validated before and after `do` |
|
|
126
|
+
| **`do`** | The body — every read, write, emit, and call goes through `fx` |
|
|
127
|
+
| **Effects** | Inferred from those `fx` calls — not hand-annotated |
|
|
95
128
|
|
|
96
|
-
|
|
97
|
-
| ------------------ | --------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
98
|
-
| Behavior model | Endpoints, jobs, consumers, workflows as separate species | One species — Flow: `on(Trigger) → Effects` |
|
|
99
|
-
| Cache invalidation | Hand-written keys; drift from writers | Derived from effects recorded through `fx` |
|
|
100
|
-
| HTTP glue | Middleware copied per repo | Official plugins — `headers`, `cors`, `csrf`, compression, IP allowlist |
|
|
101
|
-
| Secrets / config | Env sprawl; fails on first request | Vault contracts; `VaultBootError` at boot |
|
|
102
|
-
| Observability | Bolted on; separate source of truth | Console from the Manifest, dev and prod (`:6533`) |
|
|
103
|
-
| Permissions | Ad-hoc checks scattered in handlers | Least-privilege matrix from declared effects |
|
|
104
|
-
| Local vs prod | Vendor clients; one-off compose | Protocol drivers; vendor in `images`; `oke dev` |
|
|
105
|
-
| Client / agents | Separate codegen or hand-kept schemas | Typed client and MCP (`:6535`) from the same Manifest |
|
|
129
|
+
Consequence: **one documentation path, one trace shape, one Flow model, one thing for an AI agent to learn.**
|
|
106
130
|
|
|
107
131
|
## Ambition, stated plainly
|
|
108
132
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
| **Grounded in** | Eight elements, effect inference through `fx`, Gate and Vault, the official plugin set |
|
|
113
|
-
| **Maturity** | **pre-1.0** — published and usable; not independently battle-tested at scale yet |
|
|
133
|
+
> **Ambition**: The default, most capable TypeScript backend — Bun-first, Web-Standards portable, contract-first
|
|
134
|
+
> **Grounded in**: Eight elements, effect inference through `fx`, Gate and Vault, the official plugin set
|
|
135
|
+
> **Maturity**: **pre-1.0** — published and usable; not independently battle-tested at scale yet
|
|
114
136
|
|
|
115
137
|
## Learn more
|
|
116
138
|
|
|
117
|
-
- [Introduction](/docs/get-started/introduction) — the one law, eight elements,
|
|
139
|
+
- [Introduction](/docs/get-started/introduction) — the one law, eight elements, one contract
|
|
118
140
|
- [Flow](/docs/elements/flow) — how effects are recorded and inferred
|
|
119
141
|
- [Vault](/docs/elements/vault) — fail-loud secret contracts
|
|
120
142
|
- [Plugins](/docs/reference/plugins) — the official HTTP glue set
|
|
@@ -134,7 +156,7 @@ ninth element — the set of eight is closed.
|
|
|
134
156
|
/>
|
|
135
157
|
<Card
|
|
136
158
|
title="Introduction"
|
|
137
|
-
description="The one law, eight elements,
|
|
159
|
+
description="The one law, eight elements, one contract."
|
|
138
160
|
href="/docs/get-started/introduction"
|
|
139
161
|
/>
|
|
140
162
|
</Cards>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Documentation"
|
|
3
|
-
description: "One law. Eight elements.
|
|
3
|
+
description: "A new programming model for backends. One law. Eight elements. One contract."
|
|
4
4
|
icon: "BookOpen"
|
|
5
5
|
source: "docs/spec/unified-theory.md"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
Welcome to the okengine handbook.
|
|
8
|
+
Welcome to the okengine handbook. The backend model stays small; operational surfaces are derived from it instead of maintained separately.
|
|
9
9
|
|
|
10
10
|
<Callout title="The one law">
|
|
11
11
|
Every backend behavior is a Flow: `on(Trigger) → Effects`. One species; triggers are typed values.
|
|
@@ -17,29 +17,62 @@ on(every("10m"), expireStale);
|
|
|
17
17
|
on(orderPlaced, sendReceipt);
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
##
|
|
20
|
+
## Understand OKE (Core Model)
|
|
21
|
+
|
|
22
|
+
<Cards>
|
|
23
|
+
<Card
|
|
24
|
+
title="Why OKE"
|
|
25
|
+
description="From forty tools to one species — the problem, the law, and the Manifest contract."
|
|
26
|
+
href="/docs/get-started/why"
|
|
27
|
+
/>
|
|
28
|
+
<Card
|
|
29
|
+
title="Elements"
|
|
30
|
+
description="The eight closed core elements: Flow, Signal, Store, Clock, Gate, Vault, Channel, AI."
|
|
31
|
+
href="/docs/elements"
|
|
32
|
+
/>
|
|
33
|
+
<Card
|
|
34
|
+
title="Introduction"
|
|
35
|
+
description="The one law, eight elements, and one contract."
|
|
36
|
+
href="/docs/get-started/introduction"
|
|
37
|
+
/>
|
|
38
|
+
</Cards>
|
|
39
|
+
|
|
40
|
+
## Build with OKE (Surfaces & Infrastructure)
|
|
21
41
|
|
|
22
42
|
<Cards>
|
|
23
43
|
<Card
|
|
24
44
|
title="Get Started"
|
|
25
|
-
description="
|
|
45
|
+
description="Installation, basic usage, project structure, and testing."
|
|
26
46
|
href="/docs/get-started"
|
|
27
47
|
/>
|
|
28
|
-
<Card title="Elements" description="Flow → AI reference, one page each." href="/docs/elements" />
|
|
29
48
|
<Card
|
|
30
49
|
title="Plugins"
|
|
31
|
-
description="
|
|
50
|
+
description="22 official extensions — authentication, OAuth, security, ops, performance."
|
|
32
51
|
href="/docs/plugins"
|
|
33
52
|
/>
|
|
34
53
|
<Card
|
|
35
|
-
title="
|
|
36
|
-
description="
|
|
37
|
-
href="/docs/
|
|
54
|
+
title="Providers"
|
|
55
|
+
description="12 managed cloud infrastructure options behind protocol-named drivers."
|
|
56
|
+
href="/docs/providers"
|
|
57
|
+
/>
|
|
58
|
+
<Card
|
|
59
|
+
title="Recipes"
|
|
60
|
+
description="20 self-hosted infrastructure recipes via Docker compose."
|
|
61
|
+
href="/docs/recipes"
|
|
38
62
|
/>
|
|
39
63
|
<Card
|
|
40
64
|
title="Reference"
|
|
41
|
-
description="
|
|
65
|
+
description="Configuration, fx ledger, environment variables, errors, client, and security."
|
|
42
66
|
href="/docs/reference"
|
|
43
67
|
/>
|
|
44
|
-
<Card
|
|
68
|
+
<Card
|
|
69
|
+
title="AI Resources & MCP"
|
|
70
|
+
description="Runtime MCP (:6535), docs MCP (:6536), skills, and /llms.txt."
|
|
71
|
+
href="/docs/ai"
|
|
72
|
+
/>
|
|
73
|
+
<Card
|
|
74
|
+
title="Deployment"
|
|
75
|
+
description="Docker, Swarm, Kubernetes, and reverse proxy deployment."
|
|
76
|
+
href="/docs/deployment"
|
|
77
|
+
/>
|
|
45
78
|
</Cards>
|
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
"icon": "BookOpen",
|
|
4
4
|
"pages": [
|
|
5
5
|
"index",
|
|
6
|
-
"
|
|
6
|
+
"---Understand OKE---",
|
|
7
7
|
"elements",
|
|
8
|
-
"
|
|
8
|
+
"---Build with OKE---",
|
|
9
|
+
"get-started",
|
|
10
|
+
"ai",
|
|
9
11
|
"deployment",
|
|
10
|
-
"recipes",
|
|
11
|
-
"providers",
|
|
12
12
|
"reference",
|
|
13
|
-
"
|
|
13
|
+
"plugins",
|
|
14
|
+
"---Drivers---",
|
|
15
|
+
"providers",
|
|
16
|
+
"recipes"
|
|
14
17
|
]
|
|
15
18
|
}
|