okengine 0.17.2 → 0.18.3
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/README.md +9 -5
- package/manifest.v1.schema.json +61 -2
- package/package.json +18 -4
- package/site/content/docs/elements/clock.mdx +1 -1
- package/site/content/docs/elements/flow.mdx +25 -1
- package/site/content/docs/elements/store.mdx +287 -341
- package/site/content/docs/elements/vault.mdx +5 -5
- package/site/content/docs/get-started/installation.mdx +1 -2
- package/site/content/docs/get-started/introduction.mdx +54 -110
- package/site/content/docs/get-started/meta.json +9 -1
- package/site/content/docs/get-started/testing.mdx +328 -0
- package/site/content/docs/get-started/why.mdx +94 -70
- package/site/content/docs/index.mdx +1 -1
- package/site/content/docs/plugins/apple.mdx +151 -0
- package/site/content/docs/plugins/discord.mdx +139 -0
- package/site/content/docs/plugins/facebook.mdx +134 -0
- package/site/content/docs/plugins/figma.mdx +138 -0
- package/site/content/docs/plugins/github.mdx +138 -0
- package/site/content/docs/plugins/google.mdx +153 -0
- package/site/content/docs/plugins/index.mdx +47 -1
- package/site/content/docs/plugins/meta.json +10 -0
- package/site/content/docs/plugins/microsoft.mdx +151 -0
- package/site/content/docs/plugins/oauth.mdx +188 -0
- package/site/content/docs/plugins/x.mdx +125 -0
- package/site/content/docs/reference/cli.md +3 -2
- package/site/content/docs/reference/client.mdx +58 -1
- package/site/content/docs/reference/configuration.mdx +2 -4
- package/site/content/docs/reference/fx.mdx +3 -1
- package/site/content/docs/reference/index.mdx +0 -5
- package/site/content/docs/reference/meta.json +2 -2
- package/site/content/docs/reference/okid.mdx +137 -0
- package/src/auth/bindings.ts +1 -1
- package/src/auth/config.ts +9 -0
- package/src/auth/identity-sql.ts +314 -0
- package/src/auth/identity.ts +140 -2
- package/src/auth/index.ts +17 -1
- package/src/auth/method-context.ts +3 -0
- package/src/auth/oauth-as/cimd.ts +132 -0
- package/src/auth/oauth-as/crypto.test.ts +101 -0
- package/src/auth/oauth-as/crypto.ts +393 -0
- package/src/auth/oauth-as/errors.ts +68 -0
- package/src/auth/oauth-as/http.test.ts +419 -0
- package/src/auth/oauth-as/http.ts +842 -0
- package/src/auth/oauth-as/stores.ts +61 -0
- package/src/auth/oauth-as/tables.ts +142 -0
- package/src/auth/tables.ts +0 -11
- package/src/bench/README.md +83 -0
- package/src/bench/REPORT.md +176 -0
- package/src/bench/g01-rls-stamp.bench.ts +194 -0
- package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
- package/src/bench/g03-signal-once.bench.ts +157 -0
- package/src/bench/g03-signal-reconnect.bench.ts +254 -0
- package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
- package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
- package/src/bench/g05-sustained-full.bench.ts +265 -0
- package/src/bench/g06-mixed-load.bench.ts +260 -0
- package/src/bench/g07-vault-crypto.bench.ts +100 -0
- package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
- package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
- package/src/bench/g08-store-kv-durable.bench.ts +133 -0
- package/src/bench/g08-store-sql.bench.ts +178 -0
- package/src/bench/g09-journal-sustained.bench.ts +203 -0
- package/src/bench/g10-observability-contention.bench.ts +246 -0
- package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
- package/src/bench/g13-elements.bench.ts +427 -0
- package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
- package/src/bench/g15-postgres-degradation.bench.ts +264 -0
- package/src/bench/g16-live-query-fanout.bench.ts +206 -0
- package/src/bench/lib/event-loop-lag.ts +26 -0
- package/src/bench/lib/infra.ts +60 -0
- package/src/bench/lib/report.ts +52 -0
- package/src/bench/lib/rss-sampler.ts +61 -0
- package/src/bench/lib/signal-pg.ts +88 -0
- package/src/bench/load-app.ts +337 -0
- package/src/bench/load-child.ts +108 -0
- package/src/bench/smoke.bench.ts +43 -0
- package/src/cli/competitor-mention-removal.test.ts +28 -0
- package/src/cli/doctor-fd.ts +117 -0
- package/src/cli/doctor.test.ts +192 -0
- package/src/cli/doctor.ts +129 -1
- package/src/client/create.ts +95 -1
- package/src/client/index.ts +9 -2
- package/src/client/transport.ts +11 -4
- package/src/client/use-live-query.ts +154 -0
- package/src/client-react/index.ts +15 -1
- package/src/client-react/live-resource.ts +246 -0
- package/src/client-react/use-live-query.test.ts +475 -0
- package/src/client-react/use-live-query.ts +530 -0
- package/src/compiler/extract.test.ts +518 -0
- package/src/compiler/extract.ts +386 -19
- package/src/console/server/invoke-user-flow.ts +2 -1
- package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-DY4N6nnk.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-CsPDMrVM.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-CcTDXHuz.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-CKR595wP.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
- package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-CpPFFKyE.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-BsMf-9_W.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
- package/src/drivers/cdc-outbox.ts +389 -0
- package/src/drivers/memory.ts +20 -0
- package/src/drivers/oauth-apple.ts +156 -0
- package/src/drivers/oauth-discord.ts +79 -0
- package/src/drivers/oauth-facebook.ts +80 -0
- package/src/drivers/oauth-figma.ts +116 -0
- package/src/drivers/oauth-github.ts +92 -0
- package/src/drivers/oauth-google.ts +142 -0
- package/src/drivers/oauth-microsoft.ts +174 -0
- package/src/drivers/oauth-oidc.ts +293 -0
- package/src/drivers/oauth-shared.ts +326 -0
- package/src/drivers/oauth-types.ts +159 -0
- package/src/drivers/oauth-x.ts +77 -0
- package/src/drivers/oauth2-common.ts +95 -0
- package/src/drivers/oauth2-token.ts +61 -0
- package/src/drivers/pg-rls-row-passes.ts +251 -0
- package/src/drivers/pg-rls.ts +2 -0
- package/src/drivers/postgres.ts +45 -2
- package/src/drivers/signal-postgres.ts +2 -1
- package/src/elements/channel/runtime.ts +29 -2
- package/src/elements/channel.test.ts +52 -0
- package/src/elements/gate/boot.ts +29 -2
- package/src/elements/store/emit-drizzle.ts +147 -14
- package/src/elements/store/field-ddl.test.ts +118 -0
- package/src/elements/store/field-types.test.ts +455 -0
- package/src/elements/store/list-query.golden.json +777 -0
- package/src/elements/store/list-query.parity.test.ts +396 -0
- package/src/elements/store/list-query.ts +792 -0
- package/src/elements/store/live-default.test.ts +136 -0
- package/src/elements/store/live-http.test.ts +160 -0
- package/src/elements/store/live-isolation.test.ts +291 -0
- package/src/elements/store/live-query-runtime.test.ts +323 -0
- package/src/elements/store/live-query-runtime.ts +403 -0
- package/src/elements/store/live-query-server.test.ts +377 -0
- package/src/elements/store/live-query-server.ts +102 -0
- package/src/elements/store/live-query.ts +97 -0
- package/src/elements/store/resource.ts +189 -680
- package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
- package/src/elements/store/schema-decl.ts +539 -41
- package/src/elements/store/sql-rls-stamp.test.ts +27 -0
- package/src/elements/store/sql-session.ts +297 -35
- package/src/elements/store/table.ts +102 -21
- package/src/elements/store.test.ts +3 -1
- package/src/elements/store.ts +12 -1
- package/src/elements/vault/chaos-child.ts +74 -1
- package/src/elements/vault/chaos.test.ts +4 -2
- package/src/elements/vault/storage.ts +4 -2
- package/src/index.ts +4 -1
- package/src/kernel/app-auth.ts +1 -0
- package/src/kernel/app.ts +116 -2
- package/src/kernel/auth-sharing.test.ts +196 -0
- package/src/kernel/boot.test.ts +3 -3
- package/src/kernel/errors.ts +8 -0
- package/src/kernel/fx.test.ts +1 -0
- package/src/kernel/fx.ts +14 -2
- package/src/kernel/horizontal-child.ts +2 -1
- package/src/kernel/http-resource.ts +33 -7
- package/src/kernel/identity-host-persist.test.ts +119 -0
- package/src/kernel/instance-id.ts +4 -2
- package/src/kernel/journal.ts +2 -1
- package/src/kernel/mcp-tool.test.ts +95 -0
- package/src/kernel/on.ts +9 -0
- package/src/kernel/realtime-bind.ts +326 -0
- package/src/kernel/resource-live.ts +117 -0
- package/src/kernel/triggers.ts +86 -4
- package/src/manifest/diff.ts +37 -0
- package/src/manifest/types.ts +64 -2
- package/src/okid.bench.test.ts +64 -0
- package/src/okid.test.ts +338 -0
- package/src/okid.ts +245 -0
- package/src/plugins/anonymous.ts +19 -1
- package/src/plugins/auth/shared.ts +15 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/magic-link.ts +10 -8
- package/src/plugins/mcp-oauth.ts +208 -0
- package/src/plugins/oauth/flow-store.ts +117 -0
- package/src/plugins/oauth/link.ts +69 -0
- package/src/plugins/oauth/shared.ts +108 -0
- package/src/plugins/oauth/token-vault.ts +100 -0
- package/src/plugins/oauth.security.test.ts +535 -0
- package/src/plugins/oauth.ts +532 -0
- package/src/plugins/otp.ts +48 -6
- package/src/plugins/passkey.ts +20 -1
- package/src/plugins/two-factor.ts +11 -0
- package/src/plugins/username.ts +40 -7
- package/src/release/build-lib.ts +7 -1
- package/src/release/measure.ts +1 -0
- package/src/release/official-plugins.ts +4 -1
- package/src/runs/collect.ts +2 -1
- package/src/runs/drivers/files.ts +2 -1
- package/src/test/create-test-app.ts +114 -5
- package/src/test/export-bundle.test.ts +33 -0
- package/src/test/live-signals.test.ts +83 -0
- package/src/test/tenant-isolation.test.ts +175 -0
- package/src/testing.ts +26 -0
- package/src/upgrade/codemods.ts +1 -1
- package/site/content/docs/reference/migrating-environments.mdx +0 -158
|
@@ -5,9 +5,7 @@ icon: "Database"
|
|
|
5
5
|
source: "docs/spec/unified-theory.md"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
Store is one handle, four independent facets — **sql · kv · files · index**. Each has its own
|
|
9
|
-
shape; nothing forces you to use more than one. Flow code stays the same across environments;
|
|
10
|
-
only the driver changes.
|
|
8
|
+
Store is one handle, four independent facets — **sql · kv · files · index**. Each has its own shape; nothing forces you to use more than one. Flow code stays the same across environments; only the driver changes. Every facet accepts an optional `description` — a human title in the Console (falls back to the store name) — and auto-registers when its declaring module is imported before `oke()` (`oke({ stores })` is still valid and additive).
|
|
11
9
|
|
|
12
10
|
<Callout title="The one rule">
|
|
13
11
|
Drivers are named after **protocols**, not vendors (`postgres`, `redis`, `s3` — never `neon` or
|
|
@@ -16,98 +14,7 @@ only the driver changes.
|
|
|
16
14
|
|
|
17
15
|
<StoreFacets />
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
name).
|
|
21
|
-
|
|
22
|
-
`store.sql` / `kv` / `files` / `index` auto-register when the declaring module is imported before
|
|
23
|
-
`oke()`. `oke({ stores })` is still valid and additive.
|
|
24
|
-
|
|
25
|
-
## SQL
|
|
26
|
-
|
|
27
|
-
<StoreFacetMark facet="sql" />
|
|
28
|
-
|
|
29
|
-
The relational facet: tables, schema, and typed single-table sessions through `fx.store`.
|
|
30
|
-
|
|
31
|
-
The Console SQL band opens a query console (`POST /console/store/sql`) for
|
|
32
|
-
DML and DDL. ⌘Enter runs the current statement or selection; ⌘⇧Enter runs
|
|
33
|
-
the whole script sequentially and stops on the first error.
|
|
34
|
-
|
|
35
|
-
EXPLAIN / EXPLAIN ANALYZE wrap the current statement. A script with more
|
|
36
|
-
than one result opens a tab per statement.
|
|
37
|
-
|
|
38
|
-
The table grid can insert a row (`POST /console/store/edit` without `id`).
|
|
39
|
-
Double-click a tab to rename; Save keeps named queries. Completions cover
|
|
40
|
-
Manifest tables, columns, and keywords.
|
|
41
|
-
|
|
42
|
-
Classified PII stays masked unless the toolbar **PII** control is on
|
|
43
|
-
(`revealPii: true`, audited).
|
|
44
|
-
|
|
45
|
-
**Gate** views as Operator (bypass), public, a user, or a policy. SQL,
|
|
46
|
-
browse, and grid edit send `{ asGate, asUserId }`. postgres / pglite stamp
|
|
47
|
-
`oke.gate()` / `oke.user()` / `oke.has_scope()`; memory is catalog-only.
|
|
48
|
-
|
|
49
|
-
The SQL band **Schema** control opens a visualizer of Manifest tables and
|
|
50
|
-
relations — declared `.references()` plus inferred `*_id` columns — not the
|
|
51
|
-
query-console Schema rail.
|
|
52
|
-
|
|
53
|
-
<Callout title="Query performance">
|
|
54
|
-
SQL **Performance** reads `pg_stat_statements` and live locks
|
|
55
|
-
(`QUERY /console/store/sql/stats` and `/locks`).
|
|
56
|
-
|
|
57
|
-
Alpine needs preload plus `CREATE EXTENSION`. Index Advisor is opt-in on
|
|
58
|
-
[Postgres](/docs/recipes/postgres#query-performance).
|
|
59
|
-
|
|
60
|
-
KV **Performance** reads Redis-wire `INFO` / `COMMANDSTATS` / `SLOWLOG`
|
|
61
|
-
(`QUERY /console/store/kv/stats`). In-process `memory` is unsupported.
|
|
62
|
-
|
|
63
|
-
INFO is instance-wide (`StoreKvStatsServerWideGap`). SLOWLOG args collapse
|
|
64
|
-
until reveal (`StoreKvStatsSlowlogArgsGap`). No hot-key table.
|
|
65
|
-
|
|
66
|
-
</Callout>
|
|
67
|
-
|
|
68
|
-
**Colorize** groups tables by prefix and FK family. Click a related table to
|
|
69
|
-
dim the rest; click the canvas to show all again.
|
|
70
|
-
|
|
71
|
-
Edges mark cardinality with icons (many-to-one, one-to-one, many-to-many,
|
|
72
|
-
self). Cards and the query Schema rail mark primary (key), foreign (link),
|
|
73
|
-
and unique (diamond) columns.
|
|
74
|
-
|
|
75
|
-
Each SQL store also lists **Indexes**, **Functions**, **Triggers**,
|
|
76
|
-
**Extensions**, and **RLS Policies**.
|
|
77
|
-
|
|
78
|
-
Indexes, Functions, and Triggers use the same flat form as RLS Policies —
|
|
79
|
-
paired fields, compact templates, and a pinned SQL editor — then run
|
|
80
|
-
`CREATE INDEX` / `CREATE FUNCTION` / `CREATE TRIGGER`.
|
|
81
|
-
|
|
82
|
-
**Advanced** unlocks the leftover Postgres knobs (`CONCURRENTLY` / `INCLUDE`,
|
|
83
|
-
`SECURITY DEFINER` / `PARALLEL`, `CONSTRAINT` / `UPDATE OF`).
|
|
84
|
-
|
|
85
|
-
Extensions list built-in contrib plus a **Library** (featured cards, search,
|
|
86
|
-
category filter) for TimescaleDB, PostGIS, and `pg_cron`.
|
|
87
|
-
|
|
88
|
-
Install reviews the `CREATE EXTENSION` SQL; **Advanced** can set `SCHEMA`,
|
|
89
|
-
`VERSION`, and `CASCADE` (`pg_catalog` is refused).
|
|
90
|
-
|
|
91
|
-
When a newer packaged version exists, the name chip shows **Upgrade**
|
|
92
|
-
(`ALTER EXTENSION … UPDATE`). Names link to the project or Postgres docs.
|
|
93
|
-
|
|
94
|
-
SQL table rows and the resource header show RLS (emerald / muted shield;
|
|
95
|
-
**N RLS** when policies exist) before **PII**. KV, files, and index have
|
|
96
|
-
no badge — enabling RLS on create flips the shield.
|
|
97
|
-
|
|
98
|
-
**RLS Policies** lists `pg_policies` plus Manifest `declared` / `live`.
|
|
99
|
-
Two-finger click a row for its sheet, or empty grid to create. Create
|
|
100
|
-
pins SQL | Code (`store.schema.policy` + `pgPolicy`); drag to resize.
|
|
101
|
-
|
|
102
|
-
**Templates** use `oke.user()` / `oke.gate()` / `oke.has_scope()`.
|
|
103
|
-
Owner cards pick a **Column** (guess `creator_email` / `owner_email`)
|
|
104
|
-
with PK / FK marks. Create stays blocked until a column is picked.
|
|
105
|
-
|
|
106
|
-
Create policy picks a Gate posture (Read / Write / Both), or **Policy &
|
|
107
|
-
scope**. Those picks fill `USING` / `WITH CHECK`. `TO` stays `public` —
|
|
108
|
-
Gate names are not Postgres roles.
|
|
109
|
-
|
|
110
|
-
### Quick start
|
|
17
|
+
## Quick start
|
|
111
18
|
|
|
112
19
|
<Steps>
|
|
113
20
|
|
|
@@ -117,22 +24,22 @@ Gate names are not Postgres roles.
|
|
|
117
24
|
In `src/schema.decl.ts`, describe your tables with plain field builders — no ORM syntax:
|
|
118
25
|
|
|
119
26
|
```typescript title="src/schema.decl.ts"
|
|
120
|
-
import { store, field
|
|
27
|
+
import { store, field } from "okengine";
|
|
121
28
|
|
|
122
29
|
export const notes = store.schema.table("notes", {
|
|
123
|
-
id: field.
|
|
30
|
+
id: field.id().primaryKey(),
|
|
124
31
|
title: field.text().notNull(),
|
|
125
32
|
body: field.text().notNull(),
|
|
126
|
-
createdAt: field.integer().notNull().
|
|
33
|
+
createdAt: field.integer().notNull().now(),
|
|
127
34
|
});
|
|
128
35
|
```
|
|
129
36
|
|
|
130
37
|
</Step>
|
|
131
38
|
|
|
132
39
|
<Step>
|
|
133
|
-
### Declare the store
|
|
40
|
+
### Declare the store and push the schema
|
|
134
41
|
|
|
135
|
-
Bind the schema to a SQL store in `src/core.ts
|
|
42
|
+
Bind the schema to a SQL store in `src/core.ts`. `oke dev` auto-pushes on save — or run it by hand:
|
|
136
43
|
|
|
137
44
|
```typescript title="src/core.ts"
|
|
138
45
|
import { store } from "okengine";
|
|
@@ -141,13 +48,6 @@ import { notes } from "./schema.decl";
|
|
|
141
48
|
export const db = store.sql("notes", { schema: { notes } });
|
|
142
49
|
```
|
|
143
50
|
|
|
144
|
-
</Step>
|
|
145
|
-
|
|
146
|
-
<Step>
|
|
147
|
-
### Push the schema
|
|
148
|
-
|
|
149
|
-
`oke dev` runs this automatically on save; or run it by hand:
|
|
150
|
-
|
|
151
51
|
```bash
|
|
152
52
|
oke db push # dev — applies the schema to your local database
|
|
153
53
|
```
|
|
@@ -181,7 +81,13 @@ export const createNote = on(
|
|
|
181
81
|
|
|
182
82
|
</Steps>
|
|
183
83
|
|
|
184
|
-
|
|
84
|
+
## SQL
|
|
85
|
+
|
|
86
|
+
<StoreFacetMark facet="sql" />
|
|
87
|
+
|
|
88
|
+
The relational facet: tables, schema, and typed single-table sessions through `fx.store`.
|
|
89
|
+
|
|
90
|
+
### Resource routes — `store.resource`
|
|
185
91
|
|
|
186
92
|
Five conventional endpoints (list, create, get, update, remove) expand from one declaration. Each is an ordinary Flow underneath — same contracts, same `fx`:
|
|
187
93
|
|
|
@@ -202,7 +108,7 @@ const notesR = store.resource(db, notes, {
|
|
|
202
108
|
const mounted = on(http.resource("/notes", notesR.all()).public());
|
|
203
109
|
```
|
|
204
110
|
|
|
205
|
-
`.gate(...)` / `.public()` chain like `http.get` — gates on every verb.
|
|
111
|
+
`.gate(...)` / `.public()` chain like `http.get` — gates on every verb. Passing `live: true` also mounts an SSE `GET <path>/live`; see Live queries below.
|
|
206
112
|
|
|
207
113
|
The list endpoint's URL is the whole query language:
|
|
208
114
|
|
|
@@ -217,29 +123,123 @@ The list endpoint's URL is the whole query language:
|
|
|
217
123
|
|
|
218
124
|
Responses follow the Stripe-style envelope: `{ data, meta, error }`. Clients page with `page.next()` or `for await (const page of api.notes.list({ limit: 20 }))` — `meta.next` / `meta.prev` are the next request (`{ cursor }`), not flags. `mode` is diagnostic. `create` answers **201**, `remove` answers **204**, and a missing row is a typed `NotFound` — never a crash.
|
|
219
125
|
|
|
220
|
-
|
|
126
|
+
**Consequence:** `count: "exact"` (the offset default) runs `COUNT(*)` per page. Set `count: "none"` to return only `meta.offset`. Keyset (cursor) paging is the default when `cursor` columns are set — pages stay stable while new rows are inserted, where offset pages would shift and show duplicates.
|
|
127
|
+
|
|
128
|
+
A request that filters on a forbidden column — `?secret=eq.x` — fails with **422** and the exact message `unknown list param "secret"`. Filterable columns are a whitelist, never an accident.
|
|
129
|
+
|
|
130
|
+
### Live queries — `live: true` and `.live(table)`
|
|
131
|
+
|
|
132
|
+
Pass `live: true` to also mount `GET <path>/live` — an SSE feed of per-subscriber classified events (`upsert` / `revoked` / `delete`), gated the same way as the CRUD verbs:
|
|
221
133
|
|
|
222
134
|
```typescript
|
|
223
|
-
|
|
135
|
+
const notesR = store.resource(db, notes, {
|
|
136
|
+
in: NewNote,
|
|
137
|
+
out: Note,
|
|
138
|
+
live: true, // synthesizes GET /notes/live + internal live signal
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const mounted = on(http.resource("/notes", notesR.all()).gate(member));
|
|
224
142
|
```
|
|
225
143
|
|
|
226
|
-
|
|
144
|
+
The live route re-checks RLS for every CDC event under the subscriber's stamped identity. A row that leaves the caller's visibility window arrives as `{ kind: "revoked", id, reason: "rls" | "query" }` — never as a silent stale upsert. `"rls"` means the row no longer passes the subscriber's policies; `"query"` means RLS still allows it but it fell out of the filter or search window. Clients consume it with `useLiveQuery` from `okengine/client-react`.
|
|
227
145
|
|
|
228
|
-
|
|
146
|
+
`store.resource` is optional. A hand-written flow declares the same live surface with the `.live(table)` trigger modifier and the `liveQuery` helper — identical stream, guardrails, and per-subscriber classification:
|
|
229
147
|
|
|
230
148
|
```typescript
|
|
231
|
-
|
|
149
|
+
import { liveQuery } from "okengine";
|
|
150
|
+
|
|
151
|
+
export const live = on(
|
|
152
|
+
http.get("/tasks/live").gate(member).live(tasks),
|
|
153
|
+
flow("tasks.live", {
|
|
154
|
+
in: { unknown: true },
|
|
155
|
+
do: async (input, fx) =>
|
|
156
|
+
liveQuery(fx, tasks, input, {
|
|
157
|
+
search: [tasks.title],
|
|
158
|
+
filter: [tasks.status], // same list grammar as store.resource
|
|
159
|
+
order: "all",
|
|
160
|
+
}),
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
232
163
|
```
|
|
233
164
|
|
|
234
|
-
|
|
165
|
+
`.live(tasks)` stamps the internal `oke/live/sql:tasks` signal at extract and enforces the same guardrails as `live: true` (PK required; `updatedAt` / RLS warnings). The flow body parses the request's query params with the exact list grammar, then opens the classified CDC stream.
|
|
235
166
|
|
|
236
|
-
|
|
167
|
+
- `?status=eq.open` filters the live window the same way it filters `GET /tasks`.
|
|
168
|
+
- A flow that also writes other tables (an activity log alongside the main write) never leaks those tables into the stream: classification is per-table.
|
|
169
|
+
|
|
170
|
+
#### The project-wide default — `oke({ store: { live: true } })`
|
|
171
|
+
|
|
172
|
+
Live remains **explicitly per-table** (`live: true` / `.live(table)`) unless you flip the whole project. Pass `store: { live: true }` to `oke()` to make every **new** `store.schema.table()` declaration live-by-default — the same automatic CDC + RLS-per-event stream `live: true` provides, without writing it on each declaration:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
const app = oke({
|
|
176
|
+
name: "shop",
|
|
177
|
+
store: { live: true }, // new tables are live by default from here on
|
|
178
|
+
});
|
|
179
|
+
```
|
|
237
180
|
|
|
238
181
|
```typescript
|
|
239
|
-
|
|
182
|
+
// No explicit live needed — this table is live by default.
|
|
183
|
+
export const notes = store.schema.table(
|
|
184
|
+
"notes",
|
|
185
|
+
{
|
|
186
|
+
id: field.text().primaryKey(),
|
|
187
|
+
updatedAt: field.integer().notNull(),
|
|
188
|
+
},
|
|
189
|
+
[store.schema.policy.owner("owner")],
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const notesR = store.resource(db, notes, {}); // GET /notes/live synthesized
|
|
193
|
+
const mounted = on(http.resource("/notes", notesR.all()).gate(member));
|
|
240
194
|
```
|
|
241
195
|
|
|
242
|
-
|
|
196
|
+
Three rules keep the flip predictable:
|
|
197
|
+
|
|
198
|
+
- **Default `false`** — today's explicit-only behavior is unchanged. An app that never sets `store.live` pays zero: no signals synthesized, no `/live` routes, no bundle or runtime difference.
|
|
199
|
+
- **Per-table opt-out** — a specific table stays NOT live under the project flag by declaring `store.schema.live(false)` in its extras array (the same third-argument slot as `store.schema.unscoped()` / `rls()` / policies). `store.resource(db, table, { live: false })` is the equivalent opt-out at the resource layer.
|
|
200
|
+
- **Explicit still wins** — `live: true` on a resource or `.live(table)` on a flow opts a specific surface back in even under an opt-out, matching today's behavior.
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
export const archive = store.schema.table(
|
|
204
|
+
"archive",
|
|
205
|
+
{
|
|
206
|
+
id: field.text().primaryKey(),
|
|
207
|
+
},
|
|
208
|
+
[
|
|
209
|
+
store.schema.policy.owner("owner"),
|
|
210
|
+
store.schema.live(false), // exempt from the project-wide default
|
|
211
|
+
],
|
|
212
|
+
);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**This flag changes declaration defaults only — never the runtime cost model.** Live-by-default consumes the exact same per-event RLS stamping as an explicit `live: true`. The guardrails below (PK required; `updatedAt` / RLS warnings) fire identically whether a table became live explicitly or via the default flip — there is no separate, weaker code path. What you are trading against is the fan-out profile of the writes below.
|
|
216
|
+
|
|
217
|
+
#### Live fan-out latency — the real cost model
|
|
218
|
+
|
|
219
|
+
Every live write fans out one **stamped RLS visibility check per subscriber** (per-event, per-connection): p50 fan-out for one CDC event to `S` subscribers grows linearly with `S` (default Postgres pool concurrency ~16–32, `stamp_p50` ≈ 0.5–1 ms):
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
p50_fanout ≈ ceil(S / pool_concurrency) × stamp_p50
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Measured from the benchmark sweep (G8a / G8c):
|
|
226
|
+
|
|
227
|
+
| Active subscribers (same table/query) | Estimated p50 fan-out | UX |
|
|
228
|
+
| ------------------------------------- | --------------------- | --------------------------------------------- |
|
|
229
|
+
| **1–30** | 1–15 ms | Instant |
|
|
230
|
+
| **30–80** | 15–50 ms | Instant to snappy |
|
|
231
|
+
| **80–150** | 50–100 ms | Snappy; power users may notice on hot tables |
|
|
232
|
+
| **150–300** | 100–200 ms | **Perceptible lag** — updates arrive in waves |
|
|
233
|
+
| **300+** | 200 ms+ linear | **Not instant** — capacity planning required |
|
|
234
|
+
|
|
235
|
+
So before you flip `store.live` project-wide on a large application, weigh it against how many concurrent subscribers each hot table is likely to hold. **>150 concurrent live subscribers on the same resource starts to feel like perceptible lag; >300 is the soft ceiling** for the "real-time feel" without an architectural change. Same cost either way — explicit `live: true` or the default flip — this table is the tradeoff you are enabling. (PGlite dev serializes stamping behind a lock; treat fan-out numbers there as correctness-only, never prod latency.)
|
|
236
|
+
|
|
237
|
+
`live: true` carries compile-time guardrails at extract, plus durable delivery and runtime watchtowers:
|
|
238
|
+
|
|
239
|
+
- A table with **no primary key fails extract loud** — upsert/revoked/delete address rows by PK.
|
|
240
|
+
- Missing `updatedAt` / `updated_at` warns (no `rowVersion` LWW guard; races rely on `X-Oke-Mutation-Id` echo dedupe), missing RLS policies warns (every gated subscriber would see every row).
|
|
241
|
+
- CDC events ride the durable `oke_cdc_outbox`; delivered rows prune on retention defaults (24h / 50k) so the outbox never grows unbounded.
|
|
242
|
+
- `oke doctor` findings watch the runtime side: `cdc_outbox_backlog`, `cdc_outbox_retention`, `live_subscriber_pressure` (~150 subs/table in v1), and `live_fanout_queue_saturated`.
|
|
243
243
|
|
|
244
244
|
### Handwritten HTTP lists
|
|
245
245
|
|
|
@@ -267,10 +267,6 @@ Zero-config: `?q=` searches every string field, extra keys auto-eq except path `
|
|
|
267
267
|
shape matches `store.resource` and the typed client (`data` is the page, `meta` is the pager).
|
|
268
268
|
</Callout>
|
|
269
269
|
|
|
270
|
-
**Consequence:** returning `{ items, count }` from `do` nests as `{ data: { items, count }, error: null }` — the pager is no longer top-level `meta`.
|
|
271
|
-
|
|
272
|
-
`fx.json.with({ data, meta })` stays for a page you already built. A third argument locks the surface or switches to keyset (`mode: "cursor"`, `cursor: ["createdAt", "id"]`).
|
|
273
|
-
|
|
274
270
|
### Querying by hand
|
|
275
271
|
|
|
276
272
|
When the list is not conventional CRUD, `fx.store(db)` is the full single-table session:
|
|
@@ -291,8 +287,7 @@ await fx.store(db).update(notes).set({ title: input.title }).where(eq(notes.id,
|
|
|
291
287
|
await fx.store(db).delete(notes).where(lt(notes.createdAt, cutoff));
|
|
292
288
|
```
|
|
293
289
|
|
|
294
|
-
`select().from(notes)` infers the declared row (`string` / `number` / `null` from
|
|
295
|
-
`field.*`). A `select({ title: notes.title })` projection stays untyped.
|
|
290
|
+
`select().from(notes)` infers the declared row (`string` / `number` / `null` from `field.*`). A `select({ title: notes.title })` projection stays untyped.
|
|
296
291
|
|
|
297
292
|
<Callout title="One table per call — no relational with:">
|
|
298
293
|
`fx.store` is deliberately **single-table**: Drizzle's relational `findMany({ with: … })` is not
|
|
@@ -304,46 +299,37 @@ await fx.store(db).delete(notes).where(lt(notes.createdAt, cutoff));
|
|
|
304
299
|
|
|
305
300
|
The recommended path: declare tables ORM-agnostically, then let `oke db` emit real Drizzle (`pgTable` for Postgres / PGLite) into `src/db/schema.drizzle.ts`.
|
|
306
301
|
|
|
307
|
-
| Field API
|
|
308
|
-
|
|
|
309
|
-
| `field.text(
|
|
310
|
-
|
|
|
311
|
-
|
|
|
312
|
-
|
|
|
313
|
-
|
|
|
314
|
-
|
|
|
315
|
-
|
|
|
316
|
-
| `
|
|
317
|
-
| `
|
|
318
|
-
| `
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
Helpers emit stable names (`gate_member_select`). Raw
|
|
339
|
-
`store.schema.policy("name", { as, to, for, using, withCheck })` is the
|
|
340
|
-
escape hatch — predicates use `oke.*`, never `current_setting`.
|
|
341
|
-
|
|
342
|
-
<Callout title="User-plane fx.store applies RLS">
|
|
343
|
-
HTTP / resource flows stamp Gate identity (`SET LOCAL ROLE oke_app` + `set_config`). Operator /
|
|
344
|
-
cron / CDC / signal stay unstamped. Tenancy on: also `oke.tenant()`; tables need `policy.tenant`
|
|
345
|
-
or `unscoped()`.
|
|
346
|
-
</Callout>
|
|
302
|
+
| Field API | Meaning |
|
|
303
|
+
| ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
304
|
+
| `field.text({ enum })` | text; enum narrows to a literal union |
|
|
305
|
+
| `field.id()` | id column with the default generation id (currently OK ID) minted on insert |
|
|
306
|
+
| `field.okid()` | id column explicitly generated with OK ID |
|
|
307
|
+
| `field.varchar({ length })` · `field.char` | bounded strings (same enum support) |
|
|
308
|
+
| `field.boolean()` | boolean |
|
|
309
|
+
| `field.smallint()` · `field.integer()` | ints (abstract integer emits as Drizzle `bigint`) |
|
|
310
|
+
| `field.bigint({ mode })` | int8 — JS `number` (default) / `bigint` / `string` |
|
|
311
|
+
| `field.serial()` · `smallserial` · `bigserial` | auto-increment — NOT NULL by SQL physics |
|
|
312
|
+
| `field.numeric({ precision, scale })` | exact decimals — infers `string`; `{ mode }` opts into `number` / `bigint` |
|
|
313
|
+
| `field.real()` · `doublePrecision()` | floats (~6 / ~15 significant digits) |
|
|
314
|
+
| `field.json<T>()` · `jsonb<T>()` | JSON payloads with a generic type |
|
|
315
|
+
| `field.uuid()` | UUID strings |
|
|
316
|
+
| `field.time()` · `timestamp()` · `date()` | temporals — `Date` by default; `{ mode: "string" }` opts into ISO `string` |
|
|
317
|
+
| `field.interval({ fields })` | time spans |
|
|
318
|
+
| `field.point({ mode })` · `line()` | geometric tuples or objects |
|
|
319
|
+
| `field.bytea()` | binary (`Buffer`) |
|
|
320
|
+
| `field.inet()` · `cidr` · `macaddr8` | network addresses as strings |
|
|
321
|
+
| `.primaryKey()` · `.notNull()` · `.unique()` | constraints |
|
|
322
|
+
| `.default(v)` · `.defaultFn(id \| now \| nowIso \| nowDate)` | defaults — typed against the column's JS type |
|
|
323
|
+
| `.okid()` · `.now()` | prepared defaults — fresh OKID / current instant. `.now()` resolves by column: epoch-ms on numbers, ISO-8601 on string-mode temporals, `Date` on default / `{ mode: "date" }` temporals |
|
|
324
|
+
| `.type<T>()` | override the inferred JS type (mirrors Drizzle `$type<T>`) |
|
|
325
|
+
| `.pii()` · `.sensitive()` · `.retain("30d")` | privacy classification |
|
|
326
|
+
| `.as("sql_name")` | override the automatic `camelCase → snake_case` |
|
|
327
|
+
| `.describe("…")` | human title in the Console (falls back to key) |
|
|
328
|
+
| `.references(() => col, { onDelete })` | foreign key |
|
|
329
|
+
| `store.schema.rls()` | `pgTable.withRLS` when there are no policies |
|
|
330
|
+
| `store.schema.policy.gate/owner/scope/tenant` | happy-path RLS (`oke.gate()` / `oke.user()` / `oke.has_scope()` / `oke.tenant()`) |
|
|
331
|
+
| `store.schema.unscoped()` | Shared table — required when tenancy is on and there is no tenant policy |
|
|
332
|
+
| `store.schema.live(false)` | Per-table opt-out from the project-wide `store.live` default — not live-by-default, no synthesized `/live` surface |
|
|
347
333
|
|
|
348
334
|
#### Foreign keys and relations
|
|
349
335
|
|
|
@@ -379,20 +365,12 @@ export const relations = store.schema.relations({ links, daily }, (r) => ({
|
|
|
379
365
|
| `oke db generate` | Write versioned SQL under `drizzle/` for review |
|
|
380
366
|
| `oke db migrate` | Apply those files — human or CI, **never** at boot |
|
|
381
367
|
|
|
382
|
-
`oke dev` auto-pushes on schema _inputs_ (`schema.decl.ts`, hand-written `schema.ts`,
|
|
383
|
-
`app.ts` plugin tables, `drizzle.config.ts`) — not when emit rewrites `schema.drizzle.ts`.
|
|
384
|
-
A `schema.decl.ts` error prints `schema.decl.ts has an error — …` (red ●) and keeps running.
|
|
368
|
+
`oke dev` auto-pushes on schema _inputs_ (`schema.decl.ts`, hand-written `schema.ts`, `app.ts` plugin tables, `drizzle.config.ts`) — not when emit rewrites `schema.drizzle.ts`. A `schema.decl.ts` error prints `schema.decl.ts has an error — …` (red ●) and keeps running.
|
|
385
369
|
|
|
386
|
-
Opt out with `--no-db-push` or `db: { autoPush: false }`. `prod` **never**
|
|
387
|
-
auto-applies DDL; a missing table fails as **OKE1101** (`oke db migrate`).
|
|
370
|
+
Opt out with `--no-db-push` or `db: { autoPush: false }`. `prod` **never** auto-applies DDL; a missing table fails as **OKE1101** (`oke db migrate`).
|
|
388
371
|
|
|
389
372
|
#### Multiple environments
|
|
390
373
|
|
|
391
|
-
`dev` can `oke db push` freely — safe to wipe and regenerate.
|
|
392
|
-
|
|
393
|
-
Staging/prod accumulate versioned SQL under `drizzle/` (`oke db generate`).
|
|
394
|
-
`oke db migrate` applies only unrecorded files in order — behind DBs catch up.
|
|
395
|
-
|
|
396
374
|
| Environment | Sync |
|
|
397
375
|
| -------------- | -------------------------------------------------------------- |
|
|
398
376
|
| `dev` | `oke db push` (or auto-push from `oke dev`) |
|
|
@@ -404,19 +382,13 @@ Staging/prod accumulate versioned SQL under `drizzle/` (`oke db generate`).
|
|
|
404
382
|
that transaction — a leaked `BEGIN` exhausts checkout.
|
|
405
383
|
</Callout>
|
|
406
384
|
|
|
407
|
-
**Why PgDog (not PgBouncer as the default).** Transaction pooling fixes the
|
|
408
|
-
math; naive poolers can leak session state (`SET`, RLS vars, `LISTEN`/`NOTIFY`)
|
|
409
|
-
across clients. PgDog re-applies those under transaction mode (PgBouncer is fine — not shipped).
|
|
385
|
+
**Why PgDog (not PgBouncer as the default).** Transaction pooling fixes the math; naive poolers can leak session state (`SET`, RLS vars, `LISTEN`/`NOTIFY`) across clients. PgDog re-applies those under transaction mode (PgBouncer is fine — not shipped).
|
|
410
386
|
|
|
411
|
-
**Read replicas later, zero app changes.** Add `role = "replica"` in
|
|
412
|
-
`pgdog.toml` later: `BEGIN READ ONLY` → replica, failover on promotion.
|
|
413
|
-
Not wired this round — readiness only.
|
|
387
|
+
**Read replicas later, zero app changes.** Add `role = "replica"` in `pgdog.toml` later: `BEGIN READ ONLY` → replica, failover on promotion. Not wired this round — readiness only.
|
|
414
388
|
|
|
415
389
|
#### Seeding
|
|
416
390
|
|
|
417
|
-
`oke db seed` loads `defineSeed` from `src/db/seed/index.ts` — a separate
|
|
418
|
-
command, never at boot. Not every project has a seed. In `oke dev`, `s` runs
|
|
419
|
-
the same command. `name` is this app's seed (template or example).
|
|
391
|
+
`oke db seed` loads `defineSeed` from `src/db/seed/index.ts` — a separate command, never at boot. Not every project has a seed. In `oke dev`, `s` runs the same command. `name` is this app's seed (template or example).
|
|
420
392
|
|
|
421
393
|
<StoreSeeding />
|
|
422
394
|
|
|
@@ -483,39 +455,30 @@ export default defineSeed({
|
|
|
483
455
|
| `already-existed` | match found; default leaves it alone |
|
|
484
456
|
| `changed` | match found and `{ onExisting: "update" }` |
|
|
485
457
|
|
|
486
|
-
Default upsert is **insert-if-missing**. Pass `{ onExisting: "update" }` only when this
|
|
487
|
-
call should also rewrite other columns on a match — opt-in per call, never global.
|
|
458
|
+
Default upsert is **insert-if-missing**. Pass `{ onExisting: "update" }` only when this call should also rewrite other columns on a match — opt-in per call, never global.
|
|
488
459
|
|
|
489
460
|
```ts
|
|
490
461
|
// Safe bootstrap — second seed run leaves the row alone
|
|
491
|
-
await fx
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
id: "welcome",
|
|
496
|
-
title: "Welcome",
|
|
497
|
-
|
|
498
|
-
createdAt: 1,
|
|
499
|
-
},
|
|
500
|
-
);
|
|
462
|
+
await fx
|
|
463
|
+
.store(db)
|
|
464
|
+
.upsert(
|
|
465
|
+
notes,
|
|
466
|
+
{ id: "welcome" },
|
|
467
|
+
{ id: "welcome", title: "Welcome", body: "Your Notes API is ready.", createdAt: 1 },
|
|
468
|
+
);
|
|
501
469
|
|
|
502
470
|
// Refresh copy on every seed — match stays; title/body are rewritten
|
|
503
|
-
await fx
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
id: "welcome",
|
|
508
|
-
title: "Welcome (updated)",
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
},
|
|
512
|
-
{ onExisting: "update" },
|
|
513
|
-
);
|
|
471
|
+
await fx
|
|
472
|
+
.store(db)
|
|
473
|
+
.upsert(
|
|
474
|
+
notes,
|
|
475
|
+
{ id: "welcome" },
|
|
476
|
+
{ id: "welcome", title: "Welcome (updated)", body: "New bootstrap copy.", createdAt: 1 },
|
|
477
|
+
{ onExisting: "update" },
|
|
478
|
+
);
|
|
514
479
|
```
|
|
515
480
|
|
|
516
|
-
|
|
517
|
-
is for seed-owned rows you intentionally overwrite. Schema-wide data fixes still belong in
|
|
518
|
-
migrations, not seed.
|
|
481
|
+
Default keeps operator edits safe across re-seeds; `onExisting: "update"` is for seed-owned rows you intentionally overwrite. Schema-wide data fixes still belong in migrations, not seed.
|
|
519
482
|
|
|
520
483
|
| Env | Confirm |
|
|
521
484
|
| -------------- | ----------------------------------------------------- |
|
|
@@ -540,6 +503,34 @@ oke db seed --env prod --force
|
|
|
540
503
|
extending an app-owned table with plugin columns is not supported in v1.
|
|
541
504
|
</Callout>
|
|
542
505
|
|
|
506
|
+
### Access policies — owner, tenant, scope
|
|
507
|
+
|
|
508
|
+
Row-level security is declared per table as the third argument to `store.schema.table` — the policy list is Drizzle-shaped extras:
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
export const bookings = store.schema.table(
|
|
512
|
+
"bookings",
|
|
513
|
+
{
|
|
514
|
+
id: field.text().primaryKey(),
|
|
515
|
+
owner: field.text().notNull(),
|
|
516
|
+
},
|
|
517
|
+
[
|
|
518
|
+
store.schema.policy.gate("member", { for: "select" }),
|
|
519
|
+
store.schema.policy.owner("owner", { for: "all" }),
|
|
520
|
+
store.schema.policy.scope("booking:create", { for: "insert" }),
|
|
521
|
+
store.schema.policy.tenant("tenant_id"),
|
|
522
|
+
],
|
|
523
|
+
);
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Helpers emit stable names (`gate_member_select`). Raw `store.schema.policy("name", { as, to, for, using, withCheck })` is the escape hatch — predicates use `oke.*`, never `current_setting`. `policy.scope` takes a scope name or the `gate.scope("…")` declaration, so you can reuse the same scope in both the gate and the policy instead of repeating the string.
|
|
527
|
+
|
|
528
|
+
<Callout title="User-plane fx.store applies RLS">
|
|
529
|
+
HTTP / resource flows stamp Gate identity (`SET LOCAL ROLE oke_app` + `set_config`). Operator /
|
|
530
|
+
cron / CDC / signal stay unstamped. Tenancy on: also `oke.tenant()`; tables need `policy.tenant`
|
|
531
|
+
or `unscoped()`.
|
|
532
|
+
</Callout>
|
|
533
|
+
|
|
543
534
|
### Per-environment SQL drivers
|
|
544
535
|
|
|
545
536
|
Same flow code, different backends — configured once in `oke.config.ts`:
|
|
@@ -568,14 +559,11 @@ SQL-backed index driver `pgvector` shares this facet's connection — see [Advan
|
|
|
568
559
|
|
|
569
560
|
<StoreFacetMark facet="kv" />
|
|
570
561
|
|
|
571
|
-
The key-value facet: short-lived cache and session data behind `fx.store`. One namespace per
|
|
572
|
-
declaration; drivers swap `memory` ↔ `redis` per environment.
|
|
562
|
+
The key-value facet: short-lived cache and session data behind `fx.store`. One namespace per declaration; drivers swap `memory` ↔ `redis` per environment.
|
|
573
563
|
|
|
574
|
-
The Console KV band opens a query console for `list`, `get`, `set`, `delete`, and `ttl`.
|
|
575
|
-
Prettify rewrites commands to call form and indents `set` JSON.
|
|
564
|
+
The Console KV band opens a query console for `list`, `get`, `set`, `delete`, and `ttl`. Prettify rewrites commands to call form and indents `set` JSON.
|
|
576
565
|
|
|
577
|
-
The same band
|
|
578
|
-
driver. In-process `memory` is unsupported (`KvStatsUnsupported`).
|
|
566
|
+
The same band's **Performance** control opens engine telemetry on the `redis` driver. In-process `memory` is unsupported (`KvStatsUnsupported`).
|
|
579
567
|
|
|
580
568
|
### Quick start
|
|
581
569
|
|
|
@@ -620,68 +608,17 @@ do: async ({ userId, data }, fx) => {
|
|
|
620
608
|
|
|
621
609
|
### TTL
|
|
622
610
|
|
|
623
|
-
Optional third argument on `set` is a duration string: `(\d+)(ms|s|m|h|d)` — e.g. `"30m"`, `"1h"`,
|
|
624
|
-
`"5000ms"`. Same call — opposite physics under each driver. Console KV browse edits that TTL
|
|
625
|
-
(empty clears expiry) and can add a key through the same `set` path.
|
|
611
|
+
Optional third argument on `set` is a duration string: `(\d+)(ms|s|m|h|d)` — e.g. `"30m"`, `"1h"`, `"5000ms"`. Same call — opposite physics under each driver. Console KV browse edits that TTL (empty clears expiry) and can add a key through the same `set` path.
|
|
626
612
|
|
|
627
613
|
<StoreKvTtl />
|
|
628
614
|
|
|
629
|
-
**Consequence:** a TTL you rely on in prod does nothing under the `test` `memory` driver. Exercise
|
|
630
|
-
expiry against `redis` under `oke dev` when lifetime matters.
|
|
631
|
-
|
|
632
|
-
### Examples — what follows from each choice
|
|
633
|
-
|
|
634
|
-
#### A session with expiry
|
|
635
|
-
|
|
636
|
-
```typescript
|
|
637
|
-
await fx.store(sessions).set(`sess:${token}`, { userId }, "7d");
|
|
638
|
-
```
|
|
639
|
-
|
|
640
|
-
**Consequence:** under `redis`, the key vanishes after seven days without a sweeper Flow. Under
|
|
641
|
-
`memory`, it lives until process exit or an explicit `delete`.
|
|
642
|
-
|
|
643
|
-
#### Browse by prefix
|
|
644
|
-
|
|
645
|
-
```typescript
|
|
646
|
-
const keys = await fx.store(sessions).list("sess:");
|
|
647
|
-
```
|
|
648
|
-
|
|
649
|
-
**Consequence:** `list` enumerates a namespace. Prefer stable key prefixes (`user:`, `sess:`) so
|
|
650
|
-
your Flows and operator tools stay readable.
|
|
651
|
-
|
|
652
|
-
### Per-environment KV drivers
|
|
653
|
-
|
|
654
|
-
```typescript title="oke.config.ts"
|
|
655
|
-
drivers: {
|
|
656
|
-
store: {
|
|
657
|
-
kv: { dev: "redis", test: "memory", prod: "redis" },
|
|
658
|
-
},
|
|
659
|
-
},
|
|
660
|
-
images: {
|
|
661
|
-
store: { kv: "redis:8-alpine" },
|
|
662
|
-
// or: "valkey/valkey:8-alpine"
|
|
663
|
-
// or: "docker.dragonflydb.io/dragonflydb/dragonfly"
|
|
664
|
-
},
|
|
665
|
-
```
|
|
666
|
-
|
|
667
|
-
| Facet | `dev` / `prod` | `test` | Runs as |
|
|
668
|
-
| ----- | -------------- | -------- | ---------------------- |
|
|
669
|
-
| `kv` | `redis` | `memory` | container → in-process |
|
|
670
|
-
|
|
671
|
-
| Variable | When |
|
|
672
|
-
| ------------------ | -------------------------------- |
|
|
673
|
-
| `REDIS_URL` | Required for the `redis` driver |
|
|
674
|
-
| `OKE_STORE_KV_URL` | Override if `REDIS_URL` is unset |
|
|
675
|
-
| `OKE_KV_DRIVER` | Force a driver id |
|
|
676
|
-
|
|
677
|
-
Missing Redis URL fails boot loudly: `oke boot: redis driver needs REDIS_URL`.
|
|
615
|
+
**Consequence:** a TTL you rely on in prod does nothing under the `test` `memory` driver. Exercise expiry against `redis` under `oke dev` when lifetime matters.
|
|
678
616
|
|
|
679
617
|
### Durable KV
|
|
680
618
|
|
|
681
619
|
Default `store.kv("sessions")` is cache-shaped — a Redis recreate drops keys.
|
|
682
620
|
|
|
683
|
-
`{ durable: true }` persists that namespace in your SQL database (`oke_kv` JSONB on
|
|
684
|
-
`DATABASE_URL`). Not a per-key Redis flag, not Flow `durable`, not tier-1 auto-cache.
|
|
621
|
+
`{ durable: true }` persists that namespace in your SQL database (`oke_kv` JSONB on `DATABASE_URL`). Not a per-key Redis flag, not Flow `durable`, not tier-1 auto-cache.
|
|
685
622
|
|
|
686
623
|
```typescript
|
|
687
624
|
export const sessions = store.kv("sessions", { description: "Session cache" });
|
|
@@ -689,24 +626,18 @@ export const ledger = store.kv("ledger", { durable: true, description: "Idempote
|
|
|
689
626
|
export const drafts = store.kv("drafts", { durable: true, description: "Compose drafts" });
|
|
690
627
|
```
|
|
691
628
|
|
|
692
|
-
**Consequence:** `oke db seed` into cache Redis looks fine until compose recreates the
|
|
693
|
-
container — Console Store then shows **No rows.** Seeded namespaces need `{ durable: true }`.
|
|
629
|
+
**Consequence:** `oke db seed` into cache Redis looks fine until compose recreates the container — Console Store then shows **No rows.** Seeded namespaces need `{ durable: true }`.
|
|
694
630
|
|
|
695
|
-
With `gate.auth.tenant` on, KV keys are prefixed `{tenantId}:` (logical keys in `do` stay
|
|
696
|
-
unprefixed). Missing `fx.tenant.id` throws **OKE1015**. Opt out with
|
|
697
|
-
`store.kv("sessions", { tenantScoped: false })`.
|
|
631
|
+
With `gate.auth.tenant` on, KV keys are prefixed `{tenantId}:` (logical keys in `do` stay unprefixed). Missing `fx.tenant.id` throws **OKE1015**. Opt out with `store.kv("sessions", { tenantScoped: false })`.
|
|
698
632
|
|
|
699
|
-
Gate rates and Signal stay on `REDIS_URL`. Missing `DATABASE_URL` with the postgres driver
|
|
700
|
-
fails boot: `oke boot: durable store.kv needs DATABASE_URL`.
|
|
633
|
+
Gate rates and Signal stay on `REDIS_URL`. Missing `DATABASE_URL` with the postgres driver fails boot: `oke boot: durable store.kv needs DATABASE_URL`.
|
|
701
634
|
|
|
702
635
|
<Callout title="Not per-key, not Flow durable, not auto-cache">
|
|
703
636
|
Durable KV is a JSONB table on the same Postgres as `store.sql`. Flow `durable` journals steps.
|
|
704
637
|
Tier-1 auto-cache is an in-process Map and never reads `store.kv`.
|
|
705
638
|
</Callout>
|
|
706
639
|
|
|
707
|
-
Driver id stays `redis` for every image below — same `REDIS_URL`, zero Flow changes.
|
|
708
|
-
Redis is the default because it is the most mature and battle-tested; Valkey and
|
|
709
|
-
Dragonfly are equally legitimate opt-in pins.
|
|
640
|
+
Driver id stays `redis` for every image below — same `REDIS_URL`, zero Flow changes. Redis is the default because it is the most mature and battle-tested; Valkey and Dragonfly are equally legitimate opt-in pins.
|
|
710
641
|
|
|
711
642
|
| Image | Pin | Why pick it | License |
|
|
712
643
|
| ------------- | --------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -717,7 +648,7 @@ Dragonfly are equally legitimate opt-in pins.
|
|
|
717
648
|
<Callout title="Licenses bite managed-service resellers — read this once" type="warn">
|
|
718
649
|
RSAL, SSPL, and BSL service restrictions apply when you offer that specific datastore **as a
|
|
719
650
|
service** to third parties — not when you run it for your own app. Pick for your situation; OKE
|
|
720
|
-
does not pick a
|
|
651
|
+
does not pick a "safer" default for you.
|
|
721
652
|
</Callout>
|
|
722
653
|
|
|
723
654
|
<Callout title="Dry-run refuses KV writes">
|
|
@@ -725,13 +656,38 @@ Dragonfly are equally legitimate opt-in pins.
|
|
|
725
656
|
double-write against a shared Redis. Reads (`get` / `list`) still run.
|
|
726
657
|
</Callout>
|
|
727
658
|
|
|
659
|
+
### Per-environment KV drivers
|
|
660
|
+
|
|
661
|
+
```typescript title="oke.config.ts"
|
|
662
|
+
drivers: {
|
|
663
|
+
store: {
|
|
664
|
+
kv: { dev: "redis", test: "memory", prod: "redis" },
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
images: {
|
|
668
|
+
store: { kv: "redis:8-alpine" },
|
|
669
|
+
// or: "valkey/valkey:8-alpine"
|
|
670
|
+
// or: "docker.dragonflydb.io/dragonflydb/dragonfly"
|
|
671
|
+
},
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
| Facet | `dev` / `prod` | `test` | Runs as |
|
|
675
|
+
| ----- | -------------- | -------- | ---------------------- |
|
|
676
|
+
| `kv` | `redis` | `memory` | container → in-process |
|
|
677
|
+
|
|
678
|
+
| Variable | When |
|
|
679
|
+
| ------------------ | -------------------------------- |
|
|
680
|
+
| `REDIS_URL` | Required for the `redis` driver |
|
|
681
|
+
| `OKE_STORE_KV_URL` | Override if `REDIS_URL` is unset |
|
|
682
|
+
| `OKE_KV_DRIVER` | Force a driver id |
|
|
683
|
+
|
|
684
|
+
Missing Redis URL fails boot loudly: `oke boot: redis driver needs REDIS_URL`.
|
|
685
|
+
|
|
728
686
|
## Files
|
|
729
687
|
|
|
730
688
|
<StoreFacetMark facet="files" />
|
|
731
689
|
|
|
732
|
-
The blob facet: opaque objects (`Uint8Array` or string) on the same handle as the optional image
|
|
733
|
-
pipeline. Drivers swap `fs` ↔ `s3` (plus `memory` for tests). The Console Files band is a folder
|
|
734
|
-
browser — upload, download, delete — not a signed-URL mint.
|
|
690
|
+
The blob facet: opaque objects (`Uint8Array` or string) on the same handle as the optional image pipeline. Drivers swap `fs` ↔ `s3` (plus `memory` for tests). The Console Files band is a folder browser — upload, download, delete — not a signed-URL mint.
|
|
735
691
|
|
|
736
692
|
### Quick start
|
|
737
693
|
|
|
@@ -829,8 +785,7 @@ images: {
|
|
|
829
785
|
|
|
830
786
|
### Images — `image` / `putImage`
|
|
831
787
|
|
|
832
|
-
For photos, use Bun's built-in `Bun.Image` pipeline on the same handle (requires Bun `>=1.4.0`).
|
|
833
|
-
`putImage` is one write that fans into several keys:
|
|
788
|
+
For photos, use Bun's built-in `Bun.Image` pipeline on the same handle (requires Bun `>=1.4.0`). `putImage` is one write that fans into several keys:
|
|
834
789
|
|
|
835
790
|
<StoreFilesVariants />
|
|
836
791
|
|
|
@@ -876,8 +831,7 @@ const result = await fx.store(uploads).putImage("photos/x.jpg", bytes, {
|
|
|
876
831
|
|
|
877
832
|
<StoreFacetMark facet="index" />
|
|
878
833
|
|
|
879
|
-
The search / similarity facet: vector ANN and opt-in full-text, both behind `store.index`. The
|
|
880
|
-
handle is a **discriminated union** on `driverId` — vector methods and text methods never mix.
|
|
834
|
+
The search / similarity facet: vector ANN and opt-in full-text, both behind `store.index`. The handle is a **discriminated union** on `driverId` — vector methods and text methods never mix.
|
|
881
835
|
|
|
882
836
|
### Quick start
|
|
883
837
|
|
|
@@ -934,8 +888,7 @@ One declaration shape — two search physics. The handle is a **discriminated un
|
|
|
934
888
|
probe. Meilisearch uses full-text `q`.
|
|
935
889
|
</Callout>
|
|
936
890
|
|
|
937
|
-
TypeScript rejects a vector query against a text index (and vice versa) at compile time — never at
|
|
938
|
-
runtime.
|
|
891
|
+
TypeScript rejects a vector query against a text index (and vice versa) at compile time — never at runtime.
|
|
939
892
|
|
|
940
893
|
### Vector ANN
|
|
941
894
|
|
|
@@ -944,16 +897,13 @@ runtime.
|
|
|
944
897
|
| `memory` | — (in-process, default) | Full scan cosine |
|
|
945
898
|
| `pgvector` | `postgres` or `pglite` sql | HNSW + `cosineDistance` |
|
|
946
899
|
|
|
947
|
-
**Consequence:** a configured SQL-backed index that cannot reach its engine — missing peer, missing
|
|
948
|
-
`vector` extension, wrong sql driver — fails loudly at first use. It never silently falls back to
|
|
949
|
-
`memory`. A vector whose length ≠ `dims` throws `vector dims X !== index dims Y`.
|
|
900
|
+
**Consequence:** a configured SQL-backed index that cannot reach its engine — missing peer, missing `vector` extension, wrong sql driver — fails loudly at first use. It never silently falls back to `memory`. A vector whose length ≠ `dims` throws `vector dims X !== index dims Y`.
|
|
950
901
|
|
|
951
902
|
Pair SQL + index under [Advanced — composing facets together](#advanced--composing-facets-together).
|
|
952
903
|
|
|
953
904
|
### Full-text search — `meilisearch` (opt-in)
|
|
954
905
|
|
|
955
|
-
`meilisearch` is a fourth `store.index` backend, opt-in only — `memory` stays the default when
|
|
956
|
-
`drivers.store.index` is unset. Typo-tolerant, faceted full-text relevance — not cosine similarity.
|
|
906
|
+
`meilisearch` is a fourth `store.index` backend, opt-in only — `memory` stays the default when `drivers.store.index` is unset. Typo-tolerant, faceted full-text relevance — not cosine similarity.
|
|
957
907
|
|
|
958
908
|
```typescript
|
|
959
909
|
export const kb = store.index("kb", { description: "Help articles" });
|
|
@@ -1012,9 +962,7 @@ drivers: {
|
|
|
1012
962
|
|
|
1013
963
|
### With AI embeddings
|
|
1014
964
|
|
|
1015
|
-
`ai.embed` writes into a vector `store.index`; Flows search it with `fx.search` (or
|
|
1016
|
-
`fx.store(index).search`). See [AI](/docs/elements/ai) — this path is vector-only and will not
|
|
1017
|
-
target meilisearch.
|
|
965
|
+
`ai.embed` writes into a vector `store.index`; Flows search it with `fx.search` (or `fx.store(index).search`). See [AI](/docs/elements/ai) — this path is vector-only and will not target meilisearch.
|
|
1018
966
|
|
|
1019
967
|
## Advanced — composing facets together
|
|
1020
968
|
|
|
@@ -1049,84 +997,82 @@ Container images come from the `images` map — change the vendor by changing th
|
|
|
1049
997
|
|
|
1050
998
|
Columns tagged `.pii()` or `.sensitive()` are masked at the store boundary — flows, logs, and the Console see a mask, not the value. Revealing cleartext PII requires an explicit `pii:reveal` gate on the flow, so access is a permission, not a convention.
|
|
1051
999
|
|
|
1052
|
-
Optional `description` on a store (and `.describe("…")` on fields) becomes its human title in
|
|
1053
|
-
operator tools. Single-table `reads` / `writes` in the Manifest power cache invalidation and that
|
|
1054
|
-
same PII masking — see the Callout under [Querying by hand](#querying-by-hand).
|
|
1000
|
+
Optional `description` on a store (and `.describe("…")` on fields) becomes its human title in operator tools. Single-table `reads` / `writes` in the Manifest power cache invalidation and that same PII masking — see the Callout under [Querying by hand](#querying-by-hand).
|
|
1055
1001
|
|
|
1056
1002
|
## Troubleshooting
|
|
1057
1003
|
|
|
1058
1004
|
<Accordions>
|
|
1059
|
-
<Accordion title="Unable to resolve table name from value">
|
|
1060
|
-
|
|
1061
|
-
A `store.schema.table` column named `name` used to shadow the SQL table name.
|
|
1062
|
-
Inserts and selects now resolve it from the declaration. Keep the `name` column.
|
|
1063
1005
|
|
|
1006
|
+
<Accordion title="Unable to resolve table name from value">
|
|
1007
|
+
A `store.schema.table` column named `name` used to shadow the SQL table name. Inserts and selects
|
|
1008
|
+
now resolve it from the declaration. Keep the `name` column.
|
|
1064
1009
|
</Accordion>
|
|
1065
|
-
<Accordion title="OKE1101 — missing table in prod">
|
|
1066
|
-
|
|
1067
|
-
Schema DDL never runs automatically in `prod`. Under `oke dev`, `db.autoPush` (default `true`) runs `oke db push`. For production use `oke db generate` + `oke db migrate`.
|
|
1068
1010
|
|
|
1011
|
+
<Accordion title="OKE1101 — missing table in prod">
|
|
1012
|
+
Schema DDL never runs automatically in `prod`. Under `oke dev`, `db.autoPush` (default `true`)
|
|
1013
|
+
runs `oke db push`. For production use `oke db generate` + `oke db migrate`.
|
|
1069
1014
|
</Accordion>
|
|
1070
|
-
<Accordion title="oke db push: missing_hints">
|
|
1071
|
-
|
|
1072
|
-
New tables are created automatically. Push only manages `public` — it will not drop schema `oke` (RLS helpers) or `oke_console`. Remaining `missing_hints` are destructive (drop a non-empty public table, type change). Review with `oke db generate`.
|
|
1073
1015
|
|
|
1016
|
+
<Accordion title="oke db push: missing_hints">
|
|
1017
|
+
New tables are created automatically. Push only manages `public` — it will not drop schema `oke`
|
|
1018
|
+
(RLS helpers) or `oke_console`. Remaining `missing_hints` are destructive (drop a non-empty public
|
|
1019
|
+
table, type change). Review with `oke db generate`.
|
|
1074
1020
|
</Accordion>
|
|
1075
|
-
<Accordion title="I need a join — with: is not supported">
|
|
1076
|
-
|
|
1077
|
-
`fx.store` is one table per call by design. Read each table separately and compose in the flow (or extract a shared flow and `fx.call` it). Every table then appears explicitly in the Manifest's effect graph — which is what powers cache invalidation and PII masking.
|
|
1078
1021
|
|
|
1022
|
+
<Accordion title="I need a join — with: is not supported">
|
|
1023
|
+
`fx.store` is one table per call by design. Read each table separately and compose in the flow (or
|
|
1024
|
+
extract a shared flow and `fx.call` it). Every table then appears explicitly in the Manifest's
|
|
1025
|
+
effect graph — which is what powers cache invalidation and PII masking.
|
|
1079
1026
|
</Accordion>
|
|
1080
|
-
<Accordion title="Pagination shows duplicates when rows are inserted">
|
|
1081
|
-
|
|
1082
|
-
You are on offset paging. Switch `list` to keyset by setting `cursor` columns with a stable order (e.g. `[createdAt, id]`) — the page boundary becomes a row predicate, not a row count.
|
|
1083
1027
|
|
|
1028
|
+
<Accordion title="Pagination shows duplicates when rows are inserted">
|
|
1029
|
+
You are on offset paging. Switch `list` to keyset by setting `cursor` columns with a stable order
|
|
1030
|
+
(e.g. `[createdAt, id]`) — the page boundary becomes a row predicate, not a row count.
|
|
1084
1031
|
</Accordion>
|
|
1085
|
-
<Accordion title="meta.total is slow on a big table">
|
|
1086
|
-
|
|
1087
|
-
`count: "exact"` runs `COUNT(*)` per page. Set `count: "none"` in the `list` options to skip it, or use keyset mode where totals are rarely needed.
|
|
1088
1032
|
|
|
1033
|
+
<Accordion title="meta.total is slow on a big table">
|
|
1034
|
+
`count: "exact"` runs `COUNT(*)` per page. Set `count: "none"` in the `list` options to skip it,
|
|
1035
|
+
or use keyset mode where totals are rarely needed.
|
|
1089
1036
|
</Accordion>
|
|
1090
|
-
<Accordion title="List data is { items, count } instead of an array">
|
|
1091
1037
|
|
|
1038
|
+
<Accordion title="List data is { items, count } instead of an array">
|
|
1092
1039
|
Success already wraps `{ data, error: null }`. Returning a page object from `do` nests the pager inside `data`. Prefer `out: z.array(Item)` and `return fx.json.withQuery(rows, input)`. Any other declared `out` is still valid.
|
|
1093
|
-
|
|
1094
1040
|
</Accordion>
|
|
1095
|
-
<Accordion title="KV TTL never expires in tests">
|
|
1096
|
-
|
|
1097
|
-
The `test` default is `memory`, which ignores TTL on `set`. Under `oke dev`, KV is `redis` by default — use that when you need real expiry.
|
|
1098
1041
|
|
|
1042
|
+
<Accordion title="KV TTL never expires in tests">
|
|
1043
|
+
The `test` default is `memory`, which ignores TTL on `set`. Under `oke dev`, KV is `redis` by
|
|
1044
|
+
default — use that when you need real expiry.
|
|
1099
1045
|
</Accordion>
|
|
1100
|
-
<Accordion title="oke boot: redis / meilisearch / S3 URL missing">
|
|
1101
|
-
|
|
1102
|
-
Driver-backed facets fail boot when their URL is absent — Redis needs `REDIS_URL`, meilisearch needs `OKE_STORE_INDEX_URL`, S3 needs `S3_BUCKET`. `oke dev` writes these into the Compose env.
|
|
1103
1046
|
|
|
1047
|
+
<Accordion title="oke boot: redis / meilisearch / S3 URL missing">
|
|
1048
|
+
Driver-backed facets fail boot when their URL is absent — Redis needs `REDIS_URL`, meilisearch
|
|
1049
|
+
needs `OKE_STORE_INDEX_URL`, S3 needs `S3_BUCKET`. `oke dev` writes these into the Compose env.
|
|
1104
1050
|
</Accordion>
|
|
1105
|
-
<Accordion title="vector dims X !== index dims Y">
|
|
1106
|
-
|
|
1107
|
-
The vector length must match `store.index(…, { dims })` (runtime default `3` if unset). Align `dims` with your embedder, or fix the vector you pass to `upsert` / `search`.
|
|
1108
1051
|
|
|
1052
|
+
<Accordion title="vector dims X !== index dims Y">
|
|
1053
|
+
The vector length must match `store.index(…, {dims})` (runtime default `3` if unset). Align `dims`
|
|
1054
|
+
with your embedder, or fix the vector you pass to `upsert` / `search`.
|
|
1109
1055
|
</Accordion>
|
|
1110
|
-
<Accordion title="Index stays on memory under oke dev">
|
|
1111
|
-
|
|
1112
|
-
Unlike KV/Files, unset `drivers.store.index` does **not** promote to `pgvector` in `dev` — it stays `memory`. Set the map explicitly (and pair SQL for `pgvector`).
|
|
1113
1056
|
|
|
1057
|
+
<Accordion title="Index stays on memory under oke dev">
|
|
1058
|
+
Unlike KV/Files, unset `drivers.store.index` does **not** promote to `pgvector` in `dev` — it
|
|
1059
|
+
stays `memory`. Set the map explicitly (and pair SQL for `pgvector`).
|
|
1114
1060
|
</Accordion>
|
|
1115
|
-
<Accordion title="Console warns Non-ASCII object key">
|
|
1116
|
-
|
|
1117
|
-
`fx.store(files).put` stores the key as-is. Console uploads mint an ASCII key and keep the original name in `.oke/catalog.json`. Re-upload to rewrite an existing object.
|
|
1118
1061
|
|
|
1062
|
+
<Accordion title="Console warns Non-ASCII object key">
|
|
1063
|
+
`fx.store(files).put` stores the key as-is. Console uploads mint an ASCII key and keep the
|
|
1064
|
+
original name in `.oke/catalog.json`. Re-upload to rewrite an existing object.
|
|
1119
1065
|
</Accordion>
|
|
1120
|
-
<Accordion title="oke dev skipped the seed prompt">
|
|
1121
|
-
|
|
1122
|
-
`.oke/state.json` stores `seededAt` plus this app's `defineSeed({ name })` — notes vs keel are different ids. A new name re-asks. Clear those keys to prompt again.
|
|
1123
1066
|
|
|
1067
|
+
<Accordion title="oke dev skipped the seed prompt">
|
|
1068
|
+
`.oke/state.json` stores `seededAt` plus this app's `defineSeed({name})` — notes vs keel are
|
|
1069
|
+
different ids. A new name re-asks. Clear those keys to prompt again.
|
|
1124
1070
|
</Accordion>
|
|
1125
|
-
<Accordion title="Console KV shows No rows after oke db seed">
|
|
1126
1071
|
|
|
1072
|
+
<Accordion title="Console KV shows No rows after oke db seed">
|
|
1127
1073
|
Default `store.kv` is cache Redis with no AOF — a recreate drops keys. SQL seed still looks full. Mark seeded namespaces `{ durable: true }` so they live in `oke_kv`, then restart and seed again.
|
|
1128
|
-
|
|
1129
1074
|
</Accordion>
|
|
1075
|
+
|
|
1130
1076
|
</Accordions>
|
|
1131
1077
|
|
|
1132
1078
|
## Learn more
|