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
@@ -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>
@@ -151,7 +151,7 @@ images: {
151
151
  store: {
152
152
  sql: "postgres:18-alpine",
153
153
  kv: "redis:8-alpine",
154
- files: "rustfs/rustfs:1.0.0-rc.5",
154
+ files: "rustfs/rustfs:1.0.0",
155
155
  },
156
156
  channel: { email: "axllent/mailpit:v1.31.1" },
157
157
  pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.57",
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: "Errors"
3
- description: "OKE codes, gate denials, and subsystem errors — stable numeric codes with causes and fixes."
3
+ description: "fx.fail helpers, built-in HTTP statuses, OKE codes, and subsystem errors — values vs thrown invariants."
4
4
  icon: "OctagonAlert"
5
5
  source: "docs/spec/unified-theory.md"
6
6
  ---
@@ -19,8 +19,8 @@ Codes are stable after the domain-range renumber — match them safely across up
19
19
  (see the Unreleased Breaking Changes mapping if you still have a pre-renumber note).
20
20
 
21
21
  <Callout title="The one rule">
22
- Switch on `error.code` for Flow failures. Catch / match `OKE####` only for thrown framework
23
- invariants. Do not treat a typed `NotFound` as an unhandled exception.
22
+ Switch on `error.code` (or a `match` named arm) for typed `data`. Use kind arms / `explain` for
23
+ chrome when the code is unknown. Catch `OKE####` only for thrown framework invariants.
24
24
  </Callout>
25
25
 
26
26
  ## Smallest Example
@@ -34,12 +34,11 @@ Codes are stable after the domain-range renumber — match them safely across up
34
34
  on(
35
35
  http.get({
36
36
  in: z.object({ id: z.string() }),
37
- errors: { NotFound: z.object({ id: z.string() }) },
38
37
  }),
39
38
  flow({
40
39
  do: async ({ id }, fx) => {
41
40
  const [row] = await fx.store(db).select().from(notes).where(eq(notes.id, id));
42
- if (!row) return fx.fail("NotFound", { id });
41
+ if (!row) return fx.fail.notFound({ id });
43
42
  return row;
44
43
  },
45
44
  }),
@@ -52,16 +51,163 @@ on(
52
51
  ### Handle it on the client
53
52
 
54
53
  ```typescript
55
- const { data, error } = await api.notes.get({ id });
56
- if (error?.code === "NotFound") {
57
- // value — not a throw
58
- }
54
+ import { match } from "okengine/client/explain";
55
+
56
+ const result = await api.notes.get({ id });
57
+ match(result, {
58
+ ok: (note) => render(note),
59
+ NotFound: () => showMissing(),
60
+ _: (e) => toast(e.message),
61
+ });
59
62
  ```
60
63
 
61
64
  </Step>
62
65
 
63
66
  </Steps>
64
67
 
68
+ ## `fx.fail`
69
+
70
+ Same function as `import { fail } from "okengine"`. Returns `{ data: null, error }` — it does **not**
71
+ throw. Built-in codes need no `errors:` bag. Domain codes (`OutOfStock`, `FlightFull`, `Duplicate`)
72
+ still go on the exposure:
73
+
74
+ ```typescript
75
+ on(
76
+ http.post({
77
+ in: z.object({ sku: z.string() }),
78
+ errors: { OutOfStock: z.object({ available: z.number() }) },
79
+ }),
80
+ flow({
81
+ do: async ({ sku }, fx) => {
82
+ const available = 0;
83
+ if (available === 0) return fx.fail("OutOfStock", { available });
84
+ return { sku };
85
+ },
86
+ }),
87
+ );
88
+ ```
89
+
90
+ Optional `{ message }` overrides the catalog: `fx.fail("OutOfStock", data, { message })`.
91
+
92
+ ### Helpers
93
+
94
+ | Call | Code | Status | Payload |
95
+ | ----------------------------------- | -------------------- | ---------------- | --------------------------------------------------------- |
96
+ | `fx.fail.notFound(data?)` | `NotFound` | **404** | `id?`, `key?`, … |
97
+ | `fx.fail.unauthorized(data?)` | `Unauthorized` | **401** | `reason?` |
98
+ | `fx.fail.forbidden(data?)` | `Forbidden` | **403** | `gate?`, `reason?` |
99
+ | `fx.fail.conflict(data?)` | `Conflict` | **409** | `constraint?`, `table?`, `column?`, `id?` |
100
+ | `fx.fail.foreignKey(data?)` | `ForeignKey` | **409** | `constraint?`, `table?`, `column?` |
101
+ | `fx.fail.rateLimited(data?)` | `RateLimited` | **429** | `retryAfterMs?` |
102
+ | `fx.fail.serviceUnavailable(data?)` | `ServiceUnavailable` | **503** | `retryAfter?` |
103
+ | `fx.fail.database(data)` | `DatabaseError` | by `data.reason` | `reason`, `sqlstate?`, `constraint?`, `table?`, `column?` |
104
+ | `fx.fail.internal(data?)` | `InternalError` | **500** | `{}` — catalog message only |
105
+
106
+ `DatabaseError` status: `not_null` / `check` / `invalid` / `too_long` / `out_of_range` → **422**,
107
+ `retryable` → **503**, `unknown` / missing → **500**.
108
+
109
+ Declare a built-in on `errors:` **only to tighten** the payload. Shorter keeps
110
+ `NotFound: z.object({ code: z.string() })` because `{ code }` is not the built-in `{ id? }`.
111
+ Declared schema wins on the Manifest and the typed client.
112
+
113
+ The typed client always includes built-in codes. A declared key still wins.
114
+
115
+ ### HTTP status
116
+
117
+ A bare `404` with body `Not Found` means **no route matched**. `fx.fail.notFound` is a JSON envelope
118
+ at **404**. Clients distinguish by envelope, not status. Domain codes stay **400**.
119
+
120
+ | Code | Status | Helper |
121
+ | ------------------------------------------------ | ----------- | ------------------------------------------------------ |
122
+ | `ValidationError` | `422` | none — failed `in`, `do` never runs |
123
+ | `Unauthorized` | `401` | `unauthorized` · also a gate denial |
124
+ | `Forbidden` | `403` | `forbidden` · also a gate denial |
125
+ | `NotFound` | `404` | `notFound` |
126
+ | `Conflict` / `ForeignKey` | `409` | `conflict` / `foreignKey` |
127
+ | `UnsupportedMediaType` | `415` | none — QUERY `Content-Type` is not `application/json` |
128
+ | `RateLimited` / `AuthRateLimited` | `429` | `rateLimited` · `AuthRateLimited` has none |
129
+ | `InvalidQuery` | `400` | none — QUERY missing `Content-Type` or body isn't JSON |
130
+ | `AuthFailed` | `400` | none — credentials / policy, not “no session” |
131
+ | `DatabaseError` | by `reason` | `database` |
132
+ | `ServiceUnavailable` | `503` | `serviceUnavailable` |
133
+ | `InternalError` | `500` | `internal` — never copies a thrown `message` |
134
+ | Domain (`OutOfStock`, `FlightFull`, `Duplicate`) | `400` | `fx.fail("OutOfStock", data)` |
135
+
136
+ ### Client chrome
137
+
138
+ `import { match, matchError, explain } from "okengine/client/explain"`. Do not switch on every
139
+ code — a `create` can return store auto-map, gates, transport, and domain codes.
140
+
141
+ | `kind` | Codes |
142
+ | ------------- | -------------------------------------------------------------------------------------- |
143
+ | `auth` | `Unauthorized`, `AuthFailed` |
144
+ | `permission` | `Forbidden` |
145
+ | `missing` | `NotFound`; `TransportError` with HTTP 404 |
146
+ | `conflict` | `Conflict`, `ForeignKey` |
147
+ | `invalid` | `ValidationError`, `InvalidQuery`, `UnsupportedMediaType`; `DatabaseError` 422 reasons |
148
+ | `limited` | `RateLimited`, `AuthRateLimited` |
149
+ | `unavailable` | `ServiceUnavailable`; `DatabaseError` `retryable`; other `TransportError` |
150
+ | `failed` | `InternalError`, unknown `DatabaseError`, domain codes |
151
+
152
+ ```typescript
153
+ match(result, {
154
+ ok: (data) => show(data),
155
+ FlightFull: (data) => offerWaitlist(data.seatsLeft),
156
+ auth: () => goSignIn(),
157
+ invalid: (e) => setForm(e.fields),
158
+ _: (e) => toast(e.message),
159
+ });
160
+ ```
161
+
162
+ Dispatch: `ok` → named code → kind → required `_`. `explain(error).fields` maps
163
+ `ValidationError` issues to `{ email: "Required" }`. Full loop: [Calling](/docs/client/calling).
164
+
165
+ `TransportError` always sets both `error.message` and `error.data.message` to the same string.
166
+ Prefer `error.message` in UI code.
167
+
168
+ ### Store auto-map
169
+
170
+ `fx.store` constraint and connection failures become typed `fail` values (same family as
171
+ `ValidationError`). Public `data` is names only — never `message` / `detail` / `hint`.
172
+ `get` returning `null` is **not** auto-`NotFound` — return `fx.fail.notFound` yourself.
173
+
174
+ #### SQL auto-map
175
+
176
+ | Signal | Result |
177
+ | --------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
178
+ | Postgres `23505` / `23P01` · SQLite UNIQUE / PRIMARY KEY | `Conflict` **409** |
179
+ | Postgres `23503` / `23001` · SQLite FOREIGN KEY | `ForeignKey` **409** |
180
+ | Postgres `23502` / `23514` · SQLite NOT NULL / CHECK | `DatabaseError` `not_null` / `check` **422** |
181
+ | Postgres `22P02` / `22007` / `22023` / `42804` | `DatabaseError` `invalid` **422** |
182
+ | Postgres `22001` | `DatabaseError` `too_long` **422** |
183
+ | Postgres `22003` / `22008` | `DatabaseError` `out_of_range` **422** |
184
+ | Postgres `40001` / `40P01` / `55P03` · SQLite BUSY | left thrown so `flow.retry` can run; after exhaust → `ServiceUnavailable` **503** |
185
+ | `08xxx` / `53xxx` / `57P0x` / `57014` / `25P03` / connection / paused | `ServiceUnavailable` **503** |
186
+ | `42P01` / `42703` with `domainDdl: "off"` | **OKE1110** (unchanged) |
187
+ | other SQLSTATE | `DatabaseError` `unknown` **500** |
188
+
189
+ #### KV auto-map
190
+
191
+ Redis-wire `store.kv` only (not Gate / Signal clients). Memory KV does not throw these.
192
+
193
+ | Signal | Result |
194
+ | ----------------------------------------------------------- | --------------------------------------------------------------------------------- |
195
+ | Connection / `CLUSTERDOWN` / `READONLY` / `OOM` / `MISCONF` | `ServiceUnavailable` **503** |
196
+ | `BUSY` / `TRYAGAIN` / `LOADING` | left thrown so `flow.retry` can run; after exhaust → `ServiceUnavailable` **503** |
197
+ | `WRONGTYPE` / `NOSCRIPT` | unmapped — `InternalError` **500** |
198
+
199
+ #### Files auto-map
200
+
201
+ `fs` and `s3` (`fx.store` files handle). Missing objects stay `null`.
202
+
203
+ | Signal | Result |
204
+ | -------------------------------------------------- | --------------------------------------------------------------------------------- |
205
+ | `AccessDenied` / `EACCES` / `EPERM` / HTTP **403** | `Forbidden` **403** |
206
+ | `fs` key starts with `/` or contains `..` | `Forbidden` **403** (`reason: invalid_key`) |
207
+ | `NoSuchBucket` / `ENOSPC` / connection | `ServiceUnavailable` **503** |
208
+ | `SlowDown` / `EBUSY` / HTTP **429** | left thrown so `flow.retry` can run; after exhaust → `ServiceUnavailable` **503** |
209
+ | `NoSuchKey` (thrown) | unmapped — `InternalError` **500** |
210
+
65
211
  ## Localized messages
66
212
 
67
213
  Typed failures and OKE codes ship English and Arabic ICU catalogs. Locale comes
@@ -78,35 +224,36 @@ string. Custom app codes stay message-less until registered. Full catalogs:
78
224
 
79
225
  ## OKE numeric codes
80
226
 
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 |
227
+ | Code | Name | Cause | Fix |
228
+ | ------ | ---------------------- | ------------------------------------------------------ | --------------------------------------------------------------- |
229
+ | `1001` | undeclared read | Flow reads a resource not in `effects.reads` | Add it to the flow's `effects.reads` |
230
+ | `1002` | undeclared write | Flow writes a resource not in `effects.writes` | Add it to the flow's `effects.writes` |
231
+ | `1003` | undeclared emit | Flow emits a signal not in `effects.emits` | Add it to the flow's `effects.emits` |
232
+ | `1004` | undeclared send | Flow sends a template not in `effects.sends` | Add it to the flow's `effects.sends` |
233
+ | `1005` | undeclared ask | Flow asks a prompt not in `effects.asks` | Add it to the flow's `effects.asks` |
234
+ | `1006` | undeclared secret | Flow reads a secret not in `effects.secrets` | Add it to the flow's `effects.secrets` |
235
+ | `1007` | undeclared call | Flow calls a flow not in `effects.calls` | Add it to the flow's `effects.calls` |
236
+ | `1008` | undeclared fetch | Flow fetches a host not in `effects.fetches` | Add the hostname to the flow's `effects.fetches` |
237
+ | `1009` | undeclared embed | Flow embeds with a model not in `effects.embeds` | Add it to the flow's `effects.embeds` |
238
+ | `1020` | no effects declared | Flow has no `effects` and no Manifest to infer from | Run `oke build` / `oke dev`, or declare effects |
239
+ | `1030` | adopt barrel stale | A `src/flows/<unit>` folder was not adopted | Run `oke dev` or `oke build` to regenerate `src/flows/index.ts` |
240
+ | `1040` | HTTP path unresolved | Pathless `http.get()` never received a file-tree stamp | Import `@/flows`, or pass `http.get("/…")` |
241
+ | `1041` | HTTP route clash | Two HTTP flows share the same method + path | Give each flow a unique method + path |
242
+ | `1045` | HTTP flow unnamed | Adopted HTTP flow still has no `unit.export` | Export from `flows/<unit>/` or pass a named `flow` |
243
+ | `1050` | live exposure dup | Same signal, gates, and match on two GET routes | Change the gate or path-param filter |
244
+ | `1060` | MCP tool duplicate | Two MCP tool bindings share the same tool name | Give each MCP tool exposure a unique name |
245
+ | `1070` | flow name duplicate | Two Flows share the same Manifest / `fx.call` name | Give at least one an explicit `flow("…")` or tree export |
246
+ | `1071` | once-signal multi-flow | Two different Flows bound to the same `signal.once` | Use `signal.broadcast`, or bind only one Flow |
247
+ | `1072` | flow unnamed | Signal / Clock consumer still has no `unit.export` | Export from `flows/<unit>/` or pass a named `flow` |
248
+ | `1110` | schema missing | Domain table absent in `prod` — no auto-DDL | Run `oke db migrate` against this environment |
249
+ | `1510` | vault secret missing | Contract has no value in any resolution layer | `oke vault set <name>`, `.env.local`, or a `dev:` fallback |
250
+ | `1210` | live resume gap | `Last-Event-ID` is not on the retained tape | Reconnect without the cursor; remaining tape replays |
251
+ | `1240` | orphan emit | Emit with zero subscribers and `optional` false | Add `on(signal, …)` or declare `optional: true` |
252
+ | `1250` | signal schema | Emit payload failed the signal's `schema` | Pass a payload that matches `schema`, or remove it |
253
+ | `1605` | channel schema | Send payload failed the template's `schema` | Fix template `data` payload or the template `schema` |
254
+ | `1810` | tenant required | Tenant-scoped op with no `fx.tenant.id` | `switchTenant`, signed `tid`, or tenant header |
255
+ | `1820` | tenant not member | Client-supplied tenant id is not a membership | Pick from `listTenants` or add the user as a member |
256
+ | `1830` | tenant unknown scope | Tenant role used an invented or `console:*` scope | Use a declared application scope |
110
257
 
111
258
  <Callout title="Effects are usually inferred">
112
259
  The 1001–1007 · 1008 · 1009 family exists for flows that declare effects explicitly. Most apps
@@ -116,7 +263,8 @@ string. Custom app codes stay message-less until registered. Full catalogs:
116
263
 
117
264
  ## Gate denials (typed failures)
118
265
 
119
- Returned, not thrown — the request never reached `do`:
266
+ Returned before `do` — not thrown. Same codes as `fx.fail.unauthorized` / `.forbidden` /
267
+ `.rateLimited`:
120
268
 
121
269
  | Code | When | Payload |
122
270
  | -------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------ |
@@ -124,22 +272,13 @@ Returned, not thrown — the request never reached `do`:
124
272
  | `Forbidden` | Policy denied, authenticated but not allowed | `gate`, `reason` (`tenant_required` · `not_member` · `unknown_scope` · `session_only` · …) |
125
273
  | `RateLimited` | Rate gate budget exhausted | `retryAfterMs` |
126
274
 
127
- ## Framework validation failures
128
-
129
- | Code | When |
130
- | ---------------------- | ------------------------------------------------------------------------------------------------------------------- |
131
- | `ValidationError` | Input failed the `in` schema, or a list param isn't whitelisted (`unknown list param "x"`) |
132
- | `NotFound` | A `store.resource` get/update/remove hit a missing row |
133
- | `InvalidQuery` | QUERY missing `Content-Type` (`reason: missing_content_type`) or body isn't JSON (`inconsistent_content`) — **400** |
134
- | `UnsupportedMediaType` | QUERY `Content-Type` is present but not `application/json` — **415**, `Accept-Query` lists JSON |
135
-
136
275
  ## Subsystem errors
137
276
 
138
277
  Thrown by specific subsystems — each names its own cause:
139
278
 
140
279
  | Error | Thrown when | What to do |
141
280
  | ----------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------- |
142
- | `VaultBootError` | A vault contract has no value in any resolution layer | Set the missing names — the error lists every gap |
281
+ | `VaultBootError` | A vault contract has no value in any resolution layer | TTY **OKE1510** — set each listed name |
143
282
  | `VaultError` (`UNSUPPORTED`) | Managed provider unknown | Use an official id, built-in `vault`, or `env` |
144
283
  | `VaultSealed` | Console rotate-master while this process holds no master key | Export `OKE_VAULT_MASTER_KEY` or run `oke vault unseal` |
145
284
  | `VaultRotateBusy` | Master-rotation lease held or a batch is already in flight | Wait and continue, or resume with `oke vault rotate-master --new-key` |
@@ -167,7 +306,7 @@ Thrown by specific subsystems — each names its own cause:
167
306
  </Accordion>
168
307
 
169
308
  <Accordion title="OKE1040 pathless HTTP never stamped">
170
- Import `@/flows/generated`, or pass an explicit path: `http.get("/users/:id")`.
309
+ Import `@/flows`, or pass an explicit path: `http.get("/users/:id")`.
171
310
  </Accordion>
172
311
 
173
312
  <Accordion title="OKE1041 method + path bound twice">
@@ -196,20 +335,55 @@ Thrown by specific subsystems — each names its own cause:
196
335
  membership, or rate budget. See [Gate](/docs/elements/gate).
197
336
  </Accordion>
198
337
 
338
+ <Accordion title="Do I need errors: { NotFound }?">
339
+ No. Built-in codes are always on — `return fx.fail.notFound({ id })`. Put `errors:` only for
340
+ domain codes, or to tighten a built-in payload (`{ code }` instead of `{ id? }`).
341
+ </Accordion>
342
+
343
+ <Accordion title="Unique insert is 500 InternalError">
344
+ Unique / exclusion → `Conflict` **409**; missing FK → `ForeignKey` **409**; invalid →
345
+ `DatabaseError` **422**. Serialization (`40001` / `40P01`) stays thrown until retries exhaust,
346
+ then **503**. See [SQL auto-map](#sql-auto-map).
347
+ </Accordion>
348
+
349
+ <Accordion title="Redis down returns InternalError">
350
+ Connection / `CLUSTERDOWN` / `READONLY` map to `ServiceUnavailable` **503**. `BUSY` / `TRYAGAIN`
351
+ stay thrown until `flow.retry` exhausts, then **503**. `WRONGTYPE` stays `InternalError` — see [KV
352
+ auto-map](#kv-auto-map).
353
+ </Accordion>
354
+
355
+ <Accordion title="S3 AccessDenied / disk full is InternalError">
356
+ `AccessDenied` and `fs` path-escape keys → `Forbidden` **403**. `SlowDown` / `EBUSY` stay thrown
357
+ until retries exhaust, then **503**. `ENOSPC` / `NoSuchBucket` → **503**. Missing `get` is still
358
+ `null`, not `NotFound` — see [Files auto-map](#files-auto-map).
359
+ </Accordion>
360
+
361
+ <Accordion title="404 Not Found — route missing or domain miss?">
362
+ Bare body `Not Found` means no HTTP route. JSON `{ error: { code: "NotFound" } }` is
363
+ `fx.fail.notFound`. Clients distinguish by envelope.
364
+ </Accordion>
365
+
366
+ <Accordion title="InternalError leaked a stack / thrown message">
367
+ Unhandled throws use the catalog string only. The thrown `message` is not copied onto the
368
+ envelope. Expected misses belong in `fx.fail.notFound` or a domain `fx.fail("OutOfStock", data)`.
369
+ </Accordion>
370
+
199
371
  <Accordion title="VaultBootError at startup">
200
- A vault contract has no value in any resolution layer. The error lists every gap — set the missing
201
- names ([Vault](/docs/elements/vault)).
372
+ TTY title **OKE1510**. A vault contract has no value in any resolution layer. The printer lists
373
+ every gap — set the missing names ([Vault](/docs/elements/vault)).
202
374
  </Accordion>
203
375
 
204
376
  </Accordions>
205
377
 
206
378
  ## Learn more
207
379
 
380
+ - [fx](/docs/reference/fx) — `fx.fail` helpers on the door
208
381
  - [i18n](/docs/reference/i18n) — catalogs, `fx.t`, locale matching
209
- - [Flow](/docs/elements/flow) — `fx.fail` and the response envelope
382
+ - [Flow](/docs/elements/flow) — envelope and domain `errors:`
383
+ - [HTTP](/docs/elements/flow/http) — response envelopes
384
+ - [Calling](/docs/client/calling) — `match` / `explain` on the client
210
385
  - [Gate](/docs/elements/gate) — where the three denials come from
211
386
  - [CLI](/docs/reference/cli) — `oke db migrate` and friends
212
- - [fx](/docs/reference/fx) — how failures and effects surface
213
387
 
214
388
  ## Next
215
389
 
@@ -220,5 +394,5 @@ Thrown by specific subsystems — each names its own cause:
220
394
  description="Where Unauthorized and Forbidden come from."
221
395
  href="/docs/elements/gate"
222
396
  />
223
- <Card title="Client" description="Switch on error.code in the browser." href="/docs/client" />
397
+ <Card title="Client" description="match unknown errors in the browser." href="/docs/client" />
224
398
  </Cards>
@@ -141,7 +141,10 @@ on(
141
141
  | `fx.retry(fn, opts?)` | — | Exponential backoff + jitter (plain Promise) |
142
142
  | `fx.using(acq, rel, use)` | — | `release` runs once on settle or ambient abort |
143
143
  | `fx.signal` | — | Ambient `AbortSignal` for the current branch |
144
- | `fx.fail(code, data, opts?)` | — | Typed failure value (`opts.message` overrides) |
144
+ | `fx.fail(code, data, opts?)` | — | Domain failure value (`opts.message` overrides) |
145
+ | `fx.fail.notFound(data?)` | — | Built-in `NotFound` — HTTP 404. No `errors:` bag required |
146
+ | `fx.fail.forbidden(data?)` / `.unauthorized` / `.conflict` / `.foreignKey` | — | Built-in 403 / 401 / 409 |
147
+ | `fx.fail.rateLimited` / `.serviceUnavailable` / `.database` / `.internal` | — | Built-in 429 / 503 / DatabaseError / 500 |
145
148
  | `fx.auth.createApiKey({ name, scopes, expiresIn?, ipAllowlist?, rateLimit? })` | `write` `auth:api-keys` | Secret once. Creator is live `userId` / `scopes`. Session only. `ipAllowlist` is IPs or hostnames. |
146
149
  | `fx.auth.listApiKeys()` | `read` `auth:api-keys` | Keys this session minted |
147
150
  | `fx.auth.revokeApiKey(id)` | `write` `auth:api-keys` | Owner only |
@@ -155,6 +158,8 @@ on(
155
158
  `fx.call` starts the callee with an **empty** `fx.auth` (fail-closed for authorization) and
156
159
  propagates `fx.tenant.id`. For audit/attribution only, read `fx.principal` — it propagates the
157
160
  originating identity without copying into `fx.auth`. Gates never consult `fx.principal`.
161
+ Controlled `return fx.fail(...)` returns a `FlowFailure` value; an unhandled `throw` rethrows
162
+ (never silent `undefined`). The same rules apply to `app.call`.
158
163
 
159
164
  ## Concurrency and retry
160
165
 
@@ -211,7 +216,8 @@ const rows = await fx.using(
211
216
  | `fx.send(template, { to?, data?, via?, … })` | `send` | `via` orders fallback; `locale` / `profileLocale` / `acceptLanguage` feed the locale chain |
212
217
 
213
218
  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)).
219
+ contact a provider. Bodies live on `.template({ catalog })` (`{{field}}`, not ICU — see
220
+ [Channel](/docs/elements/channel)).
215
221
 
216
222
  ## Outbound HTTP
217
223
 
@@ -255,13 +261,13 @@ host tooltip).
255
261
 
256
262
  ## Clock
257
263
 
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 |
264
+ | Signature | Notes |
265
+ | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
266
+ | `fx.clock.now()` | Epoch-ms, injectable — the only legal "now"; pass into SQL timestamps as-is (store coerces numbers and ISO strings to `Date`) |
267
+ | `fx.clock.ago(duration)` | Instant before now (`"30d"` → now − 30 days) |
268
+ | `fx.clock.fromNow(duration)` | Instant after now (`"14d"` → now + 14 days) |
269
+ | `fx.clock.duration(duration)` | Span in ms — offset a stored instant (`createdAt + duration("7d")`) |
270
+ | `fx.clock.sleep(label, duration)` | Durable sleep in `durable` flows; immediate otherwise |
265
271
 
266
272
  Durations: `"200ms"` · `"30s"` · `"2m"` · `"1h"` · `"7d"`. A `"d"` is 86_400_000 ms, not a calendar day. Unknown strings parse as `0`.
267
273
 
@@ -293,6 +299,8 @@ keys. Use `cache: false` to opt out, or `cache: "30s"` for a TTL.
293
299
 
294
300
  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
301
 
302
+ 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.
303
+
296
304
  ## Logging, i18n, ids
297
305
 
298
306
  | Signature | Notes |
@@ -355,6 +363,11 @@ to the key — see [Gate](/docs/elements/gate#api-keys).
355
363
  Type the parameter as `Fx` from `okengine`, not a hand-rolled structural type.
356
364
  </Accordion>
357
365
 
366
+ <Accordion title="I mapped Date columns to ISO by hand">
367
+ Declared `out` projects success values. `fx.json.create(row)`, `return row`, and
368
+ `fx.json.withQuery(rows, input)` are enough — extra columns strip. A miss is not a client error.
369
+ </Accordion>
370
+
358
371
  </Accordions>
359
372
 
360
373
  ## 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>
@@ -248,12 +248,12 @@ The Elements section walks through each element in depth — this is just enough
248
248
 
249
249
  ## Where this goes next
250
250
 
251
- You now hold the whole model: the drift, the rule, the eight elements, the five-piece anatomy. Don't read more — run it. From an empty folder to this exact signup Flow answering a real request, in one sitting:
251
+ You now hold the whole model: the drift, the rule, the eight elements, the five-piece anatomy. Don't read more — run it. From an empty folder to `GET` and `POST /users` answering a real request:
252
252
 
253
253
  <Cards>
254
254
  <Card
255
255
  title="Try It"
256
- description="From an empty folder to a Flow running in the Console — one sitting, minimal detour."
256
+ description="Scaffold, see the files, add GET and POST /users, save a row — then email if you want."
257
257
  href="/docs/understand/try-it"
258
258
  />
259
259
  <Card