okengine 0.19.9 → 0.21.0

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 (156) hide show
  1. package/AGENTS.md +1 -1
  2. package/package.json +2 -1
  3. package/site/content/docs/client/auth.mdx +1 -2
  4. package/site/content/docs/client/calling.mdx +125 -37
  5. package/site/content/docs/client/index.mdx +7 -7
  6. package/site/content/docs/client/react.mdx +5 -0
  7. package/site/content/docs/elements/channel/email.mdx +25 -36
  8. package/site/content/docs/elements/channel/index.mdx +88 -46
  9. package/site/content/docs/elements/channel/push.mdx +7 -9
  10. package/site/content/docs/elements/channel/sms.mdx +6 -4
  11. package/site/content/docs/elements/channel/whatsapp.mdx +11 -9
  12. package/site/content/docs/elements/clock/index.mdx +14 -25
  13. package/site/content/docs/elements/flow/http.mdx +42 -23
  14. package/site/content/docs/elements/flow/index.mdx +41 -30
  15. package/site/content/docs/elements/flow/routing.mdx +166 -122
  16. package/site/content/docs/elements/gate/rls.mdx +2 -2
  17. package/site/content/docs/elements/gate/tenancy.mdx +1 -1
  18. package/site/content/docs/elements/store/files.mdx +11 -5
  19. package/site/content/docs/elements/store/index.mdx +13 -6
  20. package/site/content/docs/elements/store/kv.mdx +8 -2
  21. package/site/content/docs/elements/store/search.mdx +5 -5
  22. package/site/content/docs/elements/store/sql.mdx +100 -39
  23. package/site/content/docs/elements/vault/index.mdx +14 -13
  24. package/site/content/docs/elements/vault/secrets.mdx +6 -3
  25. package/site/content/docs/index.mdx +1 -1
  26. package/site/content/docs/plugins/magic-link.mdx +4 -3
  27. package/site/content/docs/plugins/otp.mdx +4 -3
  28. package/site/content/docs/plugins/two-factor.mdx +4 -0
  29. package/site/content/docs/providers/index.mdx +1 -1
  30. package/site/content/docs/recipes/index.mdx +1 -1
  31. package/site/content/docs/recipes/rustfs.mdx +1 -1
  32. package/site/content/docs/reference/cli.mdx +7 -4
  33. package/site/content/docs/reference/configuration.mdx +8 -8
  34. package/site/content/docs/reference/errors.mdx +229 -55
  35. package/site/content/docs/reference/fx.mdx +22 -9
  36. package/site/content/docs/reference/i18n.mdx +4 -4
  37. package/site/content/docs/reference/plugins.mdx +4 -4
  38. package/site/content/docs/understand/the-architecture.mdx +2 -2
  39. package/site/content/docs/understand/try-it.mdx +759 -24
  40. package/src/cli/ai-setup/ai-setup.test.ts +40 -0
  41. package/src/cli/ai-setup/apply.ts +28 -46
  42. package/src/cli/build.test.ts +3 -3
  43. package/src/cli/build.ts +5 -5
  44. package/src/cli/db-auto-push.test.ts +11 -0
  45. package/src/cli/db-auto-push.ts +6 -2
  46. package/src/cli/db.test.ts +1 -1
  47. package/src/cli/db.ts +6 -6
  48. package/src/cli/dev-app-runner.ts +2 -1
  49. package/src/cli/dev-db-push.test.ts +6 -2
  50. package/src/cli/dev-schema-sync.ts +1 -1
  51. package/src/cli/dev.test.ts +10 -7
  52. package/src/cli/dev.ts +13 -11
  53. package/src/cli/ensure-drizzle-config.ts +4 -3
  54. package/src/cli/start.ts +2 -1
  55. package/src/client/create.ts +3 -3
  56. package/src/client/explain.test.ts +252 -0
  57. package/src/client/explain.ts +272 -0
  58. package/src/client/live.test.ts +44 -0
  59. package/src/client/notes-contract.test.ts +10 -0
  60. package/src/client/sse.ts +6 -2
  61. package/src/client/transport.test.ts +67 -0
  62. package/src/client/transport.ts +24 -40
  63. package/src/client/types.ts +17 -6
  64. package/src/client-react/browser.test.ts +23 -0
  65. package/src/client-react/live-resource.ts +6 -2
  66. package/src/client-react/use-live-query.ts +1 -1
  67. package/src/compiler/flow-path.test.ts +1 -0
  68. package/src/compiler/flow-path.ts +1 -1
  69. package/src/compiler/generate-adopt.test.ts +55 -1
  70. package/src/compiler/generate-adopt.ts +111 -21
  71. package/src/compiler/response.ts +17 -27
  72. package/src/config/index.ts +6 -4
  73. package/src/console/server/invoke-user-flow.test.ts +8 -2
  74. package/src/console/server/invoke-user-flow.ts +12 -18
  75. package/src/console/server/security.gate.test.ts +1 -1
  76. package/src/console/ui-next/dist/assets/{access-page-BoC83Ubl.js → access-page-tIbsiphz.js} +1 -1
  77. package/src/console/ui-next/dist/assets/{agent-disclosure-CKjAEOqA.js → agent-disclosure-CSKumwS2.js} +1 -1
  78. package/src/console/ui-next/dist/assets/{cache-glyph-Ceaq9pYh.js → cache-glyph-BCC-DxKT.js} +1 -1
  79. package/src/console/ui-next/dist/assets/{call-pii-button-C4lmY7ck.js → call-pii-button-jOmYISdc.js} +1 -1
  80. package/src/console/ui-next/dist/assets/{collapsible-82y257sL.js → collapsible-DC2xNaAb.js} +1 -1
  81. package/src/console/ui-next/dist/assets/{duration-tone-W63jaMZ8.js → duration-tone-CwoV56jn.js} +1 -1
  82. package/src/console/ui-next/dist/assets/{flows-page-KFTFj2rK.js → flows-page-gT1lsWQK.js} +1 -1
  83. package/src/console/ui-next/dist/assets/{highlighted-json-yE9zNTqC.js → highlighted-json-C2GZEJNI.js} +1 -1
  84. package/src/console/ui-next/dist/assets/{http-method-CwFeFroN.js → http-method-BdYjcIrD.js} +1 -1
  85. package/src/console/ui-next/dist/assets/{index-r7xXt_VV.js → index-Cul17AcV.js} +3 -3
  86. package/src/console/ui-next/dist/assets/{observability-page-BDiliMiC.js → observability-page-oY9vdYBk.js} +1 -1
  87. package/src/console/ui-next/dist/assets/{replica-lag-DYDzWUFT.js → replica-lag-C4QdAF7J.js} +1 -1
  88. package/src/console/ui-next/dist/assets/{request-meta-CzrOfgiz.js → request-meta-C43DHyld.js} +1 -1
  89. package/src/console/ui-next/dist/assets/{store-page-CZC2cwaw.js → store-page-CL0D9dOq.js} +1 -1
  90. package/src/console/ui-next/dist/assets/{trace-detail-sheet-DgeeejW7.js → trace-detail-sheet-Dcpo4Us_.js} +1 -1
  91. package/src/console/ui-next/dist/assets/{tree-expand-toggle-CzGIyOPY.js → tree-expand-toggle-CKJTuv43.js} +1 -1
  92. package/src/console/ui-next/dist/assets/{units-page-DBiDCLIB.js → units-page-1PlT19ft.js} +1 -1
  93. package/src/console/ui-next/dist/assets/{vault-page-CcHsthPe.js → vault-page-BwZ9YjTW.js} +1 -1
  94. package/src/console/ui-next/dist/index.html +1 -1
  95. package/src/docker/docker.test.ts +3 -3
  96. package/src/docker/images-config.test.ts +4 -4
  97. package/src/docker/stack-id.test.ts +1 -1
  98. package/src/drivers/clock-postgres.test.ts +10 -2
  99. package/src/drivers/clock-postgres.ts +18 -2
  100. package/src/drivers/vault-driver-removal.test.ts +2 -2
  101. package/src/elements/channel/declare.ts +66 -3
  102. package/src/elements/channel/runtime.ts +9 -11
  103. package/src/elements/channel.test.ts +42 -0
  104. package/src/elements/channel.ts +4 -2
  105. package/src/elements/clock/reconcile.ts +45 -24
  106. package/src/elements/clock.test.ts +33 -0
  107. package/src/elements/store/emit-drizzle.ts +285 -65
  108. package/src/elements/store/files-errors.test.ts +149 -0
  109. package/src/elements/store/files-errors.ts +189 -0
  110. package/src/elements/store/kv-errors.test.ts +98 -0
  111. package/src/elements/store/kv-errors.ts +139 -0
  112. package/src/elements/store/load-plugin-tables.ts +1 -1
  113. package/src/elements/store/prepare-row.test.ts +57 -4
  114. package/src/elements/store/resource.ts +11 -7
  115. package/src/elements/store/runtime.ts +18 -13
  116. package/src/elements/store/schema-decl.test.ts +178 -0
  117. package/src/elements/store/sql-errors.test.ts +197 -0
  118. package/src/elements/store/sql-errors.ts +294 -0
  119. package/src/elements/store/sql-session.test.ts +52 -0
  120. package/src/elements/store/sql-session.ts +50 -2
  121. package/src/elements/store/store-errors.ts +47 -0
  122. package/src/elements/store/table.ts +8 -6
  123. package/src/http.ts +9 -1
  124. package/src/i18n/catalogs/ar.ts +18 -0
  125. package/src/i18n/catalogs/en.ts +18 -0
  126. package/src/index.ts +9 -1
  127. package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
  128. package/src/kernel/app.ts +40 -34
  129. package/src/kernel/auto-registry.test.ts +26 -1
  130. package/src/kernel/boot.ts +2 -2
  131. package/src/kernel/boundary-contract.ts +6 -1
  132. package/src/kernel/builtin-errors.test.ts +117 -0
  133. package/src/kernel/builtin-errors.ts +129 -0
  134. package/src/kernel/call.test.ts +182 -0
  135. package/src/kernel/errors-vault.ts +16 -0
  136. package/src/kernel/errors.registry.test.ts +7 -0
  137. package/src/kernel/errors.ts +97 -24
  138. package/src/kernel/fail-helpers.ts +34 -0
  139. package/src/kernel/flow-units.ts +3 -3
  140. package/src/kernel/fx.test.ts +8 -0
  141. package/src/kernel/fx.ts +24 -8
  142. package/src/kernel/index.ts +12 -1
  143. package/src/kernel/mutation-id.ts +8 -0
  144. package/src/kernel/plugin.ts +4 -3
  145. package/src/kernel/project-out.test.ts +176 -0
  146. package/src/kernel/project-out.ts +91 -0
  147. package/src/kernel/realtime-bind.ts +2 -3
  148. package/src/kernel/router/linear.ts +12 -6
  149. package/src/kernel/router.test.ts +13 -0
  150. package/src/plugins/magic-link.ts +25 -24
  151. package/src/plugins/otp.ts +35 -24
  152. package/src/plugins/two-factor.ts +15 -0
  153. package/src/runs/duckdb.test.ts +2 -2
  154. package/src/runtime/dev-request-log.ts +29 -11
  155. package/src/term.test.ts +76 -0
  156. package/src/term.ts +166 -3
@@ -318,13 +318,12 @@ import { uploads } from "@/core";
318
318
  export const get = on(
319
319
  http.get({
320
320
  in: z.object({ id: z.string(), name: z.string() }),
321
- errors: { NotFound: z.object({ key: z.string() }) },
322
321
  }),
323
322
  flow({
324
323
  do: async ({ id, name }, fx) => {
325
324
  const key = `notes/${id}/${name}`;
326
325
  const bytes = await fx.store(uploads).get(key);
327
- if (!bytes) return fx.fail("NotFound", { key });
326
+ if (!bytes) return fx.fail.notFound({ key });
328
327
  return { key, size: bytes.byteLength };
329
328
  },
330
329
  }),
@@ -582,7 +581,7 @@ export default defineConfig({
582
581
  },
583
582
  images: {
584
583
  store: {
585
- files: "rustfs/rustfs:1.0.0-rc.5",
584
+ files: "rustfs/rustfs:1.0.0",
586
585
  },
587
586
  },
588
587
  });
@@ -607,8 +606,14 @@ For `s3`, Compose / env typically supply `S3_ENDPOINT`, `S3_ACCESS_KEY_ID`,
607
606
  </Accordion>
608
607
 
609
608
  <Accordion title="Invalid object key">
610
- Cause: `Invalid object key: …` on the `fs` driver when the key starts with `/` or contains `..`.
611
- Use relative, non-escaping keys.
609
+ The `fs` driver rejects keys that start with `/` or contain `..`. HTTP is `Forbidden` **403**
610
+ (`reason: invalid_key`) — the key is not copied. Use relative, non-escaping keys.
611
+ </Accordion>
612
+
613
+ <Accordion title="S3 AccessDenied / disk full is InternalError">
614
+ `AccessDenied` / `EACCES` map to `Forbidden` **403**. Missing `get` stays `null`. `SlowDown` stays
615
+ thrown until `flow.retry` exhausts, then **503**. `NoSuchBucket` / `ENOSPC` are **503** — see
616
+ [Errors · Files auto-map](/docs/reference/errors#files-auto-map).
612
617
  </Accordion>
613
618
 
614
619
  <Accordion title="ERR_IMAGE_TOO_MANY_PIXELS">
@@ -644,6 +649,7 @@ For `s3`, Compose / env typically supply `S3_ENDPOINT`, `S3_ACCESS_KEY_ID`,
644
649
  - [HTTP](/docs/elements/flow/http) — routes that accept uploads
645
650
  - [Vault](/docs/elements/vault) — credentials for S3 when you configure bindings
646
651
  - [fx](/docs/reference/fx) — `fx.store(decl)`
652
+ - [Errors](/docs/reference/errors) — Files auto-map
647
653
  - [Configuration](/docs/reference/configuration) — `drivers.store.files`
648
654
 
649
655
  ## Next
@@ -24,7 +24,7 @@ For developers persisting domain data on okengine — one handle shape, drivers
24
24
  <Step>
25
25
  ### Declare a SQL store
26
26
 
27
- ```typescript title="src/db/schema.decl.ts"
27
+ ```typescript title="src/db/schema.ts"
28
28
  import { store, field } from "okengine";
29
29
 
30
30
  export const notes = store.schema.table("notes", {
@@ -49,17 +49,20 @@ import { db, notes } from "@/schema";
49
49
  export const create = on(
50
50
  http.post({
51
51
  in: z.object({ title: z.string().min(1) }),
52
+ out: z.object({ id: z.string(), title: z.string() }),
52
53
  }),
53
54
  flow({
54
55
  do: async ({ title }, fx) => {
55
56
  const id = fx.id();
56
- await fx.store(db).insert(notes).values({ id, title });
57
- return fx.json.create({ id, title });
57
+ const [row] = await fx.store(db).insert(notes).values({ id, title }).returning();
58
+ return fx.json.create(row);
58
59
  },
59
60
  }),
60
61
  );
61
62
  ```
62
63
 
64
+ `out` projects the insert row — `createdAt` strips; Date timestamps become ISO-8601.
65
+
63
66
  </Step>
64
67
 
65
68
  <Step>
@@ -84,6 +87,11 @@ Response:
84
87
 
85
88
  </Steps>
86
89
 
90
+ <Callout title="Two declare layouts">
91
+ One file (`schema.ts`) or a folder (`schema/`, one table per file). Both import as `@/db/schema`.
92
+ Pick one — see [SQL](/docs/elements/store/sql#declaring-stores).
93
+ </Callout>
94
+
87
95
  <Callout title="Effects are inferred">
88
96
  Every `fx.store` touch is recorded on the Flow as `reads` / `writes` (`sql:app`, `kv:sessions`,
89
97
  `files:uploads`, …). That powers the Manifest, Console, cache invalidation, and least privilege —
@@ -109,12 +117,11 @@ import { db, notes } from "@/schema";
109
117
  export const get = on(
110
118
  http.get({
111
119
  in: z.object({ id: z.string() }),
112
- errors: { NotFound: z.object({ id: z.string() }) },
113
120
  }),
114
121
  flow({
115
122
  do: async ({ id }, fx) => {
116
123
  const [note] = await fx.store(db).select().from(notes).where(eq(notes.id, id));
117
- if (!note) return fx.fail("NotFound", { id });
124
+ if (!note) return fx.fail.notFound({ id });
118
125
  return note;
119
126
  },
120
127
  }),
@@ -220,7 +227,7 @@ export default defineConfig({
220
227
  store: {
221
228
  sql: "postgres:18-alpine",
222
229
  kv: "redis:8-alpine",
223
- files: "rustfs/rustfs:1.0.0-rc.5",
230
+ files: "rustfs/rustfs:1.0.0",
224
231
  },
225
232
  },
226
233
  });
@@ -262,7 +262,7 @@ export const get = on(
262
262
  do: async ({ token }, fx) => {
263
263
  const userId = await fx.store(sessions).get(`session:${token}`);
264
264
  if (userId === undefined || userId === null) {
265
- return fx.fail("NotFound", { token });
265
+ return fx.fail.notFound({ token });
266
266
  }
267
267
  return { userId };
268
268
  },
@@ -599,6 +599,12 @@ Gate rate strategies inherit `drivers.store.kv` (no separate `drivers.gate`).
599
599
  does). Prefer prefix filters in admin Flows.
600
600
  </Accordion>
601
601
 
602
+ <Accordion title="Redis down returns InternalError">
603
+ Connection / `CLUSTERDOWN` / `READONLY` map to `ServiceUnavailable` **503**. `BUSY` / `TRYAGAIN`
604
+ stay thrown until `flow.retry` exhausts, then **503**. `WRONGTYPE` stays `InternalError` — see
605
+ [Errors · KV auto-map](/docs/reference/errors#kv-auto-map).
606
+ </Accordion>
607
+
602
608
  <Accordion title="I expected incr / setNx / fx.store.kv">
603
609
  Those helpers are not on the public handle. Use `get` / `set` / `delete` / `list` / `ttlMs` via
604
610
  `fx.store(decl)`. Gate rates use Redis Lua internally.
@@ -614,7 +620,7 @@ Gate rate strategies inherit `drivers.store.kv` (no separate `drivers.gate`).
614
620
  - [Gate](/docs/elements/gate) — rate buckets use Redis internally
615
621
  - [fx](/docs/reference/fx) — `fx.store(decl)`
616
622
  - [Configuration](/docs/reference/configuration) — `drivers.store.kv`
617
- - [Errors](/docs/reference/errors) — OKE1810
623
+ - [Errors](/docs/reference/errors) — OKE1810 · KV auto-map
618
624
 
619
625
  ## Next
620
626
 
@@ -24,7 +24,7 @@ For developers ranking rows on okengine — mark columns, query with `fx.store(d
24
24
  <Step>
25
25
  ### Mark columns and bind a route
26
26
 
27
- ```typescript title="src/db/schema.decl.ts"
27
+ ```typescript title="src/db/schema.ts"
28
28
  import { field, store } from "okengine";
29
29
 
30
30
  export const articles = store.schema.table("articles", {
@@ -98,7 +98,7 @@ From BM25-only ranking to hybrid LSH, fusion, and opt-in rerank:
98
98
 
99
99
  Title carries twice the BM25F field weight of body. No `ai` element, no `.embed()`:
100
100
 
101
- ```typescript title="src/db/schema.decl.ts"
101
+ ```typescript title="src/db/schema.ts"
102
102
  import { field, store } from "okengine";
103
103
 
104
104
  export const articles = store.schema.table("articles", {
@@ -283,7 +283,7 @@ Each searchable column binds with `field.text().searchable(…)` (optionally `.e
283
283
 
284
284
  Mark the text fields you want ranked. No AI, no shadow vector columns:
285
285
 
286
- ```typescript title="src/db/schema.decl.ts"
286
+ ```typescript title="src/db/schema.ts"
287
287
  import { field, store } from "okengine";
288
288
 
289
289
  export const articles = store.schema.table("articles", {
@@ -307,7 +307,7 @@ retrieval (`plainto_tsquery('english', …)`).
307
307
  Chain `.embed()` **after** `.searchable()`. Pass `{ model, dims }` on the field, or
308
308
  inherit the project default:
309
309
 
310
- ```typescript title="src/db/schema.decl.ts"
310
+ ```typescript title="src/db/schema.ts"
311
311
  import { field, store, ai } from "okengine";
312
312
 
313
313
  const embedder = ai.model("embedder", {
@@ -348,7 +348,7 @@ oke({
348
348
  });
349
349
  ```
350
350
 
351
- ```typescript title="src/db/schema.decl.ts"
351
+ ```typescript title="src/db/schema.ts"
352
352
  body: field.text().searchable().embed(), // inherits model + dims
353
353
  caption: field.text().searchable().embed({ model: captionEmbedder }), // dims still inherit
354
354
  alt: field.text().searchable().embed({ model: captionEmbedder, dims: 384 }),
@@ -22,7 +22,7 @@ For developers modeling domain data on okengine — mark columns, push schema, q
22
22
  <Step>
23
23
  ### Define a table and bind the store
24
24
 
25
- ```typescript title="src/db/schema.decl.ts"
25
+ ```typescript title="src/db/schema.ts"
26
26
  import { store, field } from "okengine";
27
27
 
28
28
  export const posts = store.schema.table("posts", {
@@ -81,9 +81,9 @@ Response:
81
81
 
82
82
  </Steps>
83
83
 
84
- <Callout title="Pathless declare">
85
- Starters often pass the whole decl module: `store.sql("app", {schema})` where `schema` is `import
86
- * as schema from "@/db/schema.decl"`. Named keys and the exported table handles are equivalent.
84
+ <Callout title="Two declare layouts">
85
+ One file (`schema.ts`, blank) or a folder (`schema/`, one table per file — shorter). Both import
86
+ as `@/db/schema`. Do not keep the file and the folder in the same app.
87
87
  </Callout>
88
88
 
89
89
  ## Progressive Patterns
@@ -97,7 +97,7 @@ Explore SQL from a bare table to classified columns, CRUD factory, and RLS:
97
97
  `field.id()` is `text` + auto id on insert. Use `.okid()` on an existing text
98
98
  column when you want the same default without the sugar factory:
99
99
 
100
- ```typescript title="src/db/schema.decl.ts"
100
+ ```typescript title="src/db/schema.ts"
101
101
  import { store, field } from "okengine";
102
102
 
103
103
  export const notes = store.schema.table("notes", {
@@ -171,7 +171,7 @@ Pass extras as the **third argument array**. Helpers take SQL/JS column
171
171
  **names** (strings) and stamp `oke.gate()` / `oke.user()` / `oke.has_scope()`
172
172
  predicates:
173
173
 
174
- ```typescript title="src/db/schema.decl.ts"
174
+ ```typescript title="src/db/schema.ts"
175
175
  import { store, field } from "okengine";
176
176
 
177
177
  export const tasks = store.schema.table(
@@ -200,23 +200,23 @@ otherwise.
200
200
 
201
201
  Factories mirror Drizzle pg-core names. Chain modifiers after the factory:
202
202
 
203
- | Factory | Infers | Notes |
204
- | -------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
205
- | `field.id()` | `string` | `text` + auto id on insert (`≡ field.text().okid()`) |
206
- | `field.okid()` | `string` | Pins OK ID on a text column |
207
- | `field.text()` / `varchar` / `char` | `string` | Optional `{ length?, enum? }` |
208
- | `field.boolean()` | `boolean` | |
209
- | `field.smallint()` / `integer()` | `number` | |
210
- | `field.bigint({ mode? })` | `number` (default) | `mode`: `"number"` · `"bigint"` · `"string"` |
211
- | `field.serial()` / `smallserial()` / `bigserial()` | `number` | NOT NULL by SQL physics |
212
- | `field.numeric()` / `decimal()` | `string` (default) | Exact decimal; `{ precision?, scale?, mode? }` |
213
- | `field.real()` / `doublePrecision()` | `number` | Float4 / float8 |
214
- | `field.json()` / `jsonb()` | generic | Narrow with `field.json<MyShape>()` |
215
- | `field.uuid()` | `string` | |
216
- | `field.time()` / `timestamp()` / `date()` / `interval()` | see type | `timestamp` / `date` default to `Date`; `{ mode: "string" }` for ISO. On write, finite epoch-ms numbers (e.g. `fx.clock.now()`) coerce to `Date` for Postgres |
217
- | `field.point()` / `line()` | tuple | `{ mode: "xy" }` / `"abc"` for objects |
218
- | `field.bytea()` | `Buffer` | |
219
- | `field.inet()` / `cidr()` / `macaddr()` / `macaddr8()` | `string` | |
203
+ | Factory | Infers | Notes |
204
+ | -------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
205
+ | `field.id()` | `string` | `text` + auto id on insert (`≡ field.text().okid()`) |
206
+ | `field.okid()` | `string` | Pins OK ID on a text column |
207
+ | `field.text()` / `varchar` / `char` | `string` | Optional `{ length?, enum? }` |
208
+ | `field.boolean()` | `boolean` | |
209
+ | `field.smallint()` / `integer()` | `number` | |
210
+ | `field.bigint({ mode? })` | `number` (default) | `mode`: `"number"` · `"bigint"` · `"string"` |
211
+ | `field.serial()` / `smallserial()` / `bigserial()` | `number` | NOT NULL by SQL physics |
212
+ | `field.numeric()` / `decimal()` | `string` (default) | Exact decimal; `{ precision?, scale?, mode? }` |
213
+ | `field.real()` / `doublePrecision()` | `number` | Float4 / float8 |
214
+ | `field.json()` / `jsonb()` | generic | Narrow with `field.json<MyShape>()` |
215
+ | `field.uuid()` | `string` | |
216
+ | `field.time()` / `timestamp()` / `date()` / `interval()` | see type | `timestamp` / `date` default to `Date`; `{ mode: "string" }` for ISO. On write and in WHERE, finite epoch-ms (`fx.clock.now()`) and parseable ISO strings coerce to `Date` for Postgres |
217
+ | `field.point()` / `line()` | tuple | `{ mode: "xy" }` / `"abc"` for objects |
218
+ | `field.bytea()` | `Buffer` | |
219
+ | `field.inet()` / `cidr()` / `macaddr()` / `macaddr8()` | `string` | |
220
220
 
221
221
  | Chain | Meaning |
222
222
  | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
@@ -238,21 +238,71 @@ suffix (`sql:app`).
238
238
  | `classify` | `table → column → tags` | `{}` | Explicit tags; wins over schema-derived on conflict |
239
239
  | `description` | `string` | store name | Console / docs label |
240
240
 
241
- **Named keys** — pass the tables you care about:
241
+ Pick **one** layout. `oke db` discovers `schema.ts` first, else `schema/index.ts`.
242
+ `schema.decl.ts` still resolves. `db.declare` overrides.
242
243
 
243
- ```typescript
244
- export const db = store.sql("app", { schema: { posts, notes } });
245
- ```
244
+ <Tabs items={["Single file", "Folder"]}>
245
+
246
+ <Tab value="Single file">
247
+
248
+ Blank starter. Add `store.schema.table` exports in one module. When it grows,
249
+ split into the folder and **delete** `schema.ts`.
250
+
251
+ ```typescript title="src/db/schema.ts"
252
+ import { store, field } from "okengine";
246
253
 
247
- **Module star** — starters re-export everything from the decl file:
254
+ export const posts = store.schema.table("posts", {
255
+ id: field.id().primaryKey(),
256
+ title: field.text().notNull(),
257
+ });
258
+ ```
248
259
 
249
260
  ```typescript title="src/core.ts"
250
261
  import { store } from "okengine";
251
- import * as schema from "@/db/schema.decl";
262
+ import * as schema from "@/db/schema";
263
+
264
+ export const db = store.sql("app", { schema });
265
+ ```
266
+
267
+ </Tab>
268
+
269
+ <Tab value="Folder">
270
+
271
+ Shorter and Keel. One file per table; the barrel is the declare module.
272
+
273
+ ```typescript title="src/db/schema/posts.ts"
274
+ import { store, field } from "okengine";
275
+
276
+ export const posts = store.schema.table("posts", {
277
+ id: field.id().primaryKey(),
278
+ title: field.text().notNull(),
279
+ });
280
+ ```
281
+
282
+ ```typescript title="src/db/schema/index.ts"
283
+ export * from "./posts.ts";
284
+ ```
285
+
286
+ ```typescript title="src/core/store.ts"
287
+ import { store } from "okengine";
288
+ import * as schema from "@/db/schema";
252
289
 
253
290
  export const db = store.sql("app", { schema });
254
291
  ```
255
292
 
293
+ </Tab>
294
+
295
+ </Tabs>
296
+
297
+ **Named keys** — pass only the tables you care about:
298
+
299
+ ```typescript
300
+ export const db = store.sql("app", { schema: { posts, notes } });
301
+ ```
302
+
303
+ `oke db` / `oke dev` emit generated Drizzle under `src/db/drizzle/` (one file
304
+ per table + `index.ts`). `oke dev` watches the declare path you picked.
305
+
256
306
  **Consequence:** every `fx.store(db)` touch stamps `reads` / `writes` as
257
307
  `sql:app` on the Flow — Manifest, Console, and least privilege follow from that.
258
308
 
@@ -294,19 +344,19 @@ export const get = on(
294
344
  http.get({
295
345
  in: z.object({ id: z.string() }),
296
346
  out: z.object({ id: z.string(), title: z.string() }),
297
- errors: { NotFound: z.object({ id: z.string() }) },
298
347
  }),
299
348
  flow({
300
349
  do: async ({ id }, fx) => {
301
350
  const [note] = await fx.store(db).select().from(notes).where(eq(notes.id, id));
302
- if (!note) return fx.fail("NotFound", { id });
351
+ if (!note) return fx.fail.notFound({ id });
303
352
  return note;
304
353
  },
305
354
  }),
306
355
  );
307
356
  ```
308
357
 
309
- `findById(notes, id)` is the same PK path without a fluent chain.
358
+ `findById(notes, id)` is the same PK path without a fluent chain. Declared `out` projects the
359
+ row — return it as-is.
310
360
 
311
361
  </Tab>
312
362
 
@@ -332,6 +382,8 @@ export const create = on(
332
382
  );
333
383
  ```
334
384
 
385
+ `out` projects the returned row — Date timestamps become ISO-8601; extra columns strip.
386
+
335
387
  </Tab>
336
388
 
337
389
  <Tab value="Update">
@@ -351,7 +403,6 @@ export const update = on(
351
403
  id: z.string(),
352
404
  title: z.string().min(1).optional(),
353
405
  }),
354
- errors: { NotFound: z.object({ id: z.string() }) },
355
406
  }),
356
407
  flow({
357
408
  do: async ({ id, title }, fx) => {
@@ -359,7 +410,7 @@ export const update = on(
359
410
  await fx.store(db).update(notes).set({ title }).where(eq(notes.id, id));
360
411
  }
361
412
  const row = await fx.store(db).findById(notes, id);
362
- if (!row) return fx.fail("NotFound", { id });
413
+ if (!row) return fx.fail.notFound({ id });
363
414
  return row;
364
415
  },
365
416
  }),
@@ -538,7 +589,7 @@ The URL id segment is always `:id`. Update is **PATCH**, not PUT.
538
589
  | `out` | Schema | _(required)_ | Item shape (get / list / update return) |
539
590
  | `update` | Schema | `in` | Patch fields. Wire body is `{ id, …patch }` |
540
591
  | `idSchema` | Schema | `update`/`in` + `{ id: string }` | Replaces the update Flow `in` when set (include the id key) |
541
- | `errors` | error map | `{ NotFound }` | Typed failures on get / update / remove |
592
+ | `errors` | error map | — | Extra domain codes. Built-in `NotFound` is always on |
542
593
  | `id` | column | table PK | Column bound to `:id` |
543
594
  | `list` | object | see List Options | List query grammar (`GET /notes`) |
544
595
  | `breaking` | `boolean` | `false` | Marks the five Flows `breaking: true` (handwritten → resource migration) |
@@ -671,7 +722,7 @@ Pass extras after the column map:
671
722
  <Accordion title="Policy helpers">
672
723
  Helpers take **string** column / gate / scope names and stamp predicates:
673
724
 
674
- ```typescript title="src/db/schema.decl.ts"
725
+ ```typescript title="src/db/schema.ts"
675
726
  export const bookings = store.schema.table(
676
727
  "bookings",
677
728
  {
@@ -717,7 +768,7 @@ export const shared = store.schema.table(
717
768
  `store.schema.relations` records one/many links for Drizzle emit — it does
718
769
  **not** add `fx.store(db).query.…`:
719
770
 
720
- ```typescript title="src/db/schema.decl.ts"
771
+ ```typescript title="src/db/schema.ts"
721
772
  export const links = store.schema.table("links", {
722
773
  id: field.id().primaryKey(),
723
774
  code: field.text().notNull().unique(),
@@ -771,7 +822,7 @@ recorded that identity yet. Categories:
771
822
  ```typescript title="src/db/seed/index.ts"
772
823
  import { defineSeed, type Fx } from "okengine";
773
824
  import { db } from "@/core";
774
- import { notes } from "@/db/schema.decl";
825
+ import { notes } from "@/db/schema";
775
826
 
776
827
  async function welcomeNote(fx: Fx) {
777
828
  await fx.store(db).upsert(
@@ -864,6 +915,11 @@ export default defineConfig({
864
915
  `oke db migrate` in that environment.
865
916
  </Accordion>
866
917
 
918
+ <Accordion title="Unique insert is 500 InternalError">
919
+ Unique / exclusion maps to `Conflict` **409**; missing FK to `ForeignKey` **409**. See [Errors ·
920
+ SQL auto-map](/docs/reference/errors#sql-auto-map).
921
+ </Accordion>
922
+
867
923
  <Accordion title="update().set().where(): condition required">
868
924
  Updates and deletes without a `where` are rejected (`delete().where(): condition required`). Pass
869
925
  a Drizzle condition or equality map — or use `delete(table, id)` for PK deletes.
@@ -898,6 +954,11 @@ export default defineConfig({
898
954
  columns on `store.resource(…, { list: { … } })` or your `liveQuery` options.
899
955
  </Accordion>
900
956
 
957
+ <Accordion title="@/db/schema hits schema.ts, not my schema/ folder">
958
+ Cause: both `schema.ts` and `schema/` exist — the file wins for the import and for `oke db`
959
+ discovery. Delete `schema.ts` after you split, or set `db.declare` to `schema/index.ts`.
960
+ </Accordion>
961
+
901
962
  <Accordion title="fx.store(db).query is undefined">
902
963
  There is no Relational Query Builder on the session handle. Use fluent `select` / `insert` /
903
964
  `update` / `page`, or `store.resource` for CRUD. `store.schema.relations` is emit metadata only.
@@ -918,7 +979,7 @@ export default defineConfig({
918
979
  - [HTTP · Resources](/docs/elements/flow/http#resources) — mount, live SSE, verb table
919
980
  - [Consumers · CDC](/docs/elements/flow/consumers#cdc) — `db.table(…).changed()`
920
981
  - [fx](/docs/reference/fx) — `fx.store` session
921
- - [Errors](/docs/reference/errors) — OKE1110 · OKE1041
982
+ - [Errors](/docs/reference/errors) — OKE1110 · OKE1041 · SQL auto-map
922
983
 
923
984
  ## Next
924
985
 
@@ -196,13 +196,16 @@ Order (Console labels match these source ids):
196
196
  3. **`.env.local`** — local overrides (gitignored)
197
197
  4. **dev-fallback** — `dev:` on the contract (dev boot only)
198
198
 
199
- Miss every layer → `VaultBootError` listing **all** gaps in one pass (including
200
- `vault.env.required` names):
199
+ Miss every layer → `VaultBootError` (TTY title **OKE1510**) listing **all** gaps in one pass
200
+ (including `vault.env.required` names):
201
201
 
202
202
  ```text
203
- vault boot failed — 2 missing secret(s):
204
- - STRIPE_KEY: Payments gateway key
205
- - DATABASE_URL: Primary SQL URL
203
+ ◇ OKE1510
204
+ │ 2 secrets have no value in any resolution layer.
205
+ │ - STRIPE_KEY
206
+ │ - DATABASE_URL
207
+ │ → Set each name (`oke vault set <name>`, or `.env.local`).
208
+ │ https://oke.omqkhafi.dev/e/1510
206
209
  ```
207
210
 
208
211
  **Consequence:** fix every listed name before traffic — boot does not take a half-configured app.
@@ -265,11 +268,9 @@ export default defineConfig({
265
268
  | `memory` | In-process map | Tests |
266
269
  | `managed` | Remote provider bag | AWS / Azure / GCP / Doppler / 1Password |
267
270
 
268
- create-oke Notes starters pin `vault.dev: "vault"` and declare stack + app contracts in
269
- `src/vault.ts` (`vault.secret` / `vault.config`). Console Vault lists those contracts —
270
- values still resolve from `.env.local`, process env, `oke vault set`, or `dev:` /
271
- `vault.fromDocker` fallbacks. Pass `oke({ secrets: NOTES_VAULT })` so configs resolve
272
- (only `vault.secret` auto-registers).
271
+ create-oke starters pin `vault.dev: "vault"` and declare contracts in `src/vault.ts` (blank) or `src/core/vault.ts` (shorter).
272
+ Pass `oke({ secrets })` so configs resolve (only `vault.secret` auto-registers).
273
+ Values still come from `.env.local`, process env, `oke vault set`, or `dev:` / `vault.fromDocker`.
273
274
 
274
275
  Managed provider ids: `aws-secrets-manager` · `azure-key-vault` · `gcp-secret-manager` ·
275
276
  `doppler` · `1password`. Env knobs: [Environment variables](/docs/reference/environment-variables).
@@ -299,9 +300,9 @@ Managed provider ids: `aws-secrets-manager` · `azure-key-vault` · `gcp-secret-
299
300
  <Accordions>
300
301
 
301
302
  <Accordion title="VaultBootError — N missing secret(s)">
302
- Cause: `vault boot failed — N missing secret(s):` with every gap listed. On a TTY, `oke dev`
303
- prompts each into `.env.local`; otherwise use `oke vault set` / env / managed, or a `dev:`
304
- fallback. `vault.env.required` joins the same list.
303
+ TTY title **OKE1510**. Cause: N secrets have no value in any resolution layer, with every gap
304
+ listed. On a TTY, `oke dev` prompts each into `.env.local`; otherwise use `oke vault set` / env /
305
+ managed, or a `dev:` fallback. `vault.env.required` joins the same list.
305
306
  </Accordion>
306
307
 
307
308
  <Accordion title='vault: secret "…" is not loaded'>
@@ -209,9 +209,12 @@ capability check.
209
209
  Missing non-tenant contracts fail boot with every hole listed once:
210
210
 
211
211
  ```text
212
- vault boot failed — 2 missing secret(s):
213
- - STRIPE_KEY: Stripe secret API key
214
- - APP_WEBHOOK_SECRET: HMAC secret for outbound webhooks
212
+ ◇ OKE1510
213
+ │ 2 secrets have no value in any resolution layer.
214
+ │ - STRIPE_KEY
215
+ │ - APP_WEBHOOK_SECRET
216
+ │ → Set each name (`oke vault set <name>`, or `.env.local`).
217
+ │ https://oke.omqkhafi.dev/e/1510
215
218
  ```
216
219
 
217
220
  Fill gaps with:
@@ -29,7 +29,7 @@ Master the mental model before exploring features:
29
29
  />
30
30
  <Card
31
31
  title="Try It"
32
- description="From an empty folder to a Flow running in the Console — one sitting, minimal detour."
32
+ description="Scaffold, see the files, add GET and POST /users, save a row — then email if you want."
33
33
  href="/docs/understand/try-it"
34
34
  />
35
35
  </Cards>
@@ -89,8 +89,9 @@ const { data } = await api.auth.verifyMagicLink({ token });
89
89
  | `auth.requestMagicLink` | `POST /auth/magic-link/request` | `gate.public` + otp rate |
90
90
  | `auth.verifyMagicLink` | `POST /auth/magic-link/verify` | `gate.public` + otp rate |
91
91
 
92
- **Consequence:** the plugin contributes the `auth-magic-link` Channel template and EN/AR
93
- catalog bodies (`{{link}}`, `{{token}}`). Override copy by merging your own catalog at boot.
92
+ **Consequence:** the plugin contributes `auth-magic-link` with EN/AR `catalog`
93
+ on the template (`{{link}}`, `{{token}}`). Override copy with
94
+ `oke({ channel.catalog })` or `.channelCatalog(…)` — later locale wins.
94
95
 
95
96
  ## Delivery drivers
96
97
 
@@ -155,7 +156,7 @@ unit tests without SMTP, set `exposeDevToken: true`.
155
156
 
156
157
  - [OTP](/docs/plugins/otp) — numeric code instead of a link
157
158
  - [Gate](/docs/elements/gate) — `gate.auth`
158
- - [Channel](/docs/elements/channel) — `fx.send`, Mailpit, consents
159
+ - [Channel](/docs/elements/channel) — `.template({ catalog })`, `fx.send`, Mailpit
159
160
 
160
161
  ## Next
161
162
 
@@ -148,8 +148,9 @@ app mode, or bind a Verify-capable driver (for example `taqnyat`).
148
148
  | Auto failover | On real provider send errors, sently `FallbackTransport` walks remaining media; Taqnyat WhatsApp may use `sendWithFailover` |
149
149
  | User resend | Explicit `resend` with `channel` — not automatic |
150
150
 
151
- Templates: `auth-otp-email`, `auth-otp-sms`, `auth-otp-whatsapp` (EN/AR,
152
- `{{otp}}`). SMS here is a plain message — not Taqnyat Verify.
151
+ Templates `auth-otp-email` / `auth-otp-sms` / `auth-otp-whatsapp` ship EN/AR
152
+ `catalog` on the declare (`{{otp}}`). SMS is a plain message — not Taqnyat Verify.
153
+ Override copy with `oke({ channel.catalog })` or `.channelCatalog(…)` (later locale wins).
153
154
 
154
155
  ## Options
155
156
 
@@ -209,7 +210,7 @@ In `local` / `test` the `console` driver captures messages. Use
209
210
 
210
211
  - [Magic link](/docs/plugins/magic-link) — link instead of a code
211
212
  - [Two-factor](/docs/plugins/two-factor) — email OTP / TOTP as a **second** factor after password (distinct from this primary `/auth/otp` sign-in)
212
- - [Channel](/docs/elements/channel) — `fx.send`, `fx.sendOtp`, drivers, Mailpit
213
+ - [Channel](/docs/elements/channel) — `.template({ catalog })`, `fx.sendOtp`, Mailpit
213
214
  - [Gate](/docs/elements/gate) — `gate.auth`
214
215
 
215
216
  ## Next
@@ -127,6 +127,9 @@ Disable also requires step-up when 2FA is already enabled.
127
127
  `{ twoFactorRequired, challengeId, method, userId }` (no tokens) when the
128
128
  account has 2FA enabled. Complete login only via `twoFactorVerify`.
129
129
 
130
+ Email OTP as second factor sends `auth-2fa-email` (`{{otp}}`, EN/AR `catalog` on
131
+ the template). Overlay copy the same way as other Channel templates.
132
+
130
133
  ## Troubleshooting
131
134
 
132
135
  <Accordions>
@@ -160,6 +163,7 @@ challenge. A recovery code works once for TOTP, then is consumed.
160
163
 
161
164
  ## Learn more
162
165
 
166
+ - [Channel](/docs/elements/channel) — `auth-2fa-email` catalog and Mailpit
163
167
  - [Passkey](/docs/plugins/passkey) — WebAuthn register / authenticate
164
168
  - [Gate](/docs/elements/gate) — session + policies
165
169
  - [Username](/docs/plugins/username) — first factor to enroll against
@@ -8,7 +8,7 @@ source: "docs/spec/unified-theory.md"
8
8
  Every provider below speaks a protocol oke already drives — Postgres wire or Redis wire.
9
9
  Hand the connection string to that driver; no new driver id, no Flow code changes.
10
10
 
11
- These managed providers are real, working infrastructure choices behind the same two templates (`standard`, `advanced`) and the same eight elements — not alternative products or separate frameworks.
11
+ These managed providers are real, working infrastructure choices behind the same templates (`blank`, `shorter`) and the same eight elements — not alternative products or separate frameworks.
12
12
 
13
13
  <Callout title="The one rule">
14
14
  Vendor choice lives in a connection URL (`DATABASE_URL` / `REDIS_URL`) — never in
@@ -8,7 +8,7 @@ source: "docs/spec/unified-theory.md"
8
8
  Every recipe below is already wired into `oke docker` — pin an image in `oke.config.ts`
9
9
  and get env, healthcheck, and a connection URL for free.
10
10
 
11
- These recipes are real, working infrastructure choices behind the same two templates (`standard`, `advanced`) and the same eight elements — not alternative products or separate frameworks.
11
+ These recipes are real, working infrastructure choices behind the same templates (`blank`, `shorter`) and the same eight elements — not alternative products or separate frameworks.
12
12
 
13
13
  <Callout title="The one rule">
14
14
  Vendor choice lives in `images[…]` — never in `drivers.*`, which only ever say protocol ids
@@ -27,7 +27,7 @@ drivers: {
27
27
  },
28
28
  },
29
29
  images: {
30
- store: { files: "rustfs/rustfs:1.0.0-rc.5" },
30
+ store: { files: "rustfs/rustfs:1.0.0" },
31
31
  },
32
32
  ```
33
33