okengine 0.1.6 → 0.2.1

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 (52) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +36 -29
  3. package/docs/spec/example.md +1187 -0
  4. package/docs/spec/unified-theory.md +1 -1
  5. package/package.json +19 -6
  6. package/src/cli/dev.ts +3 -1
  7. package/src/cli/doc-drift.ts +54 -21
  8. package/src/console/index.ts +25 -13
  9. package/src/console/server/app.ts +44 -7
  10. package/src/console/server/flows.ts +2 -6
  11. package/src/console/server/index.ts +2 -1
  12. package/src/console/server/lazy-panels.test.ts +27 -0
  13. package/src/console/server/panel-load.ts +28 -0
  14. package/src/console/server/plugin.ts +1 -1
  15. package/src/console/server/plugins.ts +7 -6
  16. package/src/console/server/public-flows.ts +12 -0
  17. package/src/console/server/state.ts +159 -122
  18. package/src/console/server/store.ts +13 -10
  19. package/src/drivers/index.ts +1 -6
  20. package/src/drivers/vault-sops.ts +20 -1
  21. package/src/kernel/app.ts +1 -1
  22. package/src/kernel/boot-bind/ai.ts +31 -0
  23. package/src/kernel/boot-bind/channel.ts +27 -0
  24. package/src/kernel/boot-bind/clock.ts +74 -0
  25. package/src/kernel/boot-bind/gate.ts +28 -0
  26. package/src/kernel/boot-bind/runs.ts +28 -0
  27. package/src/kernel/boot-bind/signal.ts +68 -0
  28. package/src/kernel/boot-bind/store.ts +47 -0
  29. package/src/kernel/boot-bind/vault.ts +36 -0
  30. package/src/kernel/boot.test.ts +85 -1
  31. package/src/kernel/boot.ts +250 -212
  32. package/src/kernel/index.ts +2 -0
  33. package/src/mcp/data.ts +1 -0
  34. package/src/mcp/docs-index.ts +252 -0
  35. package/src/mcp/docs-mcp.test.ts +176 -0
  36. package/src/mcp/docs-server.ts +233 -0
  37. package/src/mcp/docs-tools.ts +143 -0
  38. package/src/mcp/index.ts +29 -5
  39. package/src/mcp/protocol.ts +2 -1
  40. package/src/release/exports.test.ts +71 -0
  41. package/src/release/exports.ts +156 -0
  42. package/src/release/index.ts +21 -0
  43. package/src/release/limits.ts +9 -0
  44. package/src/release/measure.exports.test.ts +82 -0
  45. package/src/release/measure.ts +297 -14
  46. package/src/release/publish.ts +14 -3
  47. package/src/release/readme.test.ts +61 -0
  48. package/src/release/readme.ts +13 -0
  49. package/src/runtime/index.ts +1 -0
  50. package/src/runtime/security.test.ts +6 -1
  51. package/src/runtime/types.ts +6 -0
  52. package/src/test/create-test-app.ts +2 -0
@@ -0,0 +1,1187 @@
1
+ # OKE — Four Applications
2
+ ### A progressive path from one flow to a full system
3
+
4
+ **Package:** `okengine` · **CLI:** `oke`
5
+
6
+ Four complete, runnable applications. Each one introduces the smallest possible set of new ideas, and each ends by naming the limitation that motivates the next. Read them in order and the eight elements arrive one or two at a time instead of all at once.
7
+
8
+ | | App | Teaches | Elements | Exports | Files |
9
+ |---|---|---|---|---|---|
10
+ | **1 · Basic** | Notes | the one law · contracts · typed errors · the client | Flow · Store | 4 / 10 | 5 |
11
+ | **2 · Intermediate** | Linkly | one species, many triggers · delivery physics · transactional emit | + Signal · Clock · Gate | 7 / 10 | 11 |
12
+ | **3 · Advanced** | Provisions | durability · reaching humans · live queries · plugins | + Vault · Channel | 10 / 10 | 18 |
13
+ | **4 · Complex** | Skyport | AI and agents · tenancy · SLOs · distributed topology | all eight | all ten | 24 |
14
+
15
+ **The ten exports:** `on · flow · signal · store · clock · gate · vault · channel · ai · plugin`
16
+ **The one law:** `on(Trigger) → Effects`
17
+
18
+ ---
19
+ ---
20
+
21
+ # 1 · BASIC — Notes
22
+
23
+ **New ideas:** `oke`, `on`, `flow`, `http`, `store.sql`, `fx`, typed errors, the typed client.
24
+ **Time to running:** about two minutes.
25
+
26
+ ```
27
+ notes/
28
+ ├── oke.config.ts
29
+ ├── src/
30
+ │ ├── app.ts
31
+ │ ├── core.ts
32
+ │ ├── schema.ts
33
+ │ └── flows/notes/index.ts
34
+ └── tests/notes.test.ts
35
+ ```
36
+
37
+ Five files. Contracts live beside the flows that use them — a separate `shapes.ts` arrives in the next app, at the size where it starts to help.
38
+
39
+ ### `oke.config.ts`
40
+
41
+ ```typescript
42
+ import { defineConfig } from "okengine/config";
43
+
44
+ export default defineConfig({
45
+ drivers: {
46
+ store: { sql: { dev: "sqlite", test: "memory", prod: "postgres" } },
47
+ },
48
+ });
49
+ ```
50
+
51
+ That is the whole configuration. Drivers are named after **protocols**, so `postgres` covers Postgres, Neon, Supabase and RDS alike.
52
+
53
+ ### `src/schema.ts`
54
+
55
+ ```typescript
56
+ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
57
+ import { id, now } from "okengine/store";
58
+
59
+ export const notes = sqliteTable("notes", {
60
+ id: text("id").primaryKey().$defaultFn(id),
61
+ title: text("title").notNull(),
62
+ body: text("body").notNull(),
63
+ createdAt: integer("created_at").notNull().$defaultFn(now),
64
+ });
65
+ ```
66
+
67
+ **Defaults belong in the schema.** `$defaultFn(id)` means no handler ever writes id-generation boilerplate. (`fx.id()` still exists and is required in one specific case — see the Store reference at the end.)
68
+
69
+ Drizzle is a **required peer dependency** — never bundled, always your version, and your schema file is yours. The framework commits to Drizzle rather than abstracting over ORMs, for a reason that is architectural rather than aesthetic; the Store reference explains it.
70
+
71
+ ### `src/core.ts`
72
+
73
+ ```typescript
74
+ import { store } from "okengine";
75
+ import * as schema from "./schema";
76
+
77
+ export const db = store.sql("notes", { schema });
78
+ ```
79
+
80
+ ### `src/flows/notes/index.ts`
81
+
82
+ ```typescript
83
+ import { on, flow, http } from "okengine";
84
+ import { createInsertSchema, createSelectSchema } from "drizzle-zod";
85
+ import { z } from "zod";
86
+ import { db } from "../../core";
87
+ import { notes } from "../../schema";
88
+
89
+ // Contracts derived from the schema — one source of truth, refined where the API is stricter
90
+ const NewNote = createInsertSchema(notes, { title: (s) => s.min(1).max(120) })
91
+ .omit({ id: true, createdAt: true });
92
+ const Note = createSelectSchema(notes);
93
+ const NoteId = z.object({ id: z.string() });
94
+ const NotFound = z.object({});
95
+
96
+ export const create = on(http.post("/notes"), flow({
97
+ in: NewNote,
98
+ out: NoteId,
99
+ do: async (input, fx) => {
100
+ const [note] = await fx.store(db).insert(notes).values(input).returning();
101
+ return { id: note.id };
102
+ },
103
+ }));
104
+ // effects → writes[sql:notes]
105
+
106
+ export const list = on(http.get("/notes"), flow({
107
+ out: Note.array(),
108
+ do: (_, fx) => fx.store(db).select().from(notes),
109
+ }));
110
+ // effects → reads[sql:notes]
111
+
112
+ export const get = on(http.get("/notes/:id"), flow({
113
+ in: NoteId,
114
+ out: Note,
115
+ errors: { NotFound },
116
+ do: async ({ id }, fx) => (await fx.store(db).findById(notes, id)) ?? fx.fail("NotFound", {}),
117
+ }));
118
+
119
+ export const remove = on(http.delete("/notes/:id"), flow({
120
+ in: NoteId,
121
+ errors: { NotFound },
122
+ do: async ({ id }, fx) => {
123
+ const deleted = await fx.store(db).delete(notes, id);
124
+ if (!deleted) return fx.fail("NotFound", {});
125
+ },
126
+ }));
127
+ ```
128
+
129
+ **Four things to notice.**
130
+
131
+ `fx` is the only door to the outside world. Every read and every write passes through it — which is what lets the framework know that `create` writes `notes` and `list` reads it, with no annotation from you.
132
+
133
+ **Contracts are derived, not retyped.** `drizzle-zod` turns the table into request and response schemas, refined where the API should be stricter than storage. When the two genuinely diverge — internal columns, computed responses, a different input shape — write the schema by hand instead. Derive when they agree; hand-write when they don't.
134
+
135
+ **Errors are values, not exceptions.** `fx.fail("NotFound", {})` returns a typed error the client will narrow on. There is no `throw` and no `catch (e: any)`.
136
+
137
+ **No cache configuration appears anywhere.** `list` is cached automatically, and invalidated by exactly the writes that touch the rows it read — because the compiler knows both.
138
+
139
+ ### `src/app.ts`
140
+
141
+ ```typescript
142
+ import { oke } from "okengine";
143
+ import * as notes from "./flows/notes";
144
+
145
+ export const app = oke({ name: "notes" }).adopt({ notes });
146
+
147
+ export type App = typeof app; // ← the client needs nothing else
148
+ ```
149
+
150
+ `on()` still registers each flow with the router and the Manifest — `.adopt()` exists so the type of `app` accumulates every contract in `notes`, which is what lets the client below need no hand-written types and no separate codegen step. The namespace key (`notes`) becomes the client's namespace; each export becomes a method.
151
+
152
+ ### The client
153
+
154
+ ```typescript
155
+ import { createClient } from "okengine/client";
156
+ import type { App } from "../src/app";
157
+ import { app } from "../src/app";
158
+
159
+ const api = createClient<App>("http://localhost:6530", { $routes: app.$routes });
160
+ // equivalently: const api = createClient(app, "http://localhost:6530");
161
+
162
+ const { data, error } = await api.notes.get({ id: "n_1" });
163
+
164
+ if (error?.code === "NotFound") show("gone");
165
+ else console.log(data.title); // ← typed, no codegen ✅
166
+ // GET /notes/n_1 — the method and path are derived from the flow's own trigger,
167
+ // not from a separate RPC convention.
168
+ ```
169
+
170
+ ### `tests/notes.test.ts`
171
+
172
+ ```typescript
173
+ import { test, expect } from "bun:test";
174
+ import { createTestApp } from "okengine/test";
175
+ import { app } from "../src/app";
176
+
177
+ test("create then read", async () => {
178
+ const t = await createTestApp(app); // memory driver, automatic
179
+ const { data } = await t.api.notes.create({ title: "First", body: "Hello" });
180
+ const { data: note } = await t.api.notes.get({ id: data!.id });
181
+ expect(note!.title).toBe("First");
182
+ });
183
+ ```
184
+
185
+ ### Run it
186
+
187
+ ```bash
188
+ bun add okengine
189
+ oke dev # app :6530 · Console :6533 · MCP :6535
190
+ bun test
191
+ ```
192
+
193
+ Open `:6533` and the Console already shows the four flows, their contracts, their effects, and a live architecture diagram — derived, not configured.
194
+
195
+ ### What you have
196
+
197
+ Four exports (`oke`, `on`, `flow`, `store`), two elements, a typed client, automatic caching, and a Console.
198
+
199
+ ### What is missing
200
+
201
+ Everything here is synchronous. A real application needs work that happens *later* — after the response, on a schedule, or in reaction to something. That is the next app.
202
+
203
+ ---
204
+ ---
205
+
206
+ # 2 · INTERMEDIATE — Linkly
207
+
208
+ A URL shortener that counts clicks.
209
+
210
+ **New ideas:** `signal` and its three delivery physics · `clock` · `gate` · triggers beyond HTTP · transactional emit · cross-unit decoupling.
211
+
212
+ ```
213
+ linkly/
214
+ ├── oke.config.ts
215
+ ├── src/
216
+ │ ├── app.ts
217
+ │ ├── core.ts
218
+ │ ├── gates.ts
219
+ │ ├── schema.ts
220
+ │ └── flows/
221
+ │ ├── links/
222
+ │ │ ├── index.ts
223
+ │ │ ├── shapes.ts
224
+ │ │ └── signals.ts
225
+ │ └── analytics/index.ts
226
+ └── tests/linkly.test.ts
227
+ ```
228
+
229
+ ### `oke.config.ts`
230
+
231
+ ```typescript
232
+ import { defineConfig } from "okengine/config";
233
+
234
+ export default defineConfig({
235
+ drivers: {
236
+ store: { sql: { dev: "sqlite", test: "memory", prod: "postgres" },
237
+ kv: { dev: "memory", test: "memory", prod: "redis" } },
238
+ signal: { dev: "memory", test: "memory", prod: "postgres" },
239
+ clock: { dev: "memory", test: "frozen", prod: "postgres" },
240
+ },
241
+ });
242
+ ```
243
+
244
+ `signal` defaults to `postgres`, and the reason is correctness rather than throughput — see the note after `redirect` below.
245
+
246
+ ### `src/gates.ts`
247
+
248
+ ```typescript
249
+ import { gate } from "okengine";
250
+
251
+ export const member = gate.policy("member", ({ auth }) => !!auth?.verified);
252
+
253
+ export const fair = gate.rate({
254
+ strategy: "sliding-window-counter", // near-exact, two keys, no boundary bursts
255
+ max: 60, per: "1m", keyBy: "ip",
256
+ });
257
+ ```
258
+
259
+ Five strategies exist (`fixed-window`, `sliding-log`, `token-bucket`, `leaky-bucket`); this one is the default because it has the best accuracy-to-cost ratio.
260
+
261
+ ### `src/schema.ts`
262
+
263
+ ```typescript
264
+ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
265
+
266
+ export const links = sqliteTable("links", {
267
+ id: text("id").primaryKey(), // `increment` targets this column
268
+ code: text("code").notNull().unique(), // the short, human-facing key
269
+ url: text("url").notNull(),
270
+ userId: text("user_id").notNull(),
271
+ clicks: integer("clicks").notNull().default(0),
272
+ createdAt: integer("created_at").notNull(),
273
+ });
274
+
275
+ export const daily = sqliteTable("daily", {
276
+ id: text("id").primaryKey(),
277
+ code: text("code").notNull(),
278
+ day: text("day").notNull(), // "YYYY-MM-DD"
279
+ clicks: integer("clicks").notNull().default(0),
280
+ });
281
+ ```
282
+
283
+ A generated `id` plus a separate unique `code` is the standard shape for a shortener: `increment` — see below — is a store-level primitive that targets a row by its primary key, so every table it touches needs one.
284
+
285
+ ### `src/flows/links/signals.ts`
286
+
287
+ ```typescript
288
+ import { signal } from "okengine";
289
+ import { z } from "zod";
290
+
291
+ export const linkClicked = signal("link-clicked", {
292
+ schema: z.object({ code: z.string(), at: z.number(), referrer: z.string().optional() }),
293
+ delivery: "once", // queue physics: one consumer, retries, DLQ
294
+ retries: 3, deadLetter: true,
295
+ });
296
+
297
+ export const linkStats = signal("link-stats", {
298
+ schema: z.object({ code: z.string(), clicks: z.number() }),
299
+ delivery: "live", // stream physics: clients subscribe
300
+ });
301
+ ```
302
+
303
+ **`delivery` is mandatory with no default.** Queue, pub/sub and stream were always the same object with different delivery physics, so physics is an option — but choosing it is a semantic decision and guessing it produces silent, expensive bugs.
304
+
305
+ ### `src/flows/links/index.ts`
306
+
307
+ ```typescript
308
+ import { on, flow, http, every } from "okengine";
309
+ import { eq, lt } from "drizzle-orm";
310
+ import { db } from "../../core";
311
+ import { member, fair } from "../../gates";
312
+ import { linkClicked, linkStats } from "./signals";
313
+ import { NewLink, LinkCode, Link, NotFound, Taken } from "./shapes";
314
+ import { links } from "../../schema";
315
+
316
+ // ① HTTP — "an endpoint"
317
+ export const shorten = on(http.post("/links").gate(member, fair), flow({
318
+ in: NewLink, out: LinkCode, errors: { Taken },
319
+ do: async ({ url, code }, fx) => {
320
+ if (await fx.store(db).exists(links, { code })) return fx.fail("Taken", {});
321
+ const id = fx.id();
322
+ await fx.store(db).insert(links).values(
323
+ { id, code, url, userId: fx.auth.userId, clicks: 0, createdAt: Date.now() });
324
+ return { code };
325
+ },
326
+ }));
327
+
328
+ // ② HTTP — the hot path
329
+ export const redirect = on(http.get("/:code").gate(fair), flow({
330
+ in: LinkCode, out: Link, errors: { NotFound },
331
+ do: async ({ code }, fx) => {
332
+ const [link] = await fx.store(db).select().from(links).where(eq(links.code, code)).limit(1);
333
+ if (!link) return fx.fail("NotFound", {});
334
+
335
+ await fx.emit(linkClicked, { code, at: Date.now() }); // same transaction as any write
336
+ return link;
337
+ },
338
+ }));
339
+
340
+ // ③ SIGNAL — "a queue consumer", and the same species as ① and ②
341
+ on(linkClicked, flow({
342
+ do: async ({ code }, fx) => {
343
+ const [link] = await fx.store(db).select().from(links).where(eq(links.code, code)).limit(1);
344
+ const clicks = await fx.store(db).increment(links, link.id, "clicks");
345
+ await fx.emit(linkStats, { code, clicks }); // live: pushed to subscribers
346
+ },
347
+ }));
348
+
349
+ // ④ CLOCK — "a cron job", and still the same species
350
+ on(every("1h"), flow({
351
+ do: (_, fx) => {
352
+ const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; // 30 days
353
+ return fx.store(db).delete(links).where(lt(links.createdAt, cutoff));
354
+ },
355
+ }));
356
+
357
+ // ⑤ A plain flow with no trigger — callable, not "private"
358
+ export const stats = flow({
359
+ in: LinkCode, out: z.object({ clicks: z.number() }),
360
+ do: async ({ code }, fx) => {
361
+ const [link] = await fx.store(db).select({ clicks: links.clicks })
362
+ .from(links).where(eq(links.code, code)).limit(1);
363
+ return link ?? { clicks: 0 };
364
+ },
365
+ });
366
+ ```
367
+
368
+ **Why `emit` inside the transaction matters.** The dual-write bug is the most common distributed-systems mistake: you write the record, then publish the message; a crash between them loses the message, or a rollback after publishing sends mail about something that does not exist. On the Postgres driver `fx.emit` enrols in the same transaction as `fx.store` writes, automatically. When you later switch to `redis` or `nats` for throughput, the driver keeps an outbox relay internally, so the guarantee does not regress — the upgrade is purely about speed.
369
+
370
+ ### `src/flows/analytics/index.ts`
371
+
372
+ ```typescript
373
+ import { on, flow, http } from "okengine";
374
+ import { z } from "zod";
375
+ import { eq, and } from "drizzle-orm";
376
+ import { linkClicked } from "../links/signals";
377
+ import { db } from "../../core";
378
+ import { member } from "../../gates";
379
+ import { daily } from "../../schema";
380
+
381
+ on(linkClicked, flow({ // a second consumer of the same signal
382
+ do: async ({ code, at }, fx) => {
383
+ const day = new Date(at).toISOString().slice(0, 10);
384
+ const [row] = await fx.store(db).select().from(daily)
385
+ .where(and(eq(daily.code, code), eq(daily.day, day))).limit(1);
386
+
387
+ if (row) await fx.store(db).increment(daily, row.id, "clicks");
388
+ else await fx.store(db).insert(daily).values({ id: fx.id(), code, day, clicks: 1 });
389
+ },
390
+ }));
391
+
392
+ export const report = on(http.get("/links/:code/report").gate(member), flow({
393
+ out: z.array(z.object({ day: z.string(), clicks: z.number() })),
394
+ do: ({ code }, fx) => fx.store(db).select({ day: daily.day, clicks: daily.clicks })
395
+ .from(daily).where(eq(daily.code, code)),
396
+ }));
397
+ ```
398
+
399
+ This unit never imports anything from `links` except the signal declaration. Decoupling is structural, not a discipline.
400
+
401
+ ### `src/app.ts`
402
+
403
+ ```typescript
404
+ import { oke } from "okengine";
405
+ import * as links from "./flows/links";
406
+ import * as analytics from "./flows/analytics";
407
+
408
+ export const app = oke({ name: "linkly" }).adopt({ links, analytics });
409
+
410
+ export type App = typeof app;
411
+ ```
412
+
413
+ ### The client — realtime with no realtime code
414
+
415
+ ```typescript
416
+ const { data, error } = await api.links.shorten({ url: "https://example.com", code: "sa" });
417
+ if (error?.code === "Taken") suggestAnother();
418
+
419
+ api.signals.linkStats.subscribe(({ code, clicks }) => paint(code, clicks));
420
+ ```
421
+
422
+ ### `tests/linkly.test.ts`
423
+
424
+ ```typescript
425
+ const t = await createTestApp(app); // memory drivers, frozen clock
426
+ const u = await t.auth.loginAs({});
427
+
428
+ await t.api.links.shorten({ url: "https://example.com", code: "sa" }, { as: u });
429
+ await t.api.links.redirect({ code: "sa" });
430
+ await t.signals.drain(); // run queued work deterministically
431
+
432
+ const { data } = await t.api.links.report({ code: "sa" }, { as: u });
433
+ expect(data![0].clicks).toBe(1);
434
+
435
+ await t.clock.advance("31d");
436
+ await t.cron.run("1h"); // time travel
437
+ ```
438
+
439
+ ### What you have
440
+
441
+ Seven exports, five elements, and five different trigger kinds — all of them the same `flow` object. There is no separate API for endpoints, consumers, cron jobs or internal functions.
442
+
443
+ ### What is missing
444
+
445
+ Nothing here survives a deploy. A payment that must wait two minutes for confirmation, an email that must actually reach a person, an order page that updates itself — none of that is expressible yet.
446
+
447
+ ---
448
+ ---
449
+
450
+ # 3 · ADVANCED — Provisions
451
+
452
+ A subscription store: orders, payments, notifications.
453
+
454
+ **New ideas:** `durable` flows and the journal · `vault` · `channel` with fallback chains and i18n · live queries · plugins · a CDC trigger · the three cache tiers.
455
+
456
+ ```
457
+ provisions/
458
+ ├── oke.config.ts
459
+ ├── src/
460
+ │ ├── app.ts
461
+ │ ├── core.ts # the primary database
462
+ │ ├── gates.ts # shared gates
463
+ │ ├── vault.ts # every secret contract, one auditable file
464
+ │ ├── channels.ts # how we reach humans
465
+ │ ├── locales/{en,ar}.ts
466
+ │ ├── plugins/audit.ts
467
+ │ ├── schema.ts
468
+ │ └── flows/
469
+ │ ├── orders/{index.ts,shapes.ts,signals.ts}
470
+ │ ├── payments/{index.ts,shapes.ts}
471
+ │ └── notifications/index.ts
472
+ └── tests/orders.test.ts
473
+ ```
474
+
475
+ **The layout rule from here on: whoever *produces* an element declares it; consumers import it.** Shared concerns (the database, shared gates, secrets, channels) sit at the root. The framework never forces this — the Manifest is built from the import graph — but the tree teaches the vocabulary.
476
+
477
+ ### `src/schema.ts`
478
+
479
+ ```typescript
480
+ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
481
+
482
+ export const products = sqliteTable("products", {
483
+ sku: text("sku").primaryKey(),
484
+ name: text("name").notNull(),
485
+ stock: integer("stock").notNull().default(0),
486
+ });
487
+
488
+ export const orders = sqliteTable("orders", {
489
+ id: text("id").primaryKey(),
490
+ userId: text("user_id").notNull(),
491
+ sku: text("sku").notNull(),
492
+ qty: integer("qty").notNull(),
493
+ status: text("status").notNull().default("pending"),
494
+ createdAt: integer("created_at").notNull(),
495
+ });
496
+ ```
497
+
498
+ ### `src/vault.ts`
499
+
500
+ ```typescript
501
+ import { vault } from "okengine";
502
+ import { z } from "zod";
503
+
504
+ // A declaration is a CONTRACT, not a value.
505
+ // Resolution: process.env → .env.local → .env.stack → vault driver → dev fallback
506
+ export const stripeKey = vault.secret("STRIPE_KEY", {
507
+ schema: z.string().startsWith("sk_"),
508
+ description: "Payments gateway key",
509
+ rotate: "90d",
510
+ dev: "sk_test_local",
511
+ });
512
+
513
+ export const dbUrl = vault.secret("DATABASE_URL", {
514
+ schema: z.string().url(),
515
+ dev: vault.fromStack("store.sql"), // generated by `oke dev --stack` — zero manual setup
516
+ });
517
+ ```
518
+
519
+ Missing or invalid at boot, `oke doctor` lists **all** of them at once with their descriptions — before a single request is served. Values are never readable from the Console; only fingerprints are shown.
520
+
521
+ ### `src/channels.ts`
522
+
523
+ ```typescript
524
+ import { channel } from "okengine";
525
+ import { z } from "zod";
526
+
527
+ export const mail = channel.email({ from: "Provisions <no-reply@provisions.sa>" });
528
+ export const sms = channel.sms({ sender: "PROVISIONS" });
529
+ export const wa = channel.whatsapp();
530
+
531
+ export const orderConfirmed = mail.template("order-confirmed", {
532
+ schema: z.object({ name: z.string(), orderId: z.string(), total: z.number() }),
533
+ });
534
+
535
+ export const otpCode = channel.template("otp-code", { // medium-agnostic
536
+ schema: z.object({ code: z.string() }),
537
+ });
538
+ ```
539
+
540
+ Recipient address, language and opt-out consent are resolved from the user automatically. In development the `console` driver puts every medium into a built-in inbox instead of sending.
541
+
542
+ ### `src/flows/orders/index.ts`
543
+
544
+ ```typescript
545
+ import { on, flow, gate, http } from "okengine";
546
+ import { eq } from "drizzle-orm";
547
+ import { db } from "../../core";
548
+ import { member } from "../../gates";
549
+ import { orderPlaced, orderNews } from "./signals";
550
+ import { chargeOrder } from "../payments";
551
+ import { NewOrder, OrderId, OrderRow, OutOfStock } from "./shapes";
552
+ import { orders, products } from "../../schema";
553
+
554
+ const canOrder = gate.policy("order:create", ({ auth }) => auth.scopes.has("order:create"));
555
+
556
+ export const create = on(http.post("/orders").gate(member, canOrder), flow({
557
+ in: NewOrder, out: OrderId, errors: { OutOfStock },
558
+ do: async (input, fx) => {
559
+ const [product] = await fx.store(db).select({ stock: products.stock })
560
+ .from(products).where(eq(products.sku, input.sku)).limit(1);
561
+ if (!product || product.stock < input.qty) return fx.fail("OutOfStock",
562
+ { left: product?.stock ?? 0 },
563
+ { message: fx.t("order.outOfStock", { left: product?.stock ?? 0 }) });
564
+
565
+ const id = fx.id();
566
+ await fx.store(db).insert(orders).values(
567
+ { id, userId: fx.auth.userId, ...input, status: "pending", createdAt: Date.now() });
568
+ await fx.emit(orderPlaced, { orderId: id });
569
+ return { id };
570
+ },
571
+ }));
572
+
573
+ // LIVE QUERY — realtime and auto-caching from one flag
574
+ export const mine = on(http.get("/orders").gate(member).live(), flow({
575
+ out: OrderRow.array(),
576
+ do: (_, fx) => fx.store(db).select().from(orders).where(eq(orders.userId, fx.auth.userId)),
577
+ }));
578
+
579
+ export const getOrder = flow({
580
+ in: OrderId, out: OrderRow,
581
+ do: async ({ id }, fx) => {
582
+ const [order] = await fx.store(db).select().from(orders).where(eq(orders.id, id)).limit(1);
583
+ return order;
584
+ },
585
+ });
586
+
587
+ // SIGNAL consumer
588
+ on(orderPlaced, flow({
589
+ do: async ({ orderId }, fx) => {
590
+ const paid = await fx.call(chargeOrder, { orderId });
591
+ await fx.store(db).update(orders).set({ status: paid ? "confirmed" : "failed" })
592
+ .where(eq(orders.id, orderId));
593
+ await fx.emit(orderNews, { orderId, status: paid ? "confirmed" : "failed" });
594
+ },
595
+ }));
596
+
597
+ // CHANGE trigger — CDC, built in
598
+ on(db.table(orders).changed("status"), flow({
599
+ do: ({ before, after }, fx) => fx.log.info("status", { from: before.status, to: after.status }),
600
+ }));
601
+ ```
602
+
603
+ **`.live()` is the whole of realtime.** The result is cached, invalidated by exactly the writes that touch those rows, and pushed to subscribed clients on exactly those writes. No cache code, no socket code.
604
+
605
+ ### `src/flows/payments/index.ts` — durability is a flag
606
+
607
+ ```typescript
608
+ import { flow } from "okengine";
609
+ import { z } from "zod";
610
+ import { stripeKey } from "../../vault";
611
+ import { OrderRef } from "./shapes";
612
+
613
+ export const chargeOrder = flow({
614
+ durable: true, // every fx call below is journaled
615
+ in: OrderRef, out: z.boolean(),
616
+ do: async ({ orderId }, fx) => {
617
+ const intent = await fx.step("create-intent", () => // never re-runs on replay
618
+ stripe(fx.vault(stripeKey)).create(orderId));
619
+
620
+ await fx.clock.sleep("verify-window", "2m"); // survives restart and deploy
621
+
622
+ return fx.step("confirm", () => stripe(fx.vault(stripeKey)).confirm(intent));
623
+ },
624
+ });
625
+ ```
626
+
627
+ **Workflows are not a separate API.** They are ordinary flows with one option. A process killed between the two steps resumes at `confirm` — the card is not charged twice.
628
+
629
+ ### `src/flows/notifications/index.ts` — reaching humans
630
+
631
+ ```typescript
632
+ import { on, flow } from "okengine";
633
+ import { z } from "zod";
634
+ import { orderNews } from "../orders/signals";
635
+ import { getOrder } from "../orders";
636
+ import { orderConfirmed, otpCode, wa, sms } from "../../channels";
637
+
638
+ on(orderNews, flow({
639
+ do: async ({ orderId, status }, fx) => {
640
+ if (status !== "confirmed") return;
641
+ const o = await fx.call(getOrder, { id: orderId });
642
+ await fx.send(orderConfirmed, { to: o.userId, data: { name: o.userName, orderId, total: o.total } });
643
+ },
644
+ }));
645
+
646
+ export const sendOtp = flow({
647
+ in: z.object({ userId: z.string(), code: z.string() }),
648
+ do: ({ userId, code }, fx) => fx.send(otpCode, { to: userId, via: [wa, sms], data: { code } }),
649
+ // ↑ fallback chain: WhatsApp, else SMS
650
+ });
651
+ ```
652
+
653
+ Fallback is recorded as a **chain**, not an outcome — so the Console can tell you that 23% of OTPs fell back to SMS this week and what that cost.
654
+
655
+ ### `src/plugins/audit.ts` — every extension point in one file
656
+
657
+ ```typescript
658
+ import { plugin, store } from "okengine";
659
+ import { z } from "zod";
660
+
661
+ export const audit = plugin("audit", { version: "1.0.0" })
662
+ .config(z.object({ retain: z.string().default("2y") }))
663
+ .element(store.sql("audit", { schema: () => import("./audit-schema") }))
664
+ .needs("store.kv")
665
+ .decorate("audit", { enabled: true })
666
+ .hook("afterHandle", async (ctx, fx) => {
667
+ if (ctx.trigger.meta?.audit) await fx.store("audit").log(ctx);
668
+ })
669
+ .errors({ AuditWriteFailed: z.object({ reason: z.string() }) })
670
+ .consolePanel({ id: "audit", title: "Audit Trail", entry: "./panel.tsx" })
671
+ .cli("audit:export", ({ fx }) => fx.store("audit").exportCsv());
672
+ ```
673
+
674
+ ### `src/app.ts` — scope is the attachment point
675
+
676
+ ```typescript
677
+ import { oke } from "okengine";
678
+ import { auth } from "okengine/auth";
679
+ import { audit } from "./plugins/audit";
680
+ import * as orders from "./flows/orders";
681
+ import * as payments from "./flows/payments";
682
+ import * as notifications from "./flows/notifications";
683
+
684
+ export const app = oke({ name: "provisions" })
685
+ .adopt({ orders, payments, notifications })
686
+ .plug(auth()) // zero ceremony: uses your configured store
687
+ .plug(audit) // app-wide
688
+ .hook("onError", (ctx, err, fx) => fx.log.error(err));
689
+
690
+ app.unit("orders").plug(rateLimit({ max: 30 })); // this unit only
691
+
692
+ export type App = typeof app;
693
+ ```
694
+
695
+ `app.plug()` is app-wide, `app.unit(name).plug()` covers one unit, `flow.plug()` covers one flow. **The position is the scope** — no `global: true`, no inheritance rule to remember. `.adopt()` is what makes `typeof app` carry every flow's contract for the client; `on()` inside each flow file still does the actual trigger registration.
696
+
697
+ **Auth needs no adapter.** The framework already knows your store; its tables come from `oke schema generate`. Options exist when you want them, and the identity provider is a seam — `auth({ provider: betterAuth(...) })`, `clerk()`, `supabase()`, `auth0()`, `kinde()` all normalise to the same `fx.auth`, so gates, ABAC, rate limits and channel recipients keep working unchanged when you switch.
698
+
699
+ ### Cache — three visible tiers
700
+
701
+ ```typescript
702
+ // Tier 1 — automatic for live and read flows; invalidation computed from effects
703
+ // Tier 2 — a flag on any flow
704
+ export const popular = on(http.get("/popular"), flow({ cache: "5m", do: /* … */ }));
705
+ // Tier 3 — manual
706
+ const rate = await fx.cache.getOrSet("fx-rate:USD-SAR", "1h", fetchRate);
707
+ ```
708
+
709
+ ### i18n
710
+
711
+ ```typescript
712
+ // src/locales/ar.ts
713
+ export default { "order.outOfStock": "لم يتبقَّ سوى {left} قطع" };
714
+ ```
715
+
716
+ Typed keys — a missing key is a compile error. Locale resolves per request: user profile → `Accept-Language` → configured default. Errors and channel templates are localised, and the `dir` flag reaches the client so the frontend gets RTL for free.
717
+
718
+ ### `tests/orders.test.ts`
719
+
720
+ ```typescript
721
+ const t = await createTestApp(app); // memory drivers, frozen clock
722
+ const u = await t.auth.loginAs({ scopes: ["order:create"] });
723
+
724
+ const { data } = await t.api.orders.create({ sku: "COFFEE", qty: 2 }, { as: u });
725
+ await t.signals.drain();
726
+ await t.clock.advance("2m"); // the durable sleep elapses instantly
727
+ await t.signals.drain();
728
+
729
+ expect(t.channels.sent()).toContainEqual(
730
+ expect.objectContaining({ template: "order-confirmed", to: u.id, locale: "ar" }));
731
+ ```
732
+
733
+ ### What you have
734
+
735
+ All ten exports and seven of the eight elements. Durable execution, human-facing delivery, realtime, plugins, i18n, and secrets with boot-time validation.
736
+
737
+ ### What is missing
738
+
739
+ The system serves one customer, treats every user the same, and has no way to state what "working" means. It also cannot reason about anything.
740
+
741
+ ---
742
+ ---
743
+
744
+ # 4 · COMPLEX — Skyport
745
+
746
+ A membership and booking platform, multi-tenant, with AI.
747
+
748
+ **New ideas:** the `ai` element (models, prompts, RAG, agents) · multi-tenancy · SLOs and journeys · distributed topology · the three scaling axes.
749
+
750
+ ```
751
+ skyport/
752
+ ├── oke.config.ts
753
+ ├── oke.images.lock
754
+ ├── src/
755
+ │ ├── app.ts · core.ts · gates.ts · vault.ts · channels.ts · ai.ts
756
+ │ ├── locales/{en,ar}.ts · schema.ts · schema/oke.ts (generated)
757
+ │ ├── plugins/audit.ts
758
+ │ └── flows/
759
+ │ ├── bookings/{index.ts,shapes.ts,signals.ts} # flights + FlightFull live here
760
+ │ ├── payments/{index.ts,shapes.ts}
761
+ │ ├── notifications/index.ts
762
+ │ ├── support/index.ts # AI triage, RAG, a bounded agent
763
+ │ └── users/{index.ts,shapes.ts,elements.ts}
764
+ └── tests/
765
+ ```
766
+
767
+ ### `oke.config.ts` — the complete surface
768
+
769
+ ```typescript
770
+ import { defineConfig } from "okengine/config";
771
+ import { dbUrl, dbReplica1, anthropicKey } from "./src/vault";
772
+
773
+ export default defineConfig({
774
+ // Drivers are named after PROTOCOLS and bind through Bun's native clients
775
+ // (Bun.sql, bun:sqlite, Bun.redis, Bun.S3) — zero npm client dependencies.
776
+ drivers: {
777
+ store: {
778
+ sql: { dev: "sqlite", test: "memory",
779
+ prod: { driver: "postgres", url: dbUrl, pool: { max: 20 },
780
+ replicas: [dbReplica1] } }, // read-only flows auto-route here
781
+ kv: { dev: "memory", test: "memory", prod: "redis" }, // Redis · Valkey · Dragonfly
782
+ files: { dev: "fs", test: "memory", prod: "s3" }, // S3 · R2 · SeaweedFS · MinIO
783
+ index: { dev: "pgvector", test: "memory", prod: "pgvector" },
784
+ },
785
+ signal: { dev: "memory", test: "memory", prod: "postgres" },
786
+ clock: { dev: "memory", test: "frozen", prod: "postgres" },
787
+ vault: { dev: "dotenv", test: "memory", prod: "sops" }, // SOPS/age — committable
788
+ runs: { dev: "files", test: "memory", prod: "files" }, // Parquet + DuckDB
789
+ channel: {
790
+ email: { dev: "console", prod: "smtp" },
791
+ sms: { dev: "console", prod: "unifonic" },
792
+ whatsapp: { dev: "console", prod: "wa-cloud" },
793
+ push: { dev: "console", prod: "fcm" },
794
+ },
795
+ ai: {
796
+ dev: "mock", // deterministic — tests never call out
797
+ prod: { driver: "anthropic", key: anthropicKey },
798
+ // no prod default: model choice is never guessed.
799
+ // "openai-compatible" covers vLLM · Groq · Together · LM Studio · most self-hosted
800
+ },
801
+ },
802
+
803
+ images: { // vendor choice, keyed by ROLE
804
+ "store.sql": "pgvector/pgvector:pg17",
805
+ "store.kv": "valkey/valkey:8-alpine",
806
+ },
807
+
808
+ i18n: { locales: ["en", "ar"], default: "ar", dir: { ar: "rtl" } },
809
+ tenancy: { resolve: (ctx) => ctx.auth.orgId, isolation: "row" },
810
+ topology: "monolith", // flip to "services" — code unchanged
811
+ ports: { app: 6530, console: 6533, mcp: 6535 }, // O·K·E = 6·5·3
812
+ console: { prod: { enabled: true, auth: "required" } },
813
+ });
814
+ ```
815
+
816
+ ### `src/schema.ts` (excerpt — the tables this section uses)
817
+
818
+ ```typescript
819
+ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
820
+ import { id } from "okengine/store";
821
+
822
+ export const bookings = sqliteTable("bookings", {
823
+ id: text("id").primaryKey().$defaultFn(id),
824
+ userId: text("user_id").notNull(),
825
+ flightId: text("flight_id").notNull(),
826
+ seats: integer("seats").notNull(),
827
+ status: text("status").notNull().default("pending"),
828
+ createdAt: integer("created_at").notNull(),
829
+ });
830
+
831
+ export const flights = sqliteTable("flights", {
832
+ id: text("id").primaryKey(),
833
+ seatsAvailable: integer("seats_available").notNull(),
834
+ });
835
+
836
+ export const tickets = sqliteTable("tickets", {
837
+ id: text("id").primaryKey(), subject: text("subject").notNull(),
838
+ body: text("body").notNull(), urgency: text("urgency"), team: text("team"),
839
+ summary: text("summary"),
840
+ });
841
+ ```
842
+
843
+ ### `src/flows/bookings/shapes.ts`
844
+
845
+ ```typescript
846
+ import { z } from "zod";
847
+
848
+ export const NewBooking = z.object({ flightId: z.string(), seats: z.number().min(1).max(9) });
849
+ export const BookingId = z.object({ id: z.string() });
850
+ export const BookingRow = z.object({ id: z.string(), status: z.string(), seats: z.number() });
851
+ export const FlightFull = z.object({ seatsLeft: z.number() });
852
+ ```
853
+
854
+ ### `src/flows/bookings/signals.ts`
855
+
856
+ ```typescript
857
+ import { signal } from "okengine";
858
+ import { z } from "zod";
859
+
860
+ export const orderPlaced = signal("order-placed", {
861
+ schema: z.object({ orderId: z.string() }), delivery: "once", retries: 5, deadLetter: true,
862
+ });
863
+ export const seatFeed = signal("seat-feed", {
864
+ schema: z.object({ flightId: z.string(), left: z.number() }), delivery: "live",
865
+ });
866
+ ```
867
+
868
+ ### `src/flows/bookings/index.ts`
869
+
870
+ ```typescript
871
+ import { on, flow, gate, http } from "okengine";
872
+ import { eq } from "drizzle-orm";
873
+ import { db } from "../../core";
874
+ import { member, fair } from "../../gates";
875
+ import { orderPlaced, seatFeed } from "./signals";
876
+ import { NewBooking, BookingId, BookingRow, FlightFull } from "./shapes";
877
+ import { bookings, flights } from "../../schema";
878
+
879
+ export const canBook = gate.policy("booking:create", ({ auth }) => auth.scopes.has("booking:create"));
880
+
881
+ export const create = on(http.post("/bookings").gate(member, canBook, fair), flow({
882
+ slo: { availability: "99.9%", latency: { p99: "200ms" } },
883
+ in: NewBooking, out: BookingId, errors: { FlightFull },
884
+ do: async ({ flightId, seats }, fx) => {
885
+ const [flight] = await fx.store(db).select().from(flights).where(eq(flights.id, flightId)).limit(1);
886
+ if (!flight || flight.seatsAvailable < seats)
887
+ return fx.fail("FlightFull", { seatsLeft: flight?.seatsAvailable ?? 0 });
888
+
889
+ const id = fx.id();
890
+ await fx.store(db).insert(bookings).values(
891
+ { id, userId: fx.auth.userId, flightId, seats, status: "pending", createdAt: Date.now() });
892
+ await fx.emit(orderPlaced, { orderId: id });
893
+ await fx.emit(seatFeed, { flightId, left: flight.seatsAvailable - seats });
894
+ return { id };
895
+ },
896
+ }));
897
+
898
+ export const mine = on(http.get("/bookings").gate(member).live(), flow({
899
+ out: BookingRow.array(),
900
+ do: (_, fx) => fx.store(db).select().from(bookings).where(eq(bookings.userId, fx.auth.userId)),
901
+ }));
902
+
903
+ export const getBooking = flow({
904
+ in: BookingId, out: BookingRow,
905
+ do: async ({ id }, fx) => {
906
+ const [b] = await fx.store(db).select().from(bookings).where(eq(bookings.id, id)).limit(1);
907
+ return b;
908
+ },
909
+ });
910
+
911
+ // The agent's second tool — refunding is a distinct, gated capability, never the same
912
+ // permission as reading a booking, since the agent's tool list is exactly its authority.
913
+ export const refundBooking = flow({
914
+ in: BookingId, out: BookingRow,
915
+ do: async ({ id }, fx) => {
916
+ await fx.store(db).update(bookings).set({ status: "refunded" }).where(eq(bookings.id, id));
917
+ const [b] = await fx.store(db).select().from(bookings).where(eq(bookings.id, id)).limit(1);
918
+ return b;
919
+ },
920
+ });
921
+ ```
922
+
923
+ ### `src/ai.ts` — the eighth element
924
+
925
+ ```typescript
926
+ import { ai, store } from "okengine";
927
+ import { z } from "zod";
928
+ import { getBooking, refundBooking } from "./flows/bookings";
929
+
930
+ export const smart = ai.model("smart", { provider: "anthropic", tier: "opus" });
931
+ export const fast = ai.model("fast", { provider: "anthropic", tier: "haiku" });
932
+
933
+ // A prompt is a VERSIONED ARTIFACT with a validated output shape — not a string in a handler
934
+ export const triage = smart.prompt("ticket-triage", {
935
+ in: z.object({ subject: z.string(), body: z.string() }),
936
+ out: z.object({ urgency: z.enum(["low", "high"]), team: z.string(), summary: z.string() }),
937
+ version: 3,
938
+ evals: "./evals/triage.jsonl", // regression-gated in CI via `oke eval`
939
+ budget: { maxCostPerCall: 0.02 }, // cost is a first-class dimension
940
+ });
941
+
942
+ export const embed = ai.embed("docs", { model: fast, into: store.index("kb") });
943
+
944
+ // An agent whose tools are YOUR OWN FLOWS — each carrying its gates and effects
945
+ export const support = ai.agent("support", {
946
+ model: smart,
947
+ tools: [getBooking, refundBooking],
948
+ maxSteps: 6,
949
+ budget: { maxCostPerRun: 0.25 },
950
+ });
951
+ ```
952
+
953
+ ### `src/flows/support/index.ts`
954
+
955
+ ```typescript
956
+ import { on, flow, http } from "okengine";
957
+ import { z } from "zod";
958
+ import { triage, support, embed, smart, fast } from "../../ai";
959
+ import { member } from "../../gates";
960
+ import { db } from "../../core";
961
+ import { tickets } from "../../schema";
962
+
963
+ // ① A prompt call with a provider fallback chain and a validated result
964
+ export const createTicket = on(http.post("/tickets").gate(member), flow({
965
+ in: z.object({ subject: z.string(), body: z.string() }),
966
+ out: z.object({ id: z.string(), urgency: z.string() }),
967
+ do: async (input, fx) => {
968
+ const t = await fx.ask(triage, input, { via: [smart, fast] });
969
+ const id = fx.id();
970
+ await fx.store(db).insert(tickets).values({ id, ...input, ...t });
971
+ return { id, urgency: t.urgency };
972
+ },
973
+ }));
974
+ // effects → writes[sql:tickets] asks[ticket-triage v3] cost[~$0.01] nondeterministic
975
+
976
+ // ② RAG — retrieve, then answer with streaming tokens
977
+ export const askDocs = on(http.post("/ask").gate(member).live(), flow({
978
+ in: z.object({ question: z.string() }),
979
+ do: async ({ question }, fx) => {
980
+ const context = await fx.search(embed, question, { topK: 5 });
981
+ return fx.stream(smart, { prompt: "answer-with-context", data: { question, context } });
982
+ // streaming reaches the client through the Signal element — no separate socket layer
983
+ },
984
+ }));
985
+
986
+ // ③ A durable, bounded agent
987
+ export const supportAgent = on(http.post("/support").gate(member), flow({
988
+ durable: true, // nondeterministic calls are ALWAYS journaled
989
+ in: z.object({ message: z.string() }),
990
+ do: ({ message }, fx) => fx.run(support, { message }),
991
+ // the agent can only call getBooking and refundBooking, and only within THIS user's
992
+ // gates and tenant scope — it cannot exceed what the code declares
993
+ }));
994
+ ```
995
+
996
+ **What the compiler enforces here, for free:**
997
+
998
+ - A field tagged `pii` in the schema **cannot reach a third-party model** — the build fails unless the flow masks it or declares `allowPii` explicitly.
999
+ - `nondeterministic` forces journaling: on replay, a model is never re-called; the recorded answer is reused.
1000
+ - Automatic caching is disabled for AI flows unless a semantic cache is explicitly enabled.
1001
+ - Cost accumulates per flow, per tenant and per release — visible in the Console and in Manifest Diff *before* deploy.
1002
+
1003
+ ### Declaring what "working" means
1004
+
1005
+ ```typescript
1006
+ // on a flow — this is bookings.create, shown in full above
1007
+ export const create = on(http.post("/bookings").gate(member, canBook, fair), flow({
1008
+ slo: { availability: "99.9%", latency: { p99: "200ms" } },
1009
+ in: NewBooking, out: BookingId, errors: { FlightFull },
1010
+ do: /* as shown above */,
1011
+ }));
1012
+
1013
+ // on a user journey — because a service SLO is not a user SLO
1014
+ journey("book-a-flight", {
1015
+ path: [bookings.create, payments.charge, notifications.send],
1016
+ slo: { availability: "99.5%" },
1017
+ });
1018
+ ```
1019
+
1020
+ Forty services at 99.9% in sequence yield 96.1% for the user. Because the causal chain is known, **the compiler rejects the impossible**: *"this path composes to 99.4% but declares 99.5%."* And because the objective lives in the Manifest, lowering a target is a code change that passes through Manifest Diff and team review — not a silent dashboard edit.
1021
+
1022
+ ### Multi-tenancy as a dimension of `fx`
1023
+
1024
+ `tenancy: { resolve, isolation: "row" }` in the config is the whole of it. Every store call passes through `fx`, so tenant scoping applies automatically — there is no forgotten `WHERE org_id`. Rate limits, caches, secrets and channel branding become per-tenant for free, and **`oke doctor` fails the build** if any flow reads a tenant-scoped table without a tenant in context.
1025
+
1026
+ ### The three scaling axes, never conflated
1027
+
1028
+ | Axis | Question | Mechanism |
1029
+ |---|---|---|
1030
+ | **Split** (`topology`) | one deployable, or one per unit? | `monolith` = in-process calls · `services` = a container per unit, `fx.call` becomes network — code unchanged |
1031
+ | **Clone** (horizontal) | how many copies of the app? | run N instances: `once` signals deliver to exactly one, crons leader-elect, live queries fan out. `oke docker --prod` emits `deploy.replicas` |
1032
+ | **Data replicas** | how many copies of the data? | `replicas:` on the driver; read-only flows auto-route, derived from effects |
1033
+
1034
+ ### `src/app.ts`
1035
+
1036
+ ```typescript
1037
+ import { oke } from "okengine";
1038
+ import { auth } from "okengine/auth";
1039
+ import { audit } from "./plugins/audit";
1040
+ import * as bookings from "./flows/bookings";
1041
+ import * as payments from "./flows/payments";
1042
+ import * as notifications from "./flows/notifications";
1043
+ import * as support from "./flows/support";
1044
+ import * as users from "./flows/users";
1045
+
1046
+ export const app = oke({ name: "skyport" })
1047
+ .adopt({ bookings, payments, notifications, support, users })
1048
+ .plug(auth())
1049
+ .plug(audit)
1050
+ .hook("onError", (ctx, err, fx) => fx.log.error(err));
1051
+
1052
+ export type App = typeof app;
1053
+ ```
1054
+
1055
+ Same shape as Provisions — `.adopt()` for the client's types, `.plug()` for cross-cutting concerns, `on()` inside each flow file for the actual trigger registration. Nothing about composition changes as an application grows from one unit to five.
1056
+
1057
+ ### `tests/` — deterministic even with AI
1058
+
1059
+ ```typescript
1060
+ const t = await createTestApp(app);
1061
+ t.ai.mock(triage, { urgency: "high", team: "ops", summary: "seat dispute" });
1062
+
1063
+ const u = await t.auth.loginAs({});
1064
+ const { data } = await t.api.support.createTicket({ subject: "…", body: "…" }, { as: u });
1065
+
1066
+ expect(data!.urgency).toBe("high");
1067
+ expect(t.ai.cost()).toBeLessThan(0.02); // budgets are assertable
1068
+ ```
1069
+
1070
+ ### What you have
1071
+
1072
+ All eight elements, all ten exports, and one law.
1073
+
1074
+ ---
1075
+ ---
1076
+
1077
+ # REFERENCE
1078
+
1079
+ ## What the compiler produced — `manifest.oke.json`
1080
+
1081
+ ```json
1082
+ {
1083
+ "oke": "1.0",
1084
+ "app": "skyport",
1085
+ "flows": {
1086
+ "bookings.create": {
1087
+ "trigger": { "http": { "method": "POST", "path": "/bookings" } },
1088
+ "gates": ["member", "booking:create", "rate:sliding-window-counter:300/1m"],
1089
+ "in": "…", "out": "…", "errors": ["FlightFull"],
1090
+ "effects": {
1091
+ "reads": ["sql:bookings"],
1092
+ "writes": ["sql:bookings"],
1093
+ "emits": ["order-placed", "seat-feed"],
1094
+ "secrets": []
1095
+ },
1096
+ "slo": { "availability": "99.9%", "latency": { "p99": "200ms" } },
1097
+ "source": "src/flows/bookings/index.ts:18"
1098
+ },
1099
+ "bookings.mine": { "live": true, "cacheKeys": "computed:sql:bookings/userId" },
1100
+ "payments.chargeBooking": { "durable": true, "steps": ["create-intent", "confirm"],
1101
+ "effects": { "secrets": ["STRIPE_KEY"] } },
1102
+ "support.createTicket": {
1103
+ "effects": { "writes": ["sql:tickets"], "asks": ["ticket-triage@3"] },
1104
+ "nondeterministic": true,
1105
+ "cost": { "estimatePerCall": 0.011, "budget": 0.02 },
1106
+ "pii": "masked"
1107
+ }
1108
+ },
1109
+ "signals": {
1110
+ "order-placed": { "delivery": "once", "retries": 5, "deadLetter": true },
1111
+ "seat-feed": { "delivery": "live" }
1112
+ },
1113
+ "channels": { "booking-confirmed": { "medium": "email", "locales": ["en", "ar"] } },
1114
+ "ai": {
1115
+ "models": { "smart": { "provider": "anthropic", "tier": "opus" } },
1116
+ "prompts": { "ticket-triage": { "version": 3, "evals": "./evals/triage.jsonl" } },
1117
+ "agents": { "support": { "tools": ["bookings.getBooking", "bookings.refundBooking"],
1118
+ "maxSteps": 6 } }
1119
+ },
1120
+ "journeys": { "book-a-flight": { "slo": { "availability": "99.5%" }, "composes": "99.6%" } },
1121
+ "drivers": { "prod": ["postgres", "redis", "s3", "smtp", "sops", "anthropic", "pgvector"] },
1122
+ "tenancy": { "isolation": "row" }
1123
+ }
1124
+ ```
1125
+
1126
+ From this one file OKE derives: the typed client · OpenAPI + AsyncAPI · the Console catalogue, diagrams and traces · per-flow capabilities · cache invalidation keys · replica routing · the tree-shaken bundle · the Dockerfile and compose files · the MCP surface.
1127
+
1128
+ ## Commands
1129
+
1130
+ ```bash
1131
+ bun add okengine # ONE package
1132
+
1133
+ oke dev # watch · hot reload · Console :6533 · app :6530 · MCP :6535
1134
+ # → also auto-syncs client types on every save
1135
+ oke dev --stack # -s also boot the real infra stack (generated compose)
1136
+ oke dev -s store.sql,signal # partial: only these roles get real backends
1137
+
1138
+ oke start # runs exactly what production runs (this is the Docker CMD)
1139
+ oke doctor # verify secrets, ports, drivers, tenancy, schema drift
1140
+ oke stack # preview resolved images/tags/ports — writes nothing
1141
+
1142
+ oke schema generate # core + plugin tables → schema/oke.ts (--check in CI)
1143
+ oke vault set STRIPE_KEY # also: list · import .env · key rotate
1144
+ oke client add <url> # types for a separate frontend repo
1145
+
1146
+ oke docker # Dockerfile + compose.store.sql.yml · compose.store.kv.yml · …
1147
+ oke docker --prod # healthchecks, volumes, limits, secret refs, deploy.replicas
1148
+ oke images pin # tags → digests in oke.images.lock
1149
+
1150
+ oke build --target edge # < 15 kB kernel profile
1151
+ oke eval # run prompt eval sets; fails CI on regression
1152
+ oke branch prod --at "yesterday" # fork journaled state into a sandbox
1153
+ oke privacy erase --subject <id> # crypto-shredding: deletes the key, not the terabytes
1154
+ oke upgrade # run codemods for a breaking change, print the diff
1155
+ ```
1156
+
1157
+ ## The Console at `:6533`
1158
+
1159
+ Seventeen panels, all derived and never hand-maintained: **Overview · Flows · Signals · Store · Clock · Gates · Vault · Channels · AI · Architecture · Traces · Runs · Manifest Diff · Access · Plugins**, plus **Privacy** and **Tenancy** when their optional core plugins are plugged.
1160
+
1161
+ Runtime actions execute directly; structural changes arrive as reviewable diffs in your working tree. Every Console action is a real flow through `fx`, so **the audit log is the trace**.
1162
+
1163
+ ## Store reference
1164
+
1165
+ **Why Drizzle is a required peer dependency, not an abstraction.** The framework commits to one query builder rather than supporting several, because the effect inferencer performs real static analysis on Drizzle's own shapes — a table object, `.select().from(t)`, `.insert(t).values()` — to derive `reads`/`writes`/PII classification with no annotation from you. Supporting N ORMs would mean either analysing N different query builders (and getting it wrong for the ones nobody tests) or falling back to hints, which is exactly the annotation burden the effect system exists to remove. One committed ORM is what makes automatic cache invalidation and least-privilege capability tokens possible at all.
1166
+
1167
+ **When you still need `fx.id()` despite `$defaultFn(id)`.** The schema default fills the `id` column at insert time — fine when nothing in the flow needs the value beforehand, as in Notes' `create` (the id is only read back from `.returning()`). Generate it explicitly with `fx.id()`, and pass it into `.values({ id, … })` yourself, whenever the flow needs the same id *before or alongside* the insert — to reference it in an emitted signal payload, to use it as a foreign key in a second insert in the same flow, or to return it without a round-trip. Linkly's `shorten` is the pattern: `const id = fx.id()` because the row and any signal about it need to agree on the same identifier within one flow body.
1168
+
1169
+ ## Element checklist across the four applications
1170
+
1171
+ | Element | First appears | The unification it proves |
1172
+ |---|---|---|
1173
+ | **Flow** | Basic | endpoint = consumer = cron = CDC = workflow — one species |
1174
+ | **Store** | Basic | sql · kv · files · index; cache and replica routing derived from effects |
1175
+ | **Signal** | Intermediate | queue = pub/sub = stream; delivery is a property, not three ecosystems |
1176
+ | **Clock** | Intermediate | cron = delay = durable time |
1177
+ | **Gate** | Intermediate | auth = ABAC = rate limit, composable at the trigger |
1178
+ | **Vault** | Advanced | typed contracts, boot-time validation, per-flow read capability |
1179
+ | **Channel** | Advanced | email = sms = whatsapp = push; consent, locale and fallback built in |
1180
+ | **AI** | Complex | prompts versioned · agents bounded by your own flows · cost and PII enforced by the compiler |
1181
+ | **`fx` door** | Basic | journaling · tests · least privilege · transactions · tenancy · i18n · cost |
1182
+ | **Manifest** | Basic | client · docs · console · security · bundle · infrastructure |
1183
+ | **Plugin** | Advanced | extends through the same law; built-ins have no private API |
1184
+
1185
+ **Ten exports — `on, flow, signal, store, clock, gate, vault, channel, ai, plugin` — one law.**
1186
+
1187
+ That is the harmony.