okengine 0.19.9 → 0.20.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 (105) hide show
  1. package/AGENTS.md +1 -1
  2. package/package.json +1 -1
  3. package/site/content/docs/client/auth.mdx +1 -2
  4. package/site/content/docs/client/calling.mdx +8 -8
  5. package/site/content/docs/client/index.mdx +4 -4
  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 +24 -8
  14. package/site/content/docs/elements/flow/index.mdx +15 -12
  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/store/index.mdx +11 -3
  18. package/site/content/docs/elements/store/search.mdx +5 -5
  19. package/site/content/docs/elements/store/sql.mdx +91 -33
  20. package/site/content/docs/elements/vault/index.mdx +3 -5
  21. package/site/content/docs/plugins/magic-link.mdx +4 -3
  22. package/site/content/docs/plugins/otp.mdx +4 -3
  23. package/site/content/docs/plugins/two-factor.mdx +4 -0
  24. package/site/content/docs/providers/index.mdx +1 -1
  25. package/site/content/docs/recipes/index.mdx +1 -1
  26. package/site/content/docs/reference/cli.mdx +7 -4
  27. package/site/content/docs/reference/configuration.mdx +7 -7
  28. package/site/content/docs/reference/errors.mdx +30 -30
  29. package/site/content/docs/reference/fx.mdx +16 -8
  30. package/site/content/docs/reference/i18n.mdx +4 -4
  31. package/site/content/docs/reference/plugins.mdx +4 -4
  32. package/site/content/docs/understand/try-it.mdx +2 -0
  33. package/src/cli/ai-setup/ai-setup.test.ts +40 -0
  34. package/src/cli/ai-setup/apply.ts +28 -46
  35. package/src/cli/build.test.ts +3 -3
  36. package/src/cli/build.ts +5 -5
  37. package/src/cli/db-auto-push.test.ts +11 -0
  38. package/src/cli/db-auto-push.ts +6 -2
  39. package/src/cli/db.test.ts +1 -1
  40. package/src/cli/db.ts +6 -6
  41. package/src/cli/dev-db-push.test.ts +6 -2
  42. package/src/cli/dev-schema-sync.ts +1 -1
  43. package/src/cli/dev.test.ts +10 -7
  44. package/src/cli/dev.ts +13 -11
  45. package/src/cli/ensure-drizzle-config.ts +4 -3
  46. package/src/client-react/browser.test.ts +23 -0
  47. package/src/client-react/use-live-query.ts +1 -1
  48. package/src/compiler/flow-path.test.ts +1 -0
  49. package/src/compiler/flow-path.ts +1 -1
  50. package/src/compiler/generate-adopt.test.ts +55 -1
  51. package/src/compiler/generate-adopt.ts +111 -21
  52. package/src/config/index.ts +6 -4
  53. package/src/console/ui-next/dist/assets/{access-page-BoC83Ubl.js → access-page-DFLu0wTA.js} +1 -1
  54. package/src/console/ui-next/dist/assets/{agent-disclosure-CKjAEOqA.js → agent-disclosure-DGscxaF5.js} +1 -1
  55. package/src/console/ui-next/dist/assets/{cache-glyph-Ceaq9pYh.js → cache-glyph-BGmRZk7d.js} +1 -1
  56. package/src/console/ui-next/dist/assets/{call-pii-button-C4lmY7ck.js → call-pii-button--feUYxvG.js} +1 -1
  57. package/src/console/ui-next/dist/assets/{collapsible-82y257sL.js → collapsible-JWvpaiGY.js} +1 -1
  58. package/src/console/ui-next/dist/assets/{duration-tone-W63jaMZ8.js → duration-tone-D9yCJG4n.js} +1 -1
  59. package/src/console/ui-next/dist/assets/{flows-page-KFTFj2rK.js → flows-page-Bs6MD9GB.js} +1 -1
  60. package/src/console/ui-next/dist/assets/{highlighted-json-yE9zNTqC.js → highlighted-json-xH8MrEnv.js} +1 -1
  61. package/src/console/ui-next/dist/assets/{http-method-CwFeFroN.js → http-method-C4vB6ZIw.js} +1 -1
  62. package/src/console/ui-next/dist/assets/{index-r7xXt_VV.js → index-yTCY4AcS.js} +3 -3
  63. package/src/console/ui-next/dist/assets/{observability-page-BDiliMiC.js → observability-page-BxJ3R6dU.js} +1 -1
  64. package/src/console/ui-next/dist/assets/{replica-lag-DYDzWUFT.js → replica-lag-QRKB_IE8.js} +1 -1
  65. package/src/console/ui-next/dist/assets/{request-meta-CzrOfgiz.js → request-meta-DqZ-fMu5.js} +1 -1
  66. package/src/console/ui-next/dist/assets/{store-page-CZC2cwaw.js → store-page-Dixb6L7a.js} +1 -1
  67. package/src/console/ui-next/dist/assets/{trace-detail-sheet-DgeeejW7.js → trace-detail-sheet-CazhjtiU.js} +1 -1
  68. package/src/console/ui-next/dist/assets/{tree-expand-toggle-CzGIyOPY.js → tree-expand-toggle-DlnqYKfr.js} +1 -1
  69. package/src/console/ui-next/dist/assets/{units-page-DBiDCLIB.js → units-page-BXTLjU2-.js} +1 -1
  70. package/src/console/ui-next/dist/assets/{vault-page-CcHsthPe.js → vault-page-39KR__bc.js} +1 -1
  71. package/src/console/ui-next/dist/index.html +1 -1
  72. package/src/drivers/clock-postgres.test.ts +10 -2
  73. package/src/drivers/clock-postgres.ts +18 -2
  74. package/src/drivers/vault-driver-removal.test.ts +2 -2
  75. package/src/elements/channel/declare.ts +66 -3
  76. package/src/elements/channel/runtime.ts +9 -11
  77. package/src/elements/channel.test.ts +42 -0
  78. package/src/elements/channel.ts +4 -2
  79. package/src/elements/clock/reconcile.ts +45 -24
  80. package/src/elements/clock.test.ts +33 -0
  81. package/src/elements/store/emit-drizzle.ts +285 -65
  82. package/src/elements/store/load-plugin-tables.ts +1 -1
  83. package/src/elements/store/prepare-row.test.ts +57 -4
  84. package/src/elements/store/schema-decl.test.ts +178 -0
  85. package/src/elements/store/sql-session.ts +44 -2
  86. package/src/elements/store/table.ts +8 -6
  87. package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
  88. package/src/kernel/app.ts +26 -32
  89. package/src/kernel/auto-registry.test.ts +26 -1
  90. package/src/kernel/boot.ts +2 -2
  91. package/src/kernel/boundary-contract.ts +6 -1
  92. package/src/kernel/errors.ts +3 -3
  93. package/src/kernel/flow-units.ts +3 -3
  94. package/src/kernel/fx.ts +12 -2
  95. package/src/kernel/mutation-id.ts +8 -0
  96. package/src/kernel/plugin.ts +4 -3
  97. package/src/kernel/project-out.test.ts +176 -0
  98. package/src/kernel/project-out.ts +91 -0
  99. package/src/kernel/realtime-bind.ts +2 -3
  100. package/src/kernel/router/linear.ts +12 -6
  101. package/src/kernel/router.test.ts +13 -0
  102. package/src/plugins/magic-link.ts +25 -24
  103. package/src/plugins/otp.ts +35 -24
  104. package/src/plugins/two-factor.ts +15 -0
  105. package/src/runs/duckdb.test.ts +2 -2
@@ -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"]}>
246
245
 
247
- **Module star** — starters re-export everything from the decl file:
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";
253
+
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
 
@@ -306,7 +356,8 @@ export const get = on(
306
356
  );
307
357
  ```
308
358
 
309
- `findById(notes, id)` is the same PK path without a fluent chain.
359
+ `findById(notes, id)` is the same PK path without a fluent chain. Declared `out` projects the
360
+ row — return it as-is.
310
361
 
311
362
  </Tab>
312
363
 
@@ -332,6 +383,8 @@ export const create = on(
332
383
  );
333
384
  ```
334
385
 
386
+ `out` projects the returned row — Date timestamps become ISO-8601; extra columns strip.
387
+
335
388
  </Tab>
336
389
 
337
390
  <Tab value="Update">
@@ -671,7 +724,7 @@ Pass extras after the column map:
671
724
  <Accordion title="Policy helpers">
672
725
  Helpers take **string** column / gate / scope names and stamp predicates:
673
726
 
674
- ```typescript title="src/db/schema.decl.ts"
727
+ ```typescript title="src/db/schema.ts"
675
728
  export const bookings = store.schema.table(
676
729
  "bookings",
677
730
  {
@@ -717,7 +770,7 @@ export const shared = store.schema.table(
717
770
  `store.schema.relations` records one/many links for Drizzle emit — it does
718
771
  **not** add `fx.store(db).query.…`:
719
772
 
720
- ```typescript title="src/db/schema.decl.ts"
773
+ ```typescript title="src/db/schema.ts"
721
774
  export const links = store.schema.table("links", {
722
775
  id: field.id().primaryKey(),
723
776
  code: field.text().notNull().unique(),
@@ -771,7 +824,7 @@ recorded that identity yet. Categories:
771
824
  ```typescript title="src/db/seed/index.ts"
772
825
  import { defineSeed, type Fx } from "okengine";
773
826
  import { db } from "@/core";
774
- import { notes } from "@/db/schema.decl";
827
+ import { notes } from "@/db/schema";
775
828
 
776
829
  async function welcomeNote(fx: Fx) {
777
830
  await fx.store(db).upsert(
@@ -898,6 +951,11 @@ export default defineConfig({
898
951
  columns on `store.resource(…, { list: { … } })` or your `liveQuery` options.
899
952
  </Accordion>
900
953
 
954
+ <Accordion title="@/db/schema hits schema.ts, not my schema/ folder">
955
+ Cause: both `schema.ts` and `schema/` exist — the file wins for the import and for `oke db`
956
+ discovery. Delete `schema.ts` after you split, or set `db.declare` to `schema/index.ts`.
957
+ </Accordion>
958
+
901
959
  <Accordion title="fx.store(db).query is undefined">
902
960
  There is no Relational Query Builder on the session handle. Use fluent `select` / `insert` /
903
961
  `update` / `page`, or `store.resource` for CRUD. `store.schema.relations` is emit metadata only.
@@ -265,11 +265,9 @@ export default defineConfig({
265
265
  | `memory` | In-process map | Tests |
266
266
  | `managed` | Remote provider bag | AWS / Azure / GCP / Doppler / 1Password |
267
267
 
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).
268
+ create-oke starters pin `vault.dev: "vault"` and declare contracts in `src/vault.ts` (blank) or `src/core/vault.ts` (shorter).
269
+ Pass `oke({ secrets })` so configs resolve (only `vault.secret` auto-registers).
270
+ Values still come from `.env.local`, process env, `oke vault set`, or `dev:` / `vault.fromDocker`.
273
271
 
274
272
  Managed provider ids: `aws-secrets-manager` · `azure-key-vault` · `gcp-secret-manager` ·
275
273
  `doppler` · `1password`. Env knobs: [Environment variables](/docs/reference/environment-variables).
@@ -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
@@ -24,8 +24,8 @@ writes.
24
24
  ### Scaffold an app
25
25
 
26
26
  ```bash
27
- bunx create-oke@latest notes --yes
28
- cd notes
27
+ bunx create-oke@latest my-app --yes
28
+ cd my-app
29
29
  ```
30
30
 
31
31
  </Step>
@@ -98,7 +98,7 @@ oke start # production entry (Docker CMD)
98
98
 
99
99
  ```bash
100
100
  bunx create-oke@latest my-app --yes
101
- bunx create-oke@latest my-app -t advanced --locales ar --proxy caddy
101
+ bunx create-oke@latest my-app -t shorter --locales ar --proxy caddy
102
102
  bunx create-oke@latest # interactive (TTY only)
103
103
  ```
104
104
 
@@ -164,7 +164,7 @@ oke db search-backfill notes --batch 500
164
164
 
165
165
  | Flag | Meaning |
166
166
  | -------------------------------- | --------------------------------------------------------- |
167
- | `-t, --template <id>` | `standard` (default) or `advanced` |
167
+ | `-t, --template <id>` | `blank` (default) · `shorter` |
168
168
  | `--sql <id>` | Store SQL dialect — only `postgres` (test stays `pglite`) |
169
169
  | `-y, --yes` | No prompts; defaults + bun install (no `oke dev`) |
170
170
  | `--install` / `--no-install` | Run or skip `bun install` after scaffold |
@@ -175,6 +175,9 @@ oke db search-backfill notes --batch 500
175
175
  | `--proxy <id>` / `--no-proxy` | `none` · `caddy` · `traefik` · `nginx` |
176
176
  | `-h, --help` | Show help |
177
177
 
178
+ `blank` is an empty app (`main.health` + Store/Vault). `shorter` is a URL shortener
179
+ (`POST /links`, public `GET /:code`).
180
+
178
181
  ## Troubleshooting
179
182
 
180
183
  <Accordions>
@@ -212,13 +212,13 @@ Runs retention and redaction — **not** the same key as `drivers.runs`.
212
212
 
213
213
  Domain schema sync for `oke db push | generate | migrate` (Drizzle). Unrelated to `oke schema generate`.
214
214
 
215
- | Option | Default | Meaning |
216
- | ----------- | ---------------------------- | ------------------------------------------------------------------------- |
217
- | `autoPush` | `true` | Auto-run `db push` on schema change under `oke dev`; forced off in `prod` |
218
- | `config` | `"drizzle.config.ts"` | Path to the drizzle-kit config |
219
- | `declare` | `"src/db/schema.decl.ts"` | Abstract schema module (`store.schema.table` exports) |
220
- | `generated` | `"src/db/schema.drizzle.ts"` | Where `oke db` emits dialect Drizzle |
221
- | `entry` | `src/app.ts` | App entry for collecting plugin table contributions |
215
+ | Option | Default | Meaning |
216
+ | ----------- | --------------------- | -------------------------------------------------------------------------------- |
217
+ | `autoPush` | `true` | Auto-run `db push` on schema change under `oke dev`; forced off in `prod` |
218
+ | `config` | `"drizzle.config.ts"` | Path to the drizzle-kit config |
219
+ | `declare` | auto | `schema.ts` (file) if present, else `schema/index.ts` (folder). Set to override. |
220
+ | `generated` | auto | `src/db/drizzle/index.ts`. Set to override. |
221
+ | `entry` | `src/app.ts` | App entry for collecting plugin table contributions |
222
222
 
223
223
  ## topology
224
224
 
@@ -78,35 +78,35 @@ string. Custom app codes stay message-less until registered. Full catalogs:
78
78
 
79
79
  ## OKE numeric codes
80
80
 
81
- | Code | Name | Cause | Fix |
82
- | ------ | ---------------------- | ------------------------------------------------------ | --------------------------------------------------------- |
83
- | `1001` | undeclared read | Flow reads a resource not in `effects.reads` | Add it to the flow's `effects.reads` |
84
- | `1002` | undeclared write | Flow writes a resource not in `effects.writes` | Add it to the flow's `effects.writes` |
85
- | `1003` | undeclared emit | Flow emits a signal not in `effects.emits` | Add it to the flow's `effects.emits` |
86
- | `1004` | undeclared send | Flow sends a template not in `effects.sends` | Add it to the flow's `effects.sends` |
87
- | `1005` | undeclared ask | Flow asks a prompt not in `effects.asks` | Add it to the flow's `effects.asks` |
88
- | `1006` | undeclared secret | Flow reads a secret not in `effects.secrets` | Add it to the flow's `effects.secrets` |
89
- | `1007` | undeclared call | Flow calls a flow not in `effects.calls` | Add it to the flow's `effects.calls` |
90
- | `1008` | undeclared fetch | Flow fetches a host not in `effects.fetches` | Add the hostname to the flow's `effects.fetches` |
91
- | `1009` | undeclared embed | Flow embeds with a model not in `effects.embeds` | Add it to the flow's `effects.embeds` |
92
- | `1020` | no effects declared | Flow has no `effects` and no Manifest to infer from | Run `oke build` / `oke dev`, or declare effects |
93
- | `1030` | adopt barrel stale | A `src/flows/<unit>` folder was not adopted | Run `oke dev` or `oke build` to regenerate `generated.ts` |
94
- | `1040` | HTTP path unresolved | Pathless `http.get()` never received a file-tree stamp | Import `@/flows/generated`, or pass `http.get("/…")` |
95
- | `1041` | HTTP route clash | Two HTTP flows share the same method + path | Give each flow a unique method + path |
96
- | `1045` | HTTP flow unnamed | Adopted HTTP flow still has no `unit.export` | Export from `flows/<unit>/` or pass a named `flow` |
97
- | `1050` | live exposure dup | Same signal, gates, and match on two GET routes | Change the gate or path-param filter |
98
- | `1060` | MCP tool duplicate | Two MCP tool bindings share the same tool name | Give each MCP tool exposure a unique name |
99
- | `1070` | flow name duplicate | Two Flows share the same Manifest / `fx.call` name | Give at least one an explicit `flow("…")` or tree export |
100
- | `1071` | once-signal multi-flow | Two different Flows bound to the same `signal.once` | Use `signal.broadcast`, or bind only one Flow |
101
- | `1072` | flow unnamed | Signal / Clock consumer still has no `unit.export` | Export from `flows/<unit>/` or pass a named `flow` |
102
- | `1110` | schema missing | Domain table absent in `prod` — no auto-DDL | Run `oke db migrate` against this environment |
103
- | `1210` | live resume gap | `Last-Event-ID` is not on the retained tape | Reconnect without the cursor; remaining tape replays |
104
- | `1240` | orphan emit | Emit with zero subscribers and `optional` false | Add `on(signal, …)` or declare `optional: true` |
105
- | `1250` | signal schema | Emit payload failed the signal's `schema` | Pass a payload that matches `schema`, or remove it |
106
- | `1605` | channel schema | Send payload failed the template's `schema` | Fix template `data` payload or the template `schema` |
107
- | `1810` | tenant required | Tenant-scoped op with no `fx.tenant.id` | `switchTenant`, signed `tid`, or tenant header |
108
- | `1820` | tenant not member | Client-supplied tenant id is not a membership | Pick from `listTenants` or add the user as a member |
109
- | `1830` | tenant unknown scope | Tenant role used an invented or `console:*` scope | Use a declared application scope |
81
+ | Code | Name | Cause | Fix |
82
+ | ------ | ---------------------- | ------------------------------------------------------ | --------------------------------------------------------------- |
83
+ | `1001` | undeclared read | Flow reads a resource not in `effects.reads` | Add it to the flow's `effects.reads` |
84
+ | `1002` | undeclared write | Flow writes a resource not in `effects.writes` | Add it to the flow's `effects.writes` |
85
+ | `1003` | undeclared emit | Flow emits a signal not in `effects.emits` | Add it to the flow's `effects.emits` |
86
+ | `1004` | undeclared send | Flow sends a template not in `effects.sends` | Add it to the flow's `effects.sends` |
87
+ | `1005` | undeclared ask | Flow asks a prompt not in `effects.asks` | Add it to the flow's `effects.asks` |
88
+ | `1006` | undeclared secret | Flow reads a secret not in `effects.secrets` | Add it to the flow's `effects.secrets` |
89
+ | `1007` | undeclared call | Flow calls a flow not in `effects.calls` | Add it to the flow's `effects.calls` |
90
+ | `1008` | undeclared fetch | Flow fetches a host not in `effects.fetches` | Add the hostname to the flow's `effects.fetches` |
91
+ | `1009` | undeclared embed | Flow embeds with a model not in `effects.embeds` | Add it to the flow's `effects.embeds` |
92
+ | `1020` | no effects declared | Flow has no `effects` and no Manifest to infer from | Run `oke build` / `oke dev`, or declare effects |
93
+ | `1030` | adopt barrel stale | A `src/flows/<unit>` folder was not adopted | Run `oke dev` or `oke build` to regenerate `src/flows/index.ts` |
94
+ | `1040` | HTTP path unresolved | Pathless `http.get()` never received a file-tree stamp | Import `@/flows`, or pass `http.get("/…")` |
95
+ | `1041` | HTTP route clash | Two HTTP flows share the same method + path | Give each flow a unique method + path |
96
+ | `1045` | HTTP flow unnamed | Adopted HTTP flow still has no `unit.export` | Export from `flows/<unit>/` or pass a named `flow` |
97
+ | `1050` | live exposure dup | Same signal, gates, and match on two GET routes | Change the gate or path-param filter |
98
+ | `1060` | MCP tool duplicate | Two MCP tool bindings share the same tool name | Give each MCP tool exposure a unique name |
99
+ | `1070` | flow name duplicate | Two Flows share the same Manifest / `fx.call` name | Give at least one an explicit `flow("…")` or tree export |
100
+ | `1071` | once-signal multi-flow | Two different Flows bound to the same `signal.once` | Use `signal.broadcast`, or bind only one Flow |
101
+ | `1072` | flow unnamed | Signal / Clock consumer still has no `unit.export` | Export from `flows/<unit>/` or pass a named `flow` |
102
+ | `1110` | schema missing | Domain table absent in `prod` — no auto-DDL | Run `oke db migrate` against this environment |
103
+ | `1210` | live resume gap | `Last-Event-ID` is not on the retained tape | Reconnect without the cursor; remaining tape replays |
104
+ | `1240` | orphan emit | Emit with zero subscribers and `optional` false | Add `on(signal, …)` or declare `optional: true` |
105
+ | `1250` | signal schema | Emit payload failed the signal's `schema` | Pass a payload that matches `schema`, or remove it |
106
+ | `1605` | channel schema | Send payload failed the template's `schema` | Fix template `data` payload or the template `schema` |
107
+ | `1810` | tenant required | Tenant-scoped op with no `fx.tenant.id` | `switchTenant`, signed `tid`, or tenant header |
108
+ | `1820` | tenant not member | Client-supplied tenant id is not a membership | Pick from `listTenants` or add the user as a member |
109
+ | `1830` | tenant unknown scope | Tenant role used an invented or `console:*` scope | Use a declared application scope |
110
110
 
111
111
  <Callout title="Effects are usually inferred">
112
112
  The 1001–1007 · 1008 · 1009 family exists for flows that declare effects explicitly. Most apps
@@ -167,7 +167,7 @@ Thrown by specific subsystems — each names its own cause:
167
167
  </Accordion>
168
168
 
169
169
  <Accordion title="OKE1040 pathless HTTP never stamped">
170
- Import `@/flows/generated`, or pass an explicit path: `http.get("/users/:id")`.
170
+ Import `@/flows`, or pass an explicit path: `http.get("/users/:id")`.
171
171
  </Accordion>
172
172
 
173
173
  <Accordion title="OKE1041 method + path bound twice">
@@ -211,7 +211,8 @@ const rows = await fx.using(
211
211
  | `fx.send(template, { to?, data?, via?, … })` | `send` | `via` orders fallback; `locale` / `profileLocale` / `acceptLanguage` feed the locale chain |
212
212
 
213
213
  Omit locale opts and the send uses `fx.locale`. Dry runs record _would have fired_ and never
214
- contact a provider. Channel bodies use `{{field}}` catalogs — not ICU (see [Channel](/docs/elements/channel)).
214
+ contact a provider. Bodies live on `.template({ catalog })` (`{{field}}`, not ICU — see
215
+ [Channel](/docs/elements/channel)).
215
216
 
216
217
  ## Outbound HTTP
217
218
 
@@ -255,13 +256,13 @@ host tooltip).
255
256
 
256
257
  ## Clock
257
258
 
258
- | Signature | Notes |
259
- | --------------------------------- | ------------------------------------------------------------------- |
260
- | `fx.clock.now()` | Epoch-ms, injectable — the only legal "now" |
261
- | `fx.clock.ago(duration)` | Instant before now (`"30d"` → now − 30 days) |
262
- | `fx.clock.fromNow(duration)` | Instant after now (`"14d"` → now + 14 days) |
263
- | `fx.clock.duration(duration)` | Span in ms — offset a stored instant (`createdAt + duration("7d")`) |
264
- | `fx.clock.sleep(label, duration)` | Durable sleep in `durable` flows; immediate otherwise |
259
+ | Signature | Notes |
260
+ | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
261
+ | `fx.clock.now()` | Epoch-ms, injectable — the only legal "now"; pass into SQL timestamps as-is (store coerces numbers and ISO strings to `Date`) |
262
+ | `fx.clock.ago(duration)` | Instant before now (`"30d"` → now − 30 days) |
263
+ | `fx.clock.fromNow(duration)` | Instant after now (`"14d"` → now + 14 days) |
264
+ | `fx.clock.duration(duration)` | Span in ms — offset a stored instant (`createdAt + duration("7d")`) |
265
+ | `fx.clock.sleep(label, duration)` | Durable sleep in `durable` flows; immediate otherwise |
265
266
 
266
267
  Durations: `"200ms"` · `"30s"` · `"2m"` · `"1h"` · `"7d"`. A `"d"` is 86_400_000 ms, not a calendar day. Unknown strings parse as `0`.
267
268
 
@@ -293,6 +294,8 @@ keys. Use `cache: false` to opt out, or `cache: "30s"` for a TTL.
293
294
 
294
295
  Returning a plain value instead answers 200 with `{ data: value, error: null }` — the helpers exist for status and `meta` control. Pass `fx.stream(...)` into `fx.json.stream` to reach the HTTP client token-by-token.
295
296
 
297
+ When `out` is set, success values are projected onto it (`fx.json.create(row)`, `return row`, `fx.json.withQuery(rows, input)`). Date timestamps and temporal epoch-ms (`*At` / `*_at` / `at`) become ISO-8601 and extra keys strip. A parse miss is not a client error.
298
+
296
299
  ## Logging, i18n, ids
297
300
 
298
301
  | Signature | Notes |
@@ -355,6 +358,11 @@ to the key — see [Gate](/docs/elements/gate#api-keys).
355
358
  Type the parameter as `Fx` from `okengine`, not a hand-rolled structural type.
356
359
  </Accordion>
357
360
 
361
+ <Accordion title="I mapped Date columns to ISO by hand">
362
+ Declared `out` projects success values. `fx.json.create(row)`, `return row`, and
363
+ `fx.json.withQuery(rows, input)` are enough — extra columns strip. A miss is not a client error.
364
+ </Accordion>
365
+
358
366
  </Accordions>
359
367
 
360
368
  ## Learn more
@@ -257,8 +257,8 @@ until registered. Full tables: [Errors](/docs/reference/errors).
257
257
 
258
258
  ## Channel catalogs are separate
259
259
 
260
- `fx.send` templates use `{{field}}` bodies and their own `locales` list — not
261
- ICU. Omit `locale` / `profileLocale` / `acceptLanguage` on `fx.send` and the
260
+ `fx.send` interpolates `{{field}}` from `.template({ catalog })` — not ICU, not
261
+ `fx.t`. Omit `locale` / `profileLocale` / `acceptLanguage` on `fx.send` and the
262
262
  send uses `fx.locale`. Details: [Channel](/docs/elements/channel).
263
263
 
264
264
  ## Troubleshooting
@@ -280,8 +280,8 @@ the app (proxies sometimes strip it).
280
280
  </Accordion>
281
281
  <Accordion title="Email body is still English while fx.t is Arabic">
282
282
 
283
- Channel catalogs are separate `{{field}}` strings. Add an `ar` body on the
284
- template / plugin catalog; `fx.t` does not translate Channel templates.
283
+ Channel catalogs are separate `{{field}}` strings. Add an `ar` key on
284
+ `.template({ catalog })`; `fx.t` does not translate Channel templates.
285
285
 
286
286
  </Accordion>
287
287
  <Accordion title="TypeScript rejects a key that exists at runtime">
@@ -54,7 +54,7 @@ export const app = oke({ name: "shop", env: "dev" }).plug(audit);
54
54
  <Step>
55
55
  ### Everything derives as usual
56
56
 
57
- Plugin flows appear in the Manifest, plugin tables land in `schema.drizzle.ts` on the next `oke db push`, plugin panels show up in the Console. No extra wiring — a contribution is ordinary OKE, just authored elsewhere.
57
+ Plugin flows appear in the Manifest, plugin tables land in `src/db/drizzle/` on the next `oke db push`, plugin panels show up in the Console. No extra wiring — a contribution is ordinary OKE, just authored elsewhere.
58
58
 
59
59
  </Step>
60
60
 
@@ -76,8 +76,8 @@ Every method below exists on both the fluent definition and the boot-time builde
76
76
  | `.clock(decl)` | A named clock schedule — merged into boot clocks |
77
77
  | `.signal(decl)` | A signal declaration — merged into boot signals |
78
78
  | `.gate(decl)` | A gate declaration — merged into boot gates |
79
- | `.channelTemplate(decl)` | A channel template — merged into boot channel templates |
80
- | `.channelCatalog(catalog)` | Template body catalog entries — merged into boot channel catalog (`{{field}}` interpolation) |
79
+ | `.channelTemplate(decl)` | `channel.<medium>().template(…)` — `catalog` on the decl drains into boot |
80
+ | `.channelCatalog(catalog)` | Overlay bodies after `.template({ catalog })` (`{{field}}`); later locale wins |
81
81
  | `.driver(id, impl)` | A protocol-named driver for an existing element |
82
82
  | `.image(role, recipe)` | An image recipe for a docker role |
83
83
  | `.table(name, columns, options)` | A whole DB table, merged into the generated schema (`options.description` / `plane` optional) |
@@ -226,7 +226,7 @@ Extending an existing **app-owned** table with plugin columns is not supported i
226
226
  two configurations of one plugin would silently diverge. Pass identical config, or rename one
227
227
  instance.
228
228
  </Accordion>
229
- <Accordion title="My plugin table is missing from schema.drizzle.ts">
229
+ <Accordion title="My plugin table is missing from drizzle/">
230
230
  The CLI reads table contributions from the live app entry. Make sure the plugin is actually
231
231
  `.plug()`ed in `src/app.ts` (or `db.entry` if overridden), then re-run `oke db push`.
232
232
  </Accordion>