okengine 0.19.8 → 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.
- package/AGENTS.md +1 -1
- package/package.json +1 -1
- package/site/content/docs/client/auth.mdx +1 -2
- package/site/content/docs/client/calling.mdx +8 -8
- package/site/content/docs/client/index.mdx +4 -4
- package/site/content/docs/client/react.mdx +5 -0
- package/site/content/docs/elements/channel/email.mdx +25 -36
- package/site/content/docs/elements/channel/index.mdx +88 -46
- package/site/content/docs/elements/channel/push.mdx +7 -9
- package/site/content/docs/elements/channel/sms.mdx +6 -4
- package/site/content/docs/elements/channel/whatsapp.mdx +11 -9
- package/site/content/docs/elements/clock/index.mdx +14 -25
- package/site/content/docs/elements/flow/http.mdx +24 -8
- package/site/content/docs/elements/flow/index.mdx +15 -12
- package/site/content/docs/elements/flow/routing.mdx +166 -122
- package/site/content/docs/elements/gate/rls.mdx +2 -2
- package/site/content/docs/elements/store/index.mdx +11 -3
- package/site/content/docs/elements/store/search.mdx +5 -5
- package/site/content/docs/elements/store/sql.mdx +91 -33
- package/site/content/docs/elements/vault/index.mdx +3 -5
- package/site/content/docs/plugins/magic-link.mdx +4 -3
- package/site/content/docs/plugins/otp.mdx +4 -3
- package/site/content/docs/plugins/two-factor.mdx +4 -0
- package/site/content/docs/providers/index.mdx +1 -1
- package/site/content/docs/recipes/index.mdx +1 -1
- package/site/content/docs/reference/cli.mdx +13 -4
- package/site/content/docs/reference/configuration.mdx +7 -7
- package/site/content/docs/reference/errors.mdx +30 -30
- package/site/content/docs/reference/fx.mdx +16 -8
- package/site/content/docs/reference/i18n.mdx +4 -4
- package/site/content/docs/reference/plugins.mdx +4 -4
- package/site/content/docs/understand/try-it.mdx +2 -0
- package/src/cli/ai-setup/ai-setup.test.ts +40 -0
- package/src/cli/ai-setup/apply.ts +28 -46
- package/src/cli/ask-vault-gaps.test.ts +60 -4
- package/src/cli/ask-vault-gaps.ts +59 -3
- package/src/cli/build.test.ts +3 -3
- package/src/cli/build.ts +5 -5
- package/src/cli/db-auto-push.test.ts +11 -0
- package/src/cli/db-auto-push.ts +6 -2
- package/src/cli/db.test.ts +1 -1
- package/src/cli/db.ts +6 -6
- package/src/cli/dev-app-runner.ts +12 -4
- package/src/cli/dev-db-push.test.ts +6 -2
- package/src/cli/dev-schema-sync.ts +1 -1
- package/src/cli/dev.test.ts +10 -7
- package/src/cli/dev.ts +18 -12
- package/src/cli/ensure-drizzle-config.ts +4 -3
- package/src/client-react/browser.test.ts +23 -0
- package/src/client-react/use-live-query.ts +1 -1
- package/src/compiler/flow-path.test.ts +1 -0
- package/src/compiler/flow-path.ts +1 -1
- package/src/compiler/generate-adopt.test.ts +55 -1
- package/src/compiler/generate-adopt.ts +111 -21
- package/src/config/index.ts +6 -4
- package/src/console/ui-next/dist/assets/{access-page-C5yG4aS2.js → access-page-DFLu0wTA.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-D2ToVK86.js → agent-disclosure-DGscxaF5.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-7HUR0kCz.js → cache-glyph-BGmRZk7d.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-D0ky3aXt.js → call-pii-button--feUYxvG.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-BLUHH2dB.js → collapsible-JWvpaiGY.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-BgHEFMtm.js → duration-tone-D9yCJG4n.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-DDybqshQ.js → flows-page-Bs6MD9GB.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-BFiJKYV4.js → highlighted-json-xH8MrEnv.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BVAcB0bI.js → http-method-C4vB6ZIw.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-BID6LSYI.js → index-yTCY4AcS.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CANo_mMK.js → observability-page-BxJ3R6dU.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-CqQGCshp.js → replica-lag-QRKB_IE8.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-DyUDkEt4.js → request-meta-DqZ-fMu5.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CA0rJ_Ow.js → store-page-Dixb6L7a.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-DjH_fMzI.js → trace-detail-sheet-CazhjtiU.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-CWSomIeQ.js → tree-expand-toggle-DlnqYKfr.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-Cz9TIViw.js → units-page-BXTLjU2-.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-Bb1o1O9p.js → vault-page-39KR__bc.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/drivers/clock-postgres.test.ts +10 -2
- package/src/drivers/clock-postgres.ts +18 -2
- package/src/drivers/vault-driver-removal.test.ts +2 -2
- package/src/elements/channel/declare.ts +66 -3
- package/src/elements/channel/runtime.ts +9 -11
- package/src/elements/channel.test.ts +42 -0
- package/src/elements/channel.ts +4 -2
- package/src/elements/clock/reconcile.ts +45 -24
- package/src/elements/clock.test.ts +33 -0
- package/src/elements/store/emit-drizzle.ts +285 -65
- package/src/elements/store/load-plugin-tables.ts +1 -1
- package/src/elements/store/prepare-row.test.ts +57 -4
- package/src/elements/store/schema-decl.test.ts +178 -0
- package/src/elements/store/sql-session.ts +44 -2
- package/src/elements/store/table.ts +8 -6
- package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/app.ts +26 -32
- package/src/kernel/auto-registry.test.ts +26 -1
- package/src/kernel/boot.ts +2 -2
- package/src/kernel/boundary-contract.ts +6 -1
- package/src/kernel/errors.ts +3 -3
- package/src/kernel/flow-units.ts +3 -3
- package/src/kernel/fx.ts +12 -2
- package/src/kernel/mutation-id.ts +8 -0
- package/src/kernel/plugin.ts +4 -3
- package/src/kernel/project-out.test.ts +176 -0
- package/src/kernel/project-out.ts +91 -0
- package/src/kernel/realtime-bind.ts +2 -3
- package/src/kernel/router/linear.ts +12 -6
- package/src/kernel/router.test.ts +13 -0
- package/src/plugins/magic-link.ts +25 -24
- package/src/plugins/otp.ts +35 -24
- package/src/plugins/two-factor.ts +15 -0
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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="
|
|
85
|
-
|
|
86
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
export const db = store.sql("app", { schema: { posts, notes } });
|
|
245
|
-
```
|
|
244
|
+
<Tabs items={["Single file", "Folder"]}>
|
|
246
245
|
|
|
247
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
269
|
-
`
|
|
270
|
-
|
|
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
|
|
93
|
-
|
|
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
|
|
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
|
|
152
|
-
`{{otp}}`). SMS
|
|
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) —
|
|
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
|
|
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
|
|
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
|
|
28
|
-
cd
|
|
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
|
|
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>` | `
|
|
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>
|
|
@@ -185,6 +188,12 @@ oke db search-backfill notes --batch 500
|
|
|
185
188
|
okengine`.
|
|
186
189
|
</Accordion>
|
|
187
190
|
|
|
191
|
+
<Accordion title="Cannot find package zod from a global okengine (Windows)">
|
|
192
|
+
`oke` resolved to `%USERPROFILE%\.bun\install\global\node_modules\okengine`. From a framework
|
|
193
|
+
checkout run `bun install` then `bun run dev:keel` — not a global `oke`. Keel links with a Windows
|
|
194
|
+
junction; a plain symlink needs Developer Mode and fails with `EPERM`.
|
|
195
|
+
</Accordion>
|
|
196
|
+
|
|
188
197
|
<Accordion title="Ctrl+C emptied the new create-oke folder (Windows)">
|
|
189
198
|
Interrupting install or `bun run dev` used to delete a folder create-oke had just created. On
|
|
190
199
|
Windows that wipe often failed mid-tree (`EBUSY`) and left an empty or half-deleted project.
|
|
@@ -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
|
|
216
|
-
| ----------- |
|
|
217
|
-
| `autoPush` | `true`
|
|
218
|
-
| `config` | `"drizzle.config.ts"`
|
|
219
|
-
| `declare` | `
|
|
220
|
-
| `generated` | `
|
|
221
|
-
| `entry` | `src/app.ts`
|
|
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 `
|
|
94
|
-
| `1040` | HTTP path unresolved | Pathless `http.get()` never received a file-tree stamp | Import `@/flows
|
|
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
|
|
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.
|
|
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`
|
|
261
|
-
|
|
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`
|
|
284
|
-
template
|
|
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">
|