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.
- package/AGENTS.md +1 -1
- package/package.json +2 -1
- package/site/content/docs/client/auth.mdx +1 -2
- package/site/content/docs/client/calling.mdx +125 -37
- package/site/content/docs/client/index.mdx +7 -7
- 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 +42 -23
- package/site/content/docs/elements/flow/index.mdx +41 -30
- 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/gate/tenancy.mdx +1 -1
- package/site/content/docs/elements/store/files.mdx +11 -5
- package/site/content/docs/elements/store/index.mdx +13 -6
- package/site/content/docs/elements/store/kv.mdx +8 -2
- package/site/content/docs/elements/store/search.mdx +5 -5
- package/site/content/docs/elements/store/sql.mdx +100 -39
- package/site/content/docs/elements/vault/index.mdx +14 -13
- package/site/content/docs/elements/vault/secrets.mdx +6 -3
- package/site/content/docs/index.mdx +1 -1
- 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/recipes/rustfs.mdx +1 -1
- package/site/content/docs/reference/cli.mdx +7 -4
- package/site/content/docs/reference/configuration.mdx +8 -8
- package/site/content/docs/reference/errors.mdx +229 -55
- package/site/content/docs/reference/fx.mdx +22 -9
- package/site/content/docs/reference/i18n.mdx +4 -4
- package/site/content/docs/reference/plugins.mdx +4 -4
- package/site/content/docs/understand/the-architecture.mdx +2 -2
- package/site/content/docs/understand/try-it.mdx +759 -24
- package/src/cli/ai-setup/ai-setup.test.ts +40 -0
- package/src/cli/ai-setup/apply.ts +28 -46
- 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 +2 -1
- 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 +13 -11
- package/src/cli/ensure-drizzle-config.ts +4 -3
- package/src/cli/start.ts +2 -1
- package/src/client/create.ts +3 -3
- package/src/client/explain.test.ts +252 -0
- package/src/client/explain.ts +272 -0
- package/src/client/live.test.ts +44 -0
- package/src/client/notes-contract.test.ts +10 -0
- package/src/client/sse.ts +6 -2
- package/src/client/transport.test.ts +67 -0
- package/src/client/transport.ts +24 -40
- package/src/client/types.ts +17 -6
- package/src/client-react/browser.test.ts +23 -0
- package/src/client-react/live-resource.ts +6 -2
- 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/compiler/response.ts +17 -27
- package/src/config/index.ts +6 -4
- package/src/console/server/invoke-user-flow.test.ts +8 -2
- package/src/console/server/invoke-user-flow.ts +12 -18
- package/src/console/server/security.gate.test.ts +1 -1
- package/src/console/ui-next/dist/assets/{access-page-BoC83Ubl.js → access-page-tIbsiphz.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-CKjAEOqA.js → agent-disclosure-CSKumwS2.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-Ceaq9pYh.js → cache-glyph-BCC-DxKT.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-C4lmY7ck.js → call-pii-button-jOmYISdc.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-82y257sL.js → collapsible-DC2xNaAb.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-W63jaMZ8.js → duration-tone-CwoV56jn.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-KFTFj2rK.js → flows-page-gT1lsWQK.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-yE9zNTqC.js → highlighted-json-C2GZEJNI.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-CwFeFroN.js → http-method-BdYjcIrD.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-r7xXt_VV.js → index-Cul17AcV.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-BDiliMiC.js → observability-page-oY9vdYBk.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-DYDzWUFT.js → replica-lag-C4QdAF7J.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-CzrOfgiz.js → request-meta-C43DHyld.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CZC2cwaw.js → store-page-CL0D9dOq.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-DgeeejW7.js → trace-detail-sheet-Dcpo4Us_.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-CzGIyOPY.js → tree-expand-toggle-CKJTuv43.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-DBiDCLIB.js → units-page-1PlT19ft.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-CcHsthPe.js → vault-page-BwZ9YjTW.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/docker/docker.test.ts +3 -3
- package/src/docker/images-config.test.ts +4 -4
- package/src/docker/stack-id.test.ts +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/files-errors.test.ts +149 -0
- package/src/elements/store/files-errors.ts +189 -0
- package/src/elements/store/kv-errors.test.ts +98 -0
- package/src/elements/store/kv-errors.ts +139 -0
- package/src/elements/store/load-plugin-tables.ts +1 -1
- package/src/elements/store/prepare-row.test.ts +57 -4
- package/src/elements/store/resource.ts +11 -7
- package/src/elements/store/runtime.ts +18 -13
- package/src/elements/store/schema-decl.test.ts +178 -0
- package/src/elements/store/sql-errors.test.ts +197 -0
- package/src/elements/store/sql-errors.ts +294 -0
- package/src/elements/store/sql-session.test.ts +52 -0
- package/src/elements/store/sql-session.ts +50 -2
- package/src/elements/store/store-errors.ts +47 -0
- package/src/elements/store/table.ts +8 -6
- package/src/http.ts +9 -1
- package/src/i18n/catalogs/ar.ts +18 -0
- package/src/i18n/catalogs/en.ts +18 -0
- package/src/index.ts +9 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/app.ts +40 -34
- 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/builtin-errors.test.ts +117 -0
- package/src/kernel/builtin-errors.ts +129 -0
- package/src/kernel/call.test.ts +182 -0
- package/src/kernel/errors-vault.ts +16 -0
- package/src/kernel/errors.registry.test.ts +7 -0
- package/src/kernel/errors.ts +97 -24
- package/src/kernel/fail-helpers.ts +34 -0
- package/src/kernel/flow-units.ts +3 -3
- package/src/kernel/fx.test.ts +8 -0
- package/src/kernel/fx.ts +24 -8
- package/src/kernel/index.ts +12 -1
- 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
- package/src/runtime/dev-request-log.ts +29 -11
- package/src/term.test.ts +76 -0
- package/src/term.ts +166 -3
|
@@ -318,13 +318,12 @@ import { uploads } from "@/core";
|
|
|
318
318
|
export const get = on(
|
|
319
319
|
http.get({
|
|
320
320
|
in: z.object({ id: z.string(), name: z.string() }),
|
|
321
|
-
errors: { NotFound: z.object({ key: z.string() }) },
|
|
322
321
|
}),
|
|
323
322
|
flow({
|
|
324
323
|
do: async ({ id, name }, fx) => {
|
|
325
324
|
const key = `notes/${id}/${name}`;
|
|
326
325
|
const bytes = await fx.store(uploads).get(key);
|
|
327
|
-
if (!bytes) return fx.fail(
|
|
326
|
+
if (!bytes) return fx.fail.notFound({ key });
|
|
328
327
|
return { key, size: bytes.byteLength };
|
|
329
328
|
},
|
|
330
329
|
}),
|
|
@@ -582,7 +581,7 @@ export default defineConfig({
|
|
|
582
581
|
},
|
|
583
582
|
images: {
|
|
584
583
|
store: {
|
|
585
|
-
files: "rustfs/rustfs:1.0.0
|
|
584
|
+
files: "rustfs/rustfs:1.0.0",
|
|
586
585
|
},
|
|
587
586
|
},
|
|
588
587
|
});
|
|
@@ -607,8 +606,14 @@ For `s3`, Compose / env typically supply `S3_ENDPOINT`, `S3_ACCESS_KEY_ID`,
|
|
|
607
606
|
</Accordion>
|
|
608
607
|
|
|
609
608
|
<Accordion title="Invalid object key">
|
|
610
|
-
|
|
611
|
-
Use relative, non-escaping keys.
|
|
609
|
+
The `fs` driver rejects keys that start with `/` or contain `..`. HTTP is `Forbidden` **403**
|
|
610
|
+
(`reason: invalid_key`) — the key is not copied. Use relative, non-escaping keys.
|
|
611
|
+
</Accordion>
|
|
612
|
+
|
|
613
|
+
<Accordion title="S3 AccessDenied / disk full is InternalError">
|
|
614
|
+
`AccessDenied` / `EACCES` map to `Forbidden` **403**. Missing `get` stays `null`. `SlowDown` stays
|
|
615
|
+
thrown until `flow.retry` exhausts, then **503**. `NoSuchBucket` / `ENOSPC` are **503** — see
|
|
616
|
+
[Errors · Files auto-map](/docs/reference/errors#files-auto-map).
|
|
612
617
|
</Accordion>
|
|
613
618
|
|
|
614
619
|
<Accordion title="ERR_IMAGE_TOO_MANY_PIXELS">
|
|
@@ -644,6 +649,7 @@ For `s3`, Compose / env typically supply `S3_ENDPOINT`, `S3_ACCESS_KEY_ID`,
|
|
|
644
649
|
- [HTTP](/docs/elements/flow/http) — routes that accept uploads
|
|
645
650
|
- [Vault](/docs/elements/vault) — credentials for S3 when you configure bindings
|
|
646
651
|
- [fx](/docs/reference/fx) — `fx.store(decl)`
|
|
652
|
+
- [Errors](/docs/reference/errors) — Files auto-map
|
|
647
653
|
- [Configuration](/docs/reference/configuration) — `drivers.store.files`
|
|
648
654
|
|
|
649
655
|
## Next
|
|
@@ -24,7 +24,7 @@ For developers persisting domain data on okengine — one handle shape, drivers
|
|
|
24
24
|
<Step>
|
|
25
25
|
### Declare a SQL store
|
|
26
26
|
|
|
27
|
-
```typescript title="src/db/schema.
|
|
27
|
+
```typescript title="src/db/schema.ts"
|
|
28
28
|
import { store, field } from "okengine";
|
|
29
29
|
|
|
30
30
|
export const notes = store.schema.table("notes", {
|
|
@@ -49,17 +49,20 @@ import { db, notes } from "@/schema";
|
|
|
49
49
|
export const create = on(
|
|
50
50
|
http.post({
|
|
51
51
|
in: z.object({ title: z.string().min(1) }),
|
|
52
|
+
out: z.object({ id: z.string(), title: z.string() }),
|
|
52
53
|
}),
|
|
53
54
|
flow({
|
|
54
55
|
do: async ({ title }, fx) => {
|
|
55
56
|
const id = fx.id();
|
|
56
|
-
await fx.store(db).insert(notes).values({ id, title });
|
|
57
|
-
return fx.json.create(
|
|
57
|
+
const [row] = await fx.store(db).insert(notes).values({ id, title }).returning();
|
|
58
|
+
return fx.json.create(row);
|
|
58
59
|
},
|
|
59
60
|
}),
|
|
60
61
|
);
|
|
61
62
|
```
|
|
62
63
|
|
|
64
|
+
`out` projects the insert row — `createdAt` strips; Date timestamps become ISO-8601.
|
|
65
|
+
|
|
63
66
|
</Step>
|
|
64
67
|
|
|
65
68
|
<Step>
|
|
@@ -84,6 +87,11 @@ Response:
|
|
|
84
87
|
|
|
85
88
|
</Steps>
|
|
86
89
|
|
|
90
|
+
<Callout title="Two declare layouts">
|
|
91
|
+
One file (`schema.ts`) or a folder (`schema/`, one table per file). Both import as `@/db/schema`.
|
|
92
|
+
Pick one — see [SQL](/docs/elements/store/sql#declaring-stores).
|
|
93
|
+
</Callout>
|
|
94
|
+
|
|
87
95
|
<Callout title="Effects are inferred">
|
|
88
96
|
Every `fx.store` touch is recorded on the Flow as `reads` / `writes` (`sql:app`, `kv:sessions`,
|
|
89
97
|
`files:uploads`, …). That powers the Manifest, Console, cache invalidation, and least privilege —
|
|
@@ -109,12 +117,11 @@ import { db, notes } from "@/schema";
|
|
|
109
117
|
export const get = on(
|
|
110
118
|
http.get({
|
|
111
119
|
in: z.object({ id: z.string() }),
|
|
112
|
-
errors: { NotFound: z.object({ id: z.string() }) },
|
|
113
120
|
}),
|
|
114
121
|
flow({
|
|
115
122
|
do: async ({ id }, fx) => {
|
|
116
123
|
const [note] = await fx.store(db).select().from(notes).where(eq(notes.id, id));
|
|
117
|
-
if (!note) return fx.fail(
|
|
124
|
+
if (!note) return fx.fail.notFound({ id });
|
|
118
125
|
return note;
|
|
119
126
|
},
|
|
120
127
|
}),
|
|
@@ -220,7 +227,7 @@ export default defineConfig({
|
|
|
220
227
|
store: {
|
|
221
228
|
sql: "postgres:18-alpine",
|
|
222
229
|
kv: "redis:8-alpine",
|
|
223
|
-
files: "rustfs/rustfs:1.0.0
|
|
230
|
+
files: "rustfs/rustfs:1.0.0",
|
|
224
231
|
},
|
|
225
232
|
},
|
|
226
233
|
});
|
|
@@ -262,7 +262,7 @@ export const get = on(
|
|
|
262
262
|
do: async ({ token }, fx) => {
|
|
263
263
|
const userId = await fx.store(sessions).get(`session:${token}`);
|
|
264
264
|
if (userId === undefined || userId === null) {
|
|
265
|
-
return fx.fail(
|
|
265
|
+
return fx.fail.notFound({ token });
|
|
266
266
|
}
|
|
267
267
|
return { userId };
|
|
268
268
|
},
|
|
@@ -599,6 +599,12 @@ Gate rate strategies inherit `drivers.store.kv` (no separate `drivers.gate`).
|
|
|
599
599
|
does). Prefer prefix filters in admin Flows.
|
|
600
600
|
</Accordion>
|
|
601
601
|
|
|
602
|
+
<Accordion title="Redis down returns InternalError">
|
|
603
|
+
Connection / `CLUSTERDOWN` / `READONLY` map to `ServiceUnavailable` **503**. `BUSY` / `TRYAGAIN`
|
|
604
|
+
stay thrown until `flow.retry` exhausts, then **503**. `WRONGTYPE` stays `InternalError` — see
|
|
605
|
+
[Errors · KV auto-map](/docs/reference/errors#kv-auto-map).
|
|
606
|
+
</Accordion>
|
|
607
|
+
|
|
602
608
|
<Accordion title="I expected incr / setNx / fx.store.kv">
|
|
603
609
|
Those helpers are not on the public handle. Use `get` / `set` / `delete` / `list` / `ttlMs` via
|
|
604
610
|
`fx.store(decl)`. Gate rates use Redis Lua internally.
|
|
@@ -614,7 +620,7 @@ Gate rate strategies inherit `drivers.store.kv` (no separate `drivers.gate`).
|
|
|
614
620
|
- [Gate](/docs/elements/gate) — rate buckets use Redis internally
|
|
615
621
|
- [fx](/docs/reference/fx) — `fx.store(decl)`
|
|
616
622
|
- [Configuration](/docs/reference/configuration) — `drivers.store.kv`
|
|
617
|
-
- [Errors](/docs/reference/errors) — OKE1810
|
|
623
|
+
- [Errors](/docs/reference/errors) — OKE1810 · KV auto-map
|
|
618
624
|
|
|
619
625
|
## Next
|
|
620
626
|
|
|
@@ -24,7 +24,7 @@ For developers ranking rows on okengine — mark columns, query with `fx.store(d
|
|
|
24
24
|
<Step>
|
|
25
25
|
### Mark columns and bind a route
|
|
26
26
|
|
|
27
|
-
```typescript title="src/db/schema.
|
|
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
|
-
|
|
245
|
-
|
|
244
|
+
<Tabs items={["Single file", "Folder"]}>
|
|
245
|
+
|
|
246
|
+
<Tab value="Single file">
|
|
247
|
+
|
|
248
|
+
Blank starter. Add `store.schema.table` exports in one module. When it grows,
|
|
249
|
+
split into the folder and **delete** `schema.ts`.
|
|
250
|
+
|
|
251
|
+
```typescript title="src/db/schema.ts"
|
|
252
|
+
import { store, field } from "okengine";
|
|
246
253
|
|
|
247
|
-
|
|
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
|
|
|
@@ -294,19 +344,19 @@ export const get = on(
|
|
|
294
344
|
http.get({
|
|
295
345
|
in: z.object({ id: z.string() }),
|
|
296
346
|
out: z.object({ id: z.string(), title: z.string() }),
|
|
297
|
-
errors: { NotFound: z.object({ id: z.string() }) },
|
|
298
347
|
}),
|
|
299
348
|
flow({
|
|
300
349
|
do: async ({ id }, fx) => {
|
|
301
350
|
const [note] = await fx.store(db).select().from(notes).where(eq(notes.id, id));
|
|
302
|
-
if (!note) return fx.fail(
|
|
351
|
+
if (!note) return fx.fail.notFound({ id });
|
|
303
352
|
return note;
|
|
304
353
|
},
|
|
305
354
|
}),
|
|
306
355
|
);
|
|
307
356
|
```
|
|
308
357
|
|
|
309
|
-
`findById(notes, id)` is the same PK path without a fluent chain.
|
|
358
|
+
`findById(notes, id)` is the same PK path without a fluent chain. Declared `out` projects the
|
|
359
|
+
row — return it as-is.
|
|
310
360
|
|
|
311
361
|
</Tab>
|
|
312
362
|
|
|
@@ -332,6 +382,8 @@ export const create = on(
|
|
|
332
382
|
);
|
|
333
383
|
```
|
|
334
384
|
|
|
385
|
+
`out` projects the returned row — Date timestamps become ISO-8601; extra columns strip.
|
|
386
|
+
|
|
335
387
|
</Tab>
|
|
336
388
|
|
|
337
389
|
<Tab value="Update">
|
|
@@ -351,7 +403,6 @@ export const update = on(
|
|
|
351
403
|
id: z.string(),
|
|
352
404
|
title: z.string().min(1).optional(),
|
|
353
405
|
}),
|
|
354
|
-
errors: { NotFound: z.object({ id: z.string() }) },
|
|
355
406
|
}),
|
|
356
407
|
flow({
|
|
357
408
|
do: async ({ id, title }, fx) => {
|
|
@@ -359,7 +410,7 @@ export const update = on(
|
|
|
359
410
|
await fx.store(db).update(notes).set({ title }).where(eq(notes.id, id));
|
|
360
411
|
}
|
|
361
412
|
const row = await fx.store(db).findById(notes, id);
|
|
362
|
-
if (!row) return fx.fail(
|
|
413
|
+
if (!row) return fx.fail.notFound({ id });
|
|
363
414
|
return row;
|
|
364
415
|
},
|
|
365
416
|
}),
|
|
@@ -538,7 +589,7 @@ The URL id segment is always `:id`. Update is **PATCH**, not PUT.
|
|
|
538
589
|
| `out` | Schema | _(required)_ | Item shape (get / list / update return) |
|
|
539
590
|
| `update` | Schema | `in` | Patch fields. Wire body is `{ id, …patch }` |
|
|
540
591
|
| `idSchema` | Schema | `update`/`in` + `{ id: string }` | Replaces the update Flow `in` when set (include the id key) |
|
|
541
|
-
| `errors` | error map |
|
|
592
|
+
| `errors` | error map | — | Extra domain codes. Built-in `NotFound` is always on |
|
|
542
593
|
| `id` | column | table PK | Column bound to `:id` |
|
|
543
594
|
| `list` | object | see List Options | List query grammar (`GET /notes`) |
|
|
544
595
|
| `breaking` | `boolean` | `false` | Marks the five Flows `breaking: true` (handwritten → resource migration) |
|
|
@@ -671,7 +722,7 @@ Pass extras after the column map:
|
|
|
671
722
|
<Accordion title="Policy helpers">
|
|
672
723
|
Helpers take **string** column / gate / scope names and stamp predicates:
|
|
673
724
|
|
|
674
|
-
```typescript title="src/db/schema.
|
|
725
|
+
```typescript title="src/db/schema.ts"
|
|
675
726
|
export const bookings = store.schema.table(
|
|
676
727
|
"bookings",
|
|
677
728
|
{
|
|
@@ -717,7 +768,7 @@ export const shared = store.schema.table(
|
|
|
717
768
|
`store.schema.relations` records one/many links for Drizzle emit — it does
|
|
718
769
|
**not** add `fx.store(db).query.…`:
|
|
719
770
|
|
|
720
|
-
```typescript title="src/db/schema.
|
|
771
|
+
```typescript title="src/db/schema.ts"
|
|
721
772
|
export const links = store.schema.table("links", {
|
|
722
773
|
id: field.id().primaryKey(),
|
|
723
774
|
code: field.text().notNull().unique(),
|
|
@@ -771,7 +822,7 @@ recorded that identity yet. Categories:
|
|
|
771
822
|
```typescript title="src/db/seed/index.ts"
|
|
772
823
|
import { defineSeed, type Fx } from "okengine";
|
|
773
824
|
import { db } from "@/core";
|
|
774
|
-
import { notes } from "@/db/schema
|
|
825
|
+
import { notes } from "@/db/schema";
|
|
775
826
|
|
|
776
827
|
async function welcomeNote(fx: Fx) {
|
|
777
828
|
await fx.store(db).upsert(
|
|
@@ -864,6 +915,11 @@ export default defineConfig({
|
|
|
864
915
|
`oke db migrate` in that environment.
|
|
865
916
|
</Accordion>
|
|
866
917
|
|
|
918
|
+
<Accordion title="Unique insert is 500 InternalError">
|
|
919
|
+
Unique / exclusion maps to `Conflict` **409**; missing FK to `ForeignKey` **409**. See [Errors ·
|
|
920
|
+
SQL auto-map](/docs/reference/errors#sql-auto-map).
|
|
921
|
+
</Accordion>
|
|
922
|
+
|
|
867
923
|
<Accordion title="update().set().where(): condition required">
|
|
868
924
|
Updates and deletes without a `where` are rejected (`delete().where(): condition required`). Pass
|
|
869
925
|
a Drizzle condition or equality map — or use `delete(table, id)` for PK deletes.
|
|
@@ -898,6 +954,11 @@ export default defineConfig({
|
|
|
898
954
|
columns on `store.resource(…, { list: { … } })` or your `liveQuery` options.
|
|
899
955
|
</Accordion>
|
|
900
956
|
|
|
957
|
+
<Accordion title="@/db/schema hits schema.ts, not my schema/ folder">
|
|
958
|
+
Cause: both `schema.ts` and `schema/` exist — the file wins for the import and for `oke db`
|
|
959
|
+
discovery. Delete `schema.ts` after you split, or set `db.declare` to `schema/index.ts`.
|
|
960
|
+
</Accordion>
|
|
961
|
+
|
|
901
962
|
<Accordion title="fx.store(db).query is undefined">
|
|
902
963
|
There is no Relational Query Builder on the session handle. Use fluent `select` / `insert` /
|
|
903
964
|
`update` / `page`, or `store.resource` for CRUD. `store.schema.relations` is emit metadata only.
|
|
@@ -918,7 +979,7 @@ export default defineConfig({
|
|
|
918
979
|
- [HTTP · Resources](/docs/elements/flow/http#resources) — mount, live SSE, verb table
|
|
919
980
|
- [Consumers · CDC](/docs/elements/flow/consumers#cdc) — `db.table(…).changed()`
|
|
920
981
|
- [fx](/docs/reference/fx) — `fx.store` session
|
|
921
|
-
- [Errors](/docs/reference/errors) — OKE1110 · OKE1041
|
|
982
|
+
- [Errors](/docs/reference/errors) — OKE1110 · OKE1041 · SQL auto-map
|
|
922
983
|
|
|
923
984
|
## Next
|
|
924
985
|
|
|
@@ -196,13 +196,16 @@ Order (Console labels match these source ids):
|
|
|
196
196
|
3. **`.env.local`** — local overrides (gitignored)
|
|
197
197
|
4. **dev-fallback** — `dev:` on the contract (dev boot only)
|
|
198
198
|
|
|
199
|
-
Miss every layer → `VaultBootError` listing **all** gaps in one pass
|
|
200
|
-
`vault.env.required` names):
|
|
199
|
+
Miss every layer → `VaultBootError` (TTY title **OKE1510**) listing **all** gaps in one pass
|
|
200
|
+
(including `vault.env.required` names):
|
|
201
201
|
|
|
202
202
|
```text
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
-
|
|
203
|
+
◇ OKE1510
|
|
204
|
+
│ 2 secrets have no value in any resolution layer.
|
|
205
|
+
│ - STRIPE_KEY
|
|
206
|
+
│ - DATABASE_URL
|
|
207
|
+
│ → Set each name (`oke vault set <name>`, or `.env.local`).
|
|
208
|
+
│ https://oke.omqkhafi.dev/e/1510
|
|
206
209
|
```
|
|
207
210
|
|
|
208
211
|
**Consequence:** fix every listed name before traffic — boot does not take a half-configured app.
|
|
@@ -265,11 +268,9 @@ export default defineConfig({
|
|
|
265
268
|
| `memory` | In-process map | Tests |
|
|
266
269
|
| `managed` | Remote provider bag | AWS / Azure / GCP / Doppler / 1Password |
|
|
267
270
|
|
|
268
|
-
create-oke
|
|
269
|
-
`
|
|
270
|
-
|
|
271
|
-
`vault.fromDocker` fallbacks. Pass `oke({ secrets: NOTES_VAULT })` so configs resolve
|
|
272
|
-
(only `vault.secret` auto-registers).
|
|
271
|
+
create-oke starters pin `vault.dev: "vault"` and declare contracts in `src/vault.ts` (blank) or `src/core/vault.ts` (shorter).
|
|
272
|
+
Pass `oke({ secrets })` so configs resolve (only `vault.secret` auto-registers).
|
|
273
|
+
Values still come from `.env.local`, process env, `oke vault set`, or `dev:` / `vault.fromDocker`.
|
|
273
274
|
|
|
274
275
|
Managed provider ids: `aws-secrets-manager` · `azure-key-vault` · `gcp-secret-manager` ·
|
|
275
276
|
`doppler` · `1password`. Env knobs: [Environment variables](/docs/reference/environment-variables).
|
|
@@ -299,9 +300,9 @@ Managed provider ids: `aws-secrets-manager` · `azure-key-vault` · `gcp-secret-
|
|
|
299
300
|
<Accordions>
|
|
300
301
|
|
|
301
302
|
<Accordion title="VaultBootError — N missing secret(s)">
|
|
302
|
-
|
|
303
|
-
prompts each into `.env.local`; otherwise use `oke vault set` / env /
|
|
304
|
-
fallback. `vault.env.required` joins the same list.
|
|
303
|
+
TTY title **OKE1510**. Cause: N secrets have no value in any resolution layer, with every gap
|
|
304
|
+
listed. On a TTY, `oke dev` prompts each into `.env.local`; otherwise use `oke vault set` / env /
|
|
305
|
+
managed, or a `dev:` fallback. `vault.env.required` joins the same list.
|
|
305
306
|
</Accordion>
|
|
306
307
|
|
|
307
308
|
<Accordion title='vault: secret "…" is not loaded'>
|
|
@@ -209,9 +209,12 @@ capability check.
|
|
|
209
209
|
Missing non-tenant contracts fail boot with every hole listed once:
|
|
210
210
|
|
|
211
211
|
```text
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
-
|
|
212
|
+
◇ OKE1510
|
|
213
|
+
│ 2 secrets have no value in any resolution layer.
|
|
214
|
+
│ - STRIPE_KEY
|
|
215
|
+
│ - APP_WEBHOOK_SECRET
|
|
216
|
+
│ → Set each name (`oke vault set <name>`, or `.env.local`).
|
|
217
|
+
│ https://oke.omqkhafi.dev/e/1510
|
|
215
218
|
```
|
|
216
219
|
|
|
217
220
|
Fill gaps with:
|
|
@@ -29,7 +29,7 @@ Master the mental model before exploring features:
|
|
|
29
29
|
/>
|
|
30
30
|
<Card
|
|
31
31
|
title="Try It"
|
|
32
|
-
description="
|
|
32
|
+
description="Scaffold, see the files, add GET and POST /users, save a row — then email if you want."
|
|
33
33
|
href="/docs/understand/try-it"
|
|
34
34
|
/>
|
|
35
35
|
</Cards>
|
|
@@ -89,8 +89,9 @@ const { data } = await api.auth.verifyMagicLink({ token });
|
|
|
89
89
|
| `auth.requestMagicLink` | `POST /auth/magic-link/request` | `gate.public` + otp rate |
|
|
90
90
|
| `auth.verifyMagicLink` | `POST /auth/magic-link/verify` | `gate.public` + otp rate |
|
|
91
91
|
|
|
92
|
-
**Consequence:** the plugin contributes
|
|
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
|