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.
Files changed (209) hide show
  1. package/AGENTS.md +5 -3
  2. package/README.md +14 -10
  3. package/manifest.v1.schema.json +61 -2
  4. package/package.json +19 -5
  5. package/site/content/docs/ai/index.mdx +1 -1
  6. package/site/content/docs/ai/mcp.mdx +10 -1
  7. package/site/content/docs/ai/skills.mdx +9 -9
  8. package/site/content/docs/elements/ai.mdx +1 -1
  9. package/site/content/docs/elements/clock.mdx +1 -1
  10. package/site/content/docs/elements/flow.mdx +25 -1
  11. package/site/content/docs/elements/gate.mdx +3 -2
  12. package/site/content/docs/elements/store.mdx +289 -341
  13. package/site/content/docs/elements/vault.mdx +5 -5
  14. package/site/content/docs/get-started/basic-usage.mdx +3 -10
  15. package/site/content/docs/get-started/index.mdx +1 -1
  16. package/site/content/docs/get-started/installation.mdx +2 -3
  17. package/site/content/docs/get-started/introduction.mdx +58 -121
  18. package/site/content/docs/get-started/meta.json +9 -1
  19. package/site/content/docs/get-started/project-structure.mdx +4 -11
  20. package/site/content/docs/get-started/testing.mdx +328 -0
  21. package/site/content/docs/get-started/why.mdx +93 -71
  22. package/site/content/docs/index.mdx +44 -11
  23. package/site/content/docs/meta.json +8 -5
  24. package/site/content/docs/plugins/apple.mdx +151 -0
  25. package/site/content/docs/plugins/discord.mdx +139 -0
  26. package/site/content/docs/plugins/facebook.mdx +134 -0
  27. package/site/content/docs/plugins/figma.mdx +138 -0
  28. package/site/content/docs/plugins/github.mdx +138 -0
  29. package/site/content/docs/plugins/google.mdx +153 -0
  30. package/site/content/docs/plugins/index.mdx +47 -1
  31. package/site/content/docs/plugins/meta.json +10 -0
  32. package/site/content/docs/plugins/microsoft.mdx +151 -0
  33. package/site/content/docs/plugins/oauth.mdx +188 -0
  34. package/site/content/docs/plugins/x.mdx +125 -0
  35. package/site/content/docs/providers/index.mdx +2 -0
  36. package/site/content/docs/recipes/index.mdx +2 -0
  37. package/site/content/docs/reference/cli.md +3 -2
  38. package/site/content/docs/reference/client.mdx +58 -1
  39. package/site/content/docs/reference/configuration.mdx +2 -4
  40. package/site/content/docs/reference/fx.mdx +3 -1
  41. package/site/content/docs/reference/index.mdx +0 -5
  42. package/site/content/docs/reference/meta.json +2 -2
  43. package/site/content/docs/reference/okid.mdx +137 -0
  44. package/src/auth/bindings.ts +1 -1
  45. package/src/auth/config.ts +9 -0
  46. package/src/auth/identity-sql.ts +314 -0
  47. package/src/auth/identity.ts +140 -2
  48. package/src/auth/index.ts +17 -1
  49. package/src/auth/method-context.ts +3 -0
  50. package/src/auth/oauth-as/cimd.ts +132 -0
  51. package/src/auth/oauth-as/crypto.test.ts +101 -0
  52. package/src/auth/oauth-as/crypto.ts +393 -0
  53. package/src/auth/oauth-as/errors.ts +68 -0
  54. package/src/auth/oauth-as/http.test.ts +419 -0
  55. package/src/auth/oauth-as/http.ts +842 -0
  56. package/src/auth/oauth-as/stores.ts +61 -0
  57. package/src/auth/oauth-as/tables.ts +142 -0
  58. package/src/auth/tables.ts +0 -11
  59. package/src/bench/README.md +83 -0
  60. package/src/bench/REPORT.md +176 -0
  61. package/src/bench/g01-rls-stamp.bench.ts +194 -0
  62. package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
  63. package/src/bench/g03-signal-once.bench.ts +157 -0
  64. package/src/bench/g03-signal-reconnect.bench.ts +254 -0
  65. package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
  66. package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
  67. package/src/bench/g05-sustained-full.bench.ts +265 -0
  68. package/src/bench/g06-mixed-load.bench.ts +260 -0
  69. package/src/bench/g07-vault-crypto.bench.ts +100 -0
  70. package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
  71. package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
  72. package/src/bench/g08-store-kv-durable.bench.ts +133 -0
  73. package/src/bench/g08-store-sql.bench.ts +178 -0
  74. package/src/bench/g09-journal-sustained.bench.ts +203 -0
  75. package/src/bench/g10-observability-contention.bench.ts +246 -0
  76. package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
  77. package/src/bench/g13-elements.bench.ts +427 -0
  78. package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
  79. package/src/bench/g15-postgres-degradation.bench.ts +264 -0
  80. package/src/bench/g16-live-query-fanout.bench.ts +206 -0
  81. package/src/bench/lib/event-loop-lag.ts +26 -0
  82. package/src/bench/lib/infra.ts +60 -0
  83. package/src/bench/lib/report.ts +52 -0
  84. package/src/bench/lib/rss-sampler.ts +61 -0
  85. package/src/bench/lib/signal-pg.ts +88 -0
  86. package/src/bench/load-app.ts +337 -0
  87. package/src/bench/load-child.ts +108 -0
  88. package/src/bench/smoke.bench.ts +43 -0
  89. package/src/cli/competitor-mention-removal.test.ts +28 -0
  90. package/src/cli/doctor-fd.ts +117 -0
  91. package/src/cli/doctor.test.ts +192 -0
  92. package/src/cli/doctor.ts +129 -1
  93. package/src/client/create.ts +95 -1
  94. package/src/client/index.ts +9 -2
  95. package/src/client/transport.ts +11 -4
  96. package/src/client/use-live-query.ts +154 -0
  97. package/src/client-react/index.ts +15 -1
  98. package/src/client-react/live-resource.ts +246 -0
  99. package/src/client-react/use-live-query.test.ts +475 -0
  100. package/src/client-react/use-live-query.ts +530 -0
  101. package/src/compiler/extract.test.ts +518 -0
  102. package/src/compiler/extract.ts +386 -19
  103. package/src/console/server/invoke-user-flow.ts +2 -1
  104. package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-De7Lc2JC.js} +1 -1
  105. package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-RGy7VEA_.js} +1 -1
  106. package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-_rgpdVzo.js} +3 -3
  107. package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-Ds6pcnh-.js} +1 -1
  108. package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
  109. package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-4rHOePuE.js} +1 -1
  110. package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-DISPgxLM.js} +1 -1
  111. package/src/console/ui-next/dist/index.html +1 -1
  112. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
  113. package/src/drivers/cdc-outbox.ts +389 -0
  114. package/src/drivers/memory.ts +20 -0
  115. package/src/drivers/oauth-apple.ts +156 -0
  116. package/src/drivers/oauth-discord.ts +79 -0
  117. package/src/drivers/oauth-facebook.ts +80 -0
  118. package/src/drivers/oauth-figma.ts +116 -0
  119. package/src/drivers/oauth-github.ts +92 -0
  120. package/src/drivers/oauth-google.ts +142 -0
  121. package/src/drivers/oauth-microsoft.ts +174 -0
  122. package/src/drivers/oauth-oidc.ts +293 -0
  123. package/src/drivers/oauth-shared.ts +326 -0
  124. package/src/drivers/oauth-types.ts +159 -0
  125. package/src/drivers/oauth-x.ts +77 -0
  126. package/src/drivers/oauth2-common.ts +95 -0
  127. package/src/drivers/oauth2-token.ts +61 -0
  128. package/src/drivers/pg-rls-row-passes.ts +251 -0
  129. package/src/drivers/pg-rls.ts +2 -0
  130. package/src/drivers/postgres.ts +45 -2
  131. package/src/drivers/signal-postgres.ts +2 -1
  132. package/src/elements/channel/runtime.ts +29 -2
  133. package/src/elements/channel.test.ts +52 -0
  134. package/src/elements/gate/boot.ts +29 -2
  135. package/src/elements/store/emit-drizzle.ts +147 -14
  136. package/src/elements/store/field-ddl.test.ts +118 -0
  137. package/src/elements/store/field-types.test.ts +455 -0
  138. package/src/elements/store/list-query.golden.json +777 -0
  139. package/src/elements/store/list-query.parity.test.ts +396 -0
  140. package/src/elements/store/list-query.ts +792 -0
  141. package/src/elements/store/live-default.test.ts +136 -0
  142. package/src/elements/store/live-http.test.ts +160 -0
  143. package/src/elements/store/live-isolation.test.ts +291 -0
  144. package/src/elements/store/live-query-runtime.test.ts +323 -0
  145. package/src/elements/store/live-query-runtime.ts +403 -0
  146. package/src/elements/store/live-query-server.test.ts +377 -0
  147. package/src/elements/store/live-query-server.ts +102 -0
  148. package/src/elements/store/live-query.ts +97 -0
  149. package/src/elements/store/resource.ts +189 -680
  150. package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
  151. package/src/elements/store/schema-decl.ts +539 -41
  152. package/src/elements/store/sql-rls-stamp.test.ts +27 -0
  153. package/src/elements/store/sql-session.ts +297 -35
  154. package/src/elements/store/table.ts +102 -21
  155. package/src/elements/store.test.ts +3 -1
  156. package/src/elements/store.ts +12 -1
  157. package/src/elements/vault/chaos-child.ts +74 -1
  158. package/src/elements/vault/chaos.test.ts +4 -2
  159. package/src/elements/vault/storage.ts +4 -2
  160. package/src/index.ts +5 -2
  161. package/src/kernel/app-auth.ts +1 -0
  162. package/src/kernel/app.ts +116 -2
  163. package/src/kernel/auth-sharing.test.ts +196 -0
  164. package/src/kernel/boot.test.ts +3 -3
  165. package/src/kernel/errors.ts +8 -0
  166. package/src/kernel/fx.test.ts +1 -0
  167. package/src/kernel/fx.ts +14 -2
  168. package/src/kernel/horizontal-child.ts +2 -1
  169. package/src/kernel/http-resource.ts +33 -7
  170. package/src/kernel/identity-host-persist.test.ts +119 -0
  171. package/src/kernel/instance-id.ts +4 -2
  172. package/src/kernel/journal.ts +2 -1
  173. package/src/kernel/mcp-tool.test.ts +95 -0
  174. package/src/kernel/on.ts +9 -0
  175. package/src/kernel/realtime-bind.ts +326 -0
  176. package/src/kernel/resource-live.ts +117 -0
  177. package/src/kernel/triggers.ts +86 -4
  178. package/src/manifest/diff.ts +37 -0
  179. package/src/manifest/types.ts +64 -2
  180. package/src/okid.bench.test.ts +64 -0
  181. package/src/okid.test.ts +338 -0
  182. package/src/okid.ts +245 -0
  183. package/src/plugins/anonymous.ts +19 -1
  184. package/src/plugins/auth/shared.ts +15 -0
  185. package/src/plugins/index.ts +2 -0
  186. package/src/plugins/magic-link.ts +10 -8
  187. package/src/plugins/mcp-oauth.ts +208 -0
  188. package/src/plugins/oauth/flow-store.ts +117 -0
  189. package/src/plugins/oauth/link.ts +69 -0
  190. package/src/plugins/oauth/shared.ts +108 -0
  191. package/src/plugins/oauth/token-vault.ts +100 -0
  192. package/src/plugins/oauth.security.test.ts +535 -0
  193. package/src/plugins/oauth.ts +532 -0
  194. package/src/plugins/otp.ts +48 -6
  195. package/src/plugins/passkey.ts +20 -1
  196. package/src/plugins/two-factor.ts +11 -0
  197. package/src/plugins/username.ts +40 -7
  198. package/src/release/build-lib.ts +7 -1
  199. package/src/release/measure.ts +1 -0
  200. package/src/release/official-plugins.ts +4 -1
  201. package/src/runs/collect.ts +2 -1
  202. package/src/runs/drivers/files.ts +2 -1
  203. package/src/test/create-test-app.ts +114 -5
  204. package/src/test/export-bundle.test.ts +33 -0
  205. package/src/test/live-signals.test.ts +83 -0
  206. package/src/test/tenant-isolation.test.ts +175 -0
  207. package/src/testing.ts +26 -0
  208. package/src/upgrade/codemods.ts +1 -1
  209. 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: The six seams every TypeScript backend maintains by hand and what OKE derives from one Manifest instead.
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
- Every TypeScript backend works on day one. The bill arrives later: the cache
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
- None of these are router problems. They are **seams** copies of your code's
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
- <Callout title="The one rule">
16
- **All world access goes through `fx`.** What a Flow reads, writes, emits, and reveals is recorded
17
- — so the seams below are derived from one Manifest, not re-typed per project.
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 six seams
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
- ### The cache that lies
24
+ ### Cache that lies
23
25
 
24
- You add a column to `orders` and update three writers. The hand-bumped cache
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
- **OKE derives:** reads and writes are recorded through `fx`, so invalidation
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
- ### The secret that fails in prod
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
- **OKE derives:** [Vault](/docs/elements/vault) contracts declare the need in
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
- **OKE derives:** the official `okengine/plugins` set ships this glue once —
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
- **OKE derives:** its operator interface reads the Manifest —
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
- ### The permission check in the wrong place
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
- **OKE derives:** declared effects produce a least-privilege matrix — widening
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
- for the same database. "Works on my machine" is a driver mismatch.
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
- **OKE derives:** drivers are named after protocols (`postgres`, `redis`,
70
- `s3`), the vendor lives in `images`, and `oke dev` runs the real
71
- stack locally.
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
- ## The tax is drift
95
+ ## Eight elements a closed set
74
96
 
75
- Every seam above is the same shape: a hand-maintained copy of knowledge the
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
- <DriftBoard />
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
- On the left, versions scatter and stay scattered. On the right, one Manifest
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 answer's shape
112
+ ## The shape of a Flow
84
113
 
85
- Forty infrastructure concerns collapse into eight elements each kept only
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
- <CollapseBoard />
116
+ ```ts
117
+ import { on, flow, http } from "okengine/http";
90
118
 
91
- New infrastructure becomes a **driver** for an existing element, never a
92
- ninth element — the set of eight is closed.
119
+ export const health = on(http.get().public(), flow({ do: () => ({ ok: true }) }));
120
+ ```
93
121
 
94
- ## Traditional vs OKE
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
- | Seam | Maintained by hand | Derived by OKE |
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
- | | Statement |
110
- | --------------- | ------------------------------------------------------------------------------------------------ |
111
- | **Ambition** | The default, most capable TypeScript backend — Bun-first, Web-Standards portable, contract-first |
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, ten exports
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, ten exports."
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. Ten exports."
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. Pick a section below or start with Get Started if you are new.
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
- ## Browse by section
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="One law install first flows."
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="Official extensions — security, ops, performance."
50
+ description="22 official extensions — authentication, OAuth, security, ops, performance."
32
51
  href="/docs/plugins"
33
52
  />
34
53
  <Card
35
- title="Deployment"
36
- description="Compose, Swarm, Kubernetes, reverse proxy."
37
- href="/docs/deployment"
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="Config, fx, env, errors, CLI, security."
65
+ description="Configuration, fx ledger, environment variables, errors, client, and security."
42
66
  href="/docs/reference"
43
67
  />
44
- <Card title="AI Resources" description="MCP, skills, /llms.txt." href="/docs/ai" />
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
- "get-started",
6
+ "---Understand OKE---",
7
7
  "elements",
8
- "plugins",
8
+ "---Build with OKE---",
9
+ "get-started",
10
+ "ai",
9
11
  "deployment",
10
- "recipes",
11
- "providers",
12
12
  "reference",
13
- "ai"
13
+ "plugins",
14
+ "---Drivers---",
15
+ "providers",
16
+ "recipes"
14
17
  ]
15
18
  }