okengine 0.17.2 → 0.18.4
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 +5 -3
- package/README.md +14 -10
- package/manifest.v1.schema.json +61 -2
- package/package.json +19 -5
- package/site/content/docs/ai/index.mdx +1 -1
- package/site/content/docs/ai/mcp.mdx +10 -1
- package/site/content/docs/ai/skills.mdx +9 -9
- package/site/content/docs/elements/ai.mdx +1 -1
- package/site/content/docs/elements/clock.mdx +1 -1
- package/site/content/docs/elements/flow.mdx +25 -1
- package/site/content/docs/elements/gate.mdx +3 -2
- package/site/content/docs/elements/store.mdx +289 -341
- package/site/content/docs/elements/vault.mdx +5 -5
- package/site/content/docs/get-started/basic-usage.mdx +3 -10
- package/site/content/docs/get-started/index.mdx +1 -1
- package/site/content/docs/get-started/installation.mdx +2 -3
- package/site/content/docs/get-started/introduction.mdx +58 -121
- package/site/content/docs/get-started/meta.json +9 -1
- package/site/content/docs/get-started/project-structure.mdx +4 -11
- package/site/content/docs/get-started/testing.mdx +328 -0
- package/site/content/docs/get-started/why.mdx +93 -71
- package/site/content/docs/index.mdx +44 -11
- package/site/content/docs/meta.json +8 -5
- 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/providers/index.mdx +2 -0
- package/site/content/docs/recipes/index.mdx +2 -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-De7Lc2JC.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-RGy7VEA_.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-_rgpdVzo.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-Ds6pcnh-.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-4rHOePuE.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-DISPgxLM.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 +5 -2
- 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,125 @@ 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
|
+
> **Realtime did not introduce a new subsystem. It composed existing Store, Signal, Gate/RLS, Flow, and Manifest semantics.**
|
|
133
|
+
|
|
134
|
+
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
135
|
|
|
222
136
|
```typescript
|
|
223
|
-
|
|
137
|
+
const notesR = store.resource(db, notes, {
|
|
138
|
+
in: NewNote,
|
|
139
|
+
out: Note,
|
|
140
|
+
live: true, // synthesizes GET /notes/live + internal live signal
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const mounted = on(http.resource("/notes", notesR.all()).gate(member));
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
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`.
|
|
147
|
+
|
|
148
|
+
`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:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { liveQuery } from "okengine";
|
|
152
|
+
|
|
153
|
+
export const live = on(
|
|
154
|
+
http.get("/tasks/live").gate(member).live(tasks),
|
|
155
|
+
flow("tasks.live", {
|
|
156
|
+
in: { unknown: true },
|
|
157
|
+
do: async (input, fx) =>
|
|
158
|
+
liveQuery(fx, tasks, input, {
|
|
159
|
+
search: [tasks.title],
|
|
160
|
+
filter: [tasks.status], // same list grammar as store.resource
|
|
161
|
+
order: "all",
|
|
162
|
+
}),
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
224
165
|
```
|
|
225
166
|
|
|
226
|
-
|
|
167
|
+
`.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.
|
|
168
|
+
|
|
169
|
+
- `?status=eq.open` filters the live window the same way it filters `GET /tasks`.
|
|
170
|
+
- 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.
|
|
171
|
+
|
|
172
|
+
#### The project-wide default — `oke({ store: { live: true } })`
|
|
227
173
|
|
|
228
|
-
|
|
174
|
+
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:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
const app = oke({
|
|
178
|
+
name: "shop",
|
|
179
|
+
store: { live: true }, // new tables are live by default from here on
|
|
180
|
+
});
|
|
181
|
+
```
|
|
229
182
|
|
|
230
183
|
```typescript
|
|
231
|
-
|
|
184
|
+
// No explicit live needed — this table is live by default.
|
|
185
|
+
export const notes = store.schema.table(
|
|
186
|
+
"notes",
|
|
187
|
+
{
|
|
188
|
+
id: field.text().primaryKey(),
|
|
189
|
+
updatedAt: field.integer().notNull(),
|
|
190
|
+
},
|
|
191
|
+
[store.schema.policy.owner("owner")],
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const notesR = store.resource(db, notes, {}); // GET /notes/live synthesized
|
|
195
|
+
const mounted = on(http.resource("/notes", notesR.all()).gate(member));
|
|
232
196
|
```
|
|
233
197
|
|
|
234
|
-
|
|
198
|
+
Three rules keep the flip predictable:
|
|
235
199
|
|
|
236
|
-
|
|
200
|
+
- **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.
|
|
201
|
+
- **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.
|
|
202
|
+
- **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.
|
|
237
203
|
|
|
238
204
|
```typescript
|
|
239
|
-
|
|
205
|
+
export const archive = store.schema.table(
|
|
206
|
+
"archive",
|
|
207
|
+
{
|
|
208
|
+
id: field.text().primaryKey(),
|
|
209
|
+
},
|
|
210
|
+
[
|
|
211
|
+
store.schema.policy.owner("owner"),
|
|
212
|
+
store.schema.live(false), // exempt from the project-wide default
|
|
213
|
+
],
|
|
214
|
+
);
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**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.
|
|
218
|
+
|
|
219
|
+
#### Live fan-out latency — the real cost model
|
|
220
|
+
|
|
221
|
+
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):
|
|
222
|
+
|
|
240
223
|
```
|
|
224
|
+
p50_fanout ≈ ceil(S / pool_concurrency) × stamp_p50
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Measured from the benchmark sweep (G8a / G8c):
|
|
228
|
+
|
|
229
|
+
| Active subscribers (same table/query) | Estimated p50 fan-out | UX |
|
|
230
|
+
| ------------------------------------- | --------------------- | --------------------------------------------- |
|
|
231
|
+
| **1–30** | 1–15 ms | Instant |
|
|
232
|
+
| **30–80** | 15–50 ms | Instant to snappy |
|
|
233
|
+
| **80–150** | 50–100 ms | Snappy; power users may notice on hot tables |
|
|
234
|
+
| **150–300** | 100–200 ms | **Perceptible lag** — updates arrive in waves |
|
|
235
|
+
| **300+** | 200 ms+ linear | **Not instant** — capacity planning required |
|
|
236
|
+
|
|
237
|
+
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.)
|
|
241
238
|
|
|
242
|
-
|
|
239
|
+
`live: true` carries compile-time guardrails at extract, plus durable delivery and runtime watchtowers:
|
|
240
|
+
|
|
241
|
+
- A table with **no primary key fails extract loud** — upsert/revoked/delete address rows by PK.
|
|
242
|
+
- 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).
|
|
243
|
+
- CDC events ride the durable `oke_cdc_outbox`; delivered rows prune on retention defaults (24h / 50k) so the outbox never grows unbounded.
|
|
244
|
+
- `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
245
|
|
|
244
246
|
### Handwritten HTTP lists
|
|
245
247
|
|
|
@@ -267,10 +269,6 @@ Zero-config: `?q=` searches every string field, extra keys auto-eq except path `
|
|
|
267
269
|
shape matches `store.resource` and the typed client (`data` is the page, `meta` is the pager).
|
|
268
270
|
</Callout>
|
|
269
271
|
|
|
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
272
|
### Querying by hand
|
|
275
273
|
|
|
276
274
|
When the list is not conventional CRUD, `fx.store(db)` is the full single-table session:
|
|
@@ -291,8 +289,7 @@ await fx.store(db).update(notes).set({ title: input.title }).where(eq(notes.id,
|
|
|
291
289
|
await fx.store(db).delete(notes).where(lt(notes.createdAt, cutoff));
|
|
292
290
|
```
|
|
293
291
|
|
|
294
|
-
`select().from(notes)` infers the declared row (`string` / `number` / `null` from
|
|
295
|
-
`field.*`). A `select({ title: notes.title })` projection stays untyped.
|
|
292
|
+
`select().from(notes)` infers the declared row (`string` / `number` / `null` from `field.*`). A `select({ title: notes.title })` projection stays untyped.
|
|
296
293
|
|
|
297
294
|
<Callout title="One table per call — no relational with:">
|
|
298
295
|
`fx.store` is deliberately **single-table**: Drizzle's relational `findMany({ with: … })` is not
|
|
@@ -304,46 +301,37 @@ await fx.store(db).delete(notes).where(lt(notes.createdAt, cutoff));
|
|
|
304
301
|
|
|
305
302
|
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
303
|
|
|
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>
|
|
304
|
+
| Field API | Meaning |
|
|
305
|
+
| ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
306
|
+
| `field.text({ enum })` | text; enum narrows to a literal union |
|
|
307
|
+
| `field.id()` | id column with the default generation id (currently OK ID) minted on insert |
|
|
308
|
+
| `field.okid()` | id column explicitly generated with OK ID |
|
|
309
|
+
| `field.varchar({ length })` · `field.char` | bounded strings (same enum support) |
|
|
310
|
+
| `field.boolean()` | boolean |
|
|
311
|
+
| `field.smallint()` · `field.integer()` | ints (abstract integer emits as Drizzle `bigint`) |
|
|
312
|
+
| `field.bigint({ mode })` | int8 — JS `number` (default) / `bigint` / `string` |
|
|
313
|
+
| `field.serial()` · `smallserial` · `bigserial` | auto-increment — NOT NULL by SQL physics |
|
|
314
|
+
| `field.numeric({ precision, scale })` | exact decimals — infers `string`; `{ mode }` opts into `number` / `bigint` |
|
|
315
|
+
| `field.real()` · `doublePrecision()` | floats (~6 / ~15 significant digits) |
|
|
316
|
+
| `field.json<T>()` · `jsonb<T>()` | JSON payloads with a generic type |
|
|
317
|
+
| `field.uuid()` | UUID strings |
|
|
318
|
+
| `field.time()` · `timestamp()` · `date()` | temporals — `Date` by default; `{ mode: "string" }` opts into ISO `string` |
|
|
319
|
+
| `field.interval({ fields })` | time spans |
|
|
320
|
+
| `field.point({ mode })` · `line()` | geometric tuples or objects |
|
|
321
|
+
| `field.bytea()` | binary (`Buffer`) |
|
|
322
|
+
| `field.inet()` · `cidr` · `macaddr8` | network addresses as strings |
|
|
323
|
+
| `.primaryKey()` · `.notNull()` · `.unique()` | constraints |
|
|
324
|
+
| `.default(v)` · `.defaultFn(id \| now \| nowIso \| nowDate)` | defaults — typed against the column's JS type |
|
|
325
|
+
| `.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 |
|
|
326
|
+
| `.type<T>()` | override the inferred JS type (mirrors Drizzle `$type<T>`) |
|
|
327
|
+
| `.pii()` · `.sensitive()` · `.retain("30d")` | privacy classification |
|
|
328
|
+
| `.as("sql_name")` | override the automatic `camelCase → snake_case` |
|
|
329
|
+
| `.describe("…")` | human title in the Console (falls back to key) |
|
|
330
|
+
| `.references(() => col, { onDelete })` | foreign key |
|
|
331
|
+
| `store.schema.rls()` | `pgTable.withRLS` when there are no policies |
|
|
332
|
+
| `store.schema.policy.gate/owner/scope/tenant` | happy-path RLS (`oke.gate()` / `oke.user()` / `oke.has_scope()` / `oke.tenant()`) |
|
|
333
|
+
| `store.schema.unscoped()` | Shared table — required when tenancy is on and there is no tenant policy |
|
|
334
|
+
| `store.schema.live(false)` | Per-table opt-out from the project-wide `store.live` default — not live-by-default, no synthesized `/live` surface |
|
|
347
335
|
|
|
348
336
|
#### Foreign keys and relations
|
|
349
337
|
|
|
@@ -379,20 +367,12 @@ export const relations = store.schema.relations({ links, daily }, (r) => ({
|
|
|
379
367
|
| `oke db generate` | Write versioned SQL under `drizzle/` for review |
|
|
380
368
|
| `oke db migrate` | Apply those files — human or CI, **never** at boot |
|
|
381
369
|
|
|
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.
|
|
370
|
+
`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
371
|
|
|
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`).
|
|
372
|
+
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
373
|
|
|
389
374
|
#### Multiple environments
|
|
390
375
|
|
|
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
376
|
| Environment | Sync |
|
|
397
377
|
| -------------- | -------------------------------------------------------------- |
|
|
398
378
|
| `dev` | `oke db push` (or auto-push from `oke dev`) |
|
|
@@ -404,19 +384,13 @@ Staging/prod accumulate versioned SQL under `drizzle/` (`oke db generate`).
|
|
|
404
384
|
that transaction — a leaked `BEGIN` exhausts checkout.
|
|
405
385
|
</Callout>
|
|
406
386
|
|
|
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).
|
|
387
|
+
**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
388
|
|
|
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.
|
|
389
|
+
**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
390
|
|
|
415
391
|
#### Seeding
|
|
416
392
|
|
|
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).
|
|
393
|
+
`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
394
|
|
|
421
395
|
<StoreSeeding />
|
|
422
396
|
|
|
@@ -483,39 +457,30 @@ export default defineSeed({
|
|
|
483
457
|
| `already-existed` | match found; default leaves it alone |
|
|
484
458
|
| `changed` | match found and `{ onExisting: "update" }` |
|
|
485
459
|
|
|
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.
|
|
460
|
+
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
461
|
|
|
489
462
|
```ts
|
|
490
463
|
// 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
|
-
);
|
|
464
|
+
await fx
|
|
465
|
+
.store(db)
|
|
466
|
+
.upsert(
|
|
467
|
+
notes,
|
|
468
|
+
{ id: "welcome" },
|
|
469
|
+
{ id: "welcome", title: "Welcome", body: "Your Notes API is ready.", createdAt: 1 },
|
|
470
|
+
);
|
|
501
471
|
|
|
502
472
|
// 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
|
-
);
|
|
473
|
+
await fx
|
|
474
|
+
.store(db)
|
|
475
|
+
.upsert(
|
|
476
|
+
notes,
|
|
477
|
+
{ id: "welcome" },
|
|
478
|
+
{ id: "welcome", title: "Welcome (updated)", body: "New bootstrap copy.", createdAt: 1 },
|
|
479
|
+
{ onExisting: "update" },
|
|
480
|
+
);
|
|
514
481
|
```
|
|
515
482
|
|
|
516
|
-
|
|
517
|
-
is for seed-owned rows you intentionally overwrite. Schema-wide data fixes still belong in
|
|
518
|
-
migrations, not seed.
|
|
483
|
+
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
484
|
|
|
520
485
|
| Env | Confirm |
|
|
521
486
|
| -------------- | ----------------------------------------------------- |
|
|
@@ -540,6 +505,34 @@ oke db seed --env prod --force
|
|
|
540
505
|
extending an app-owned table with plugin columns is not supported in v1.
|
|
541
506
|
</Callout>
|
|
542
507
|
|
|
508
|
+
### Access policies — owner, tenant, scope
|
|
509
|
+
|
|
510
|
+
Row-level security is declared per table as the third argument to `store.schema.table` — the policy list is Drizzle-shaped extras:
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
export const bookings = store.schema.table(
|
|
514
|
+
"bookings",
|
|
515
|
+
{
|
|
516
|
+
id: field.text().primaryKey(),
|
|
517
|
+
owner: field.text().notNull(),
|
|
518
|
+
},
|
|
519
|
+
[
|
|
520
|
+
store.schema.policy.gate("member", { for: "select" }),
|
|
521
|
+
store.schema.policy.owner("owner", { for: "all" }),
|
|
522
|
+
store.schema.policy.scope("booking:create", { for: "insert" }),
|
|
523
|
+
store.schema.policy.tenant("tenant_id"),
|
|
524
|
+
],
|
|
525
|
+
);
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
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.
|
|
529
|
+
|
|
530
|
+
<Callout title="User-plane fx.store applies RLS">
|
|
531
|
+
HTTP / resource flows stamp Gate identity (`SET LOCAL ROLE oke_app` + `set_config`). Operator /
|
|
532
|
+
cron / CDC / signal stay unstamped. Tenancy on: also `oke.tenant()`; tables need `policy.tenant`
|
|
533
|
+
or `unscoped()`.
|
|
534
|
+
</Callout>
|
|
535
|
+
|
|
543
536
|
### Per-environment SQL drivers
|
|
544
537
|
|
|
545
538
|
Same flow code, different backends — configured once in `oke.config.ts`:
|
|
@@ -568,14 +561,11 @@ SQL-backed index driver `pgvector` shares this facet's connection — see [Advan
|
|
|
568
561
|
|
|
569
562
|
<StoreFacetMark facet="kv" />
|
|
570
563
|
|
|
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.
|
|
564
|
+
The key-value facet: short-lived cache and session data behind `fx.store`. One namespace per declaration; drivers swap `memory` ↔ `redis` per environment.
|
|
573
565
|
|
|
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.
|
|
566
|
+
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
567
|
|
|
577
|
-
The same band
|
|
578
|
-
driver. In-process `memory` is unsupported (`KvStatsUnsupported`).
|
|
568
|
+
The same band's **Performance** control opens engine telemetry on the `redis` driver. In-process `memory` is unsupported (`KvStatsUnsupported`).
|
|
579
569
|
|
|
580
570
|
### Quick start
|
|
581
571
|
|
|
@@ -620,68 +610,17 @@ do: async ({ userId, data }, fx) => {
|
|
|
620
610
|
|
|
621
611
|
### TTL
|
|
622
612
|
|
|
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.
|
|
613
|
+
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
614
|
|
|
627
615
|
<StoreKvTtl />
|
|
628
616
|
|
|
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`.
|
|
617
|
+
**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
618
|
|
|
679
619
|
### Durable KV
|
|
680
620
|
|
|
681
621
|
Default `store.kv("sessions")` is cache-shaped — a Redis recreate drops keys.
|
|
682
622
|
|
|
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.
|
|
623
|
+
`{ 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
624
|
|
|
686
625
|
```typescript
|
|
687
626
|
export const sessions = store.kv("sessions", { description: "Session cache" });
|
|
@@ -689,24 +628,18 @@ export const ledger = store.kv("ledger", { durable: true, description: "Idempote
|
|
|
689
628
|
export const drafts = store.kv("drafts", { durable: true, description: "Compose drafts" });
|
|
690
629
|
```
|
|
691
630
|
|
|
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 }`.
|
|
631
|
+
**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
632
|
|
|
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 })`.
|
|
633
|
+
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
634
|
|
|
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`.
|
|
635
|
+
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
636
|
|
|
702
637
|
<Callout title="Not per-key, not Flow durable, not auto-cache">
|
|
703
638
|
Durable KV is a JSONB table on the same Postgres as `store.sql`. Flow `durable` journals steps.
|
|
704
639
|
Tier-1 auto-cache is an in-process Map and never reads `store.kv`.
|
|
705
640
|
</Callout>
|
|
706
641
|
|
|
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.
|
|
642
|
+
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
643
|
|
|
711
644
|
| Image | Pin | Why pick it | License |
|
|
712
645
|
| ------------- | --------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -717,7 +650,7 @@ Dragonfly are equally legitimate opt-in pins.
|
|
|
717
650
|
<Callout title="Licenses bite managed-service resellers — read this once" type="warn">
|
|
718
651
|
RSAL, SSPL, and BSL service restrictions apply when you offer that specific datastore **as a
|
|
719
652
|
service** to third parties — not when you run it for your own app. Pick for your situation; OKE
|
|
720
|
-
does not pick a
|
|
653
|
+
does not pick a "safer" default for you.
|
|
721
654
|
</Callout>
|
|
722
655
|
|
|
723
656
|
<Callout title="Dry-run refuses KV writes">
|
|
@@ -725,13 +658,38 @@ Dragonfly are equally legitimate opt-in pins.
|
|
|
725
658
|
double-write against a shared Redis. Reads (`get` / `list`) still run.
|
|
726
659
|
</Callout>
|
|
727
660
|
|
|
661
|
+
### Per-environment KV drivers
|
|
662
|
+
|
|
663
|
+
```typescript title="oke.config.ts"
|
|
664
|
+
drivers: {
|
|
665
|
+
store: {
|
|
666
|
+
kv: { dev: "redis", test: "memory", prod: "redis" },
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
images: {
|
|
670
|
+
store: { kv: "redis:8-alpine" },
|
|
671
|
+
// or: "valkey/valkey:8-alpine"
|
|
672
|
+
// or: "docker.dragonflydb.io/dragonflydb/dragonfly"
|
|
673
|
+
},
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
| Facet | `dev` / `prod` | `test` | Runs as |
|
|
677
|
+
| ----- | -------------- | -------- | ---------------------- |
|
|
678
|
+
| `kv` | `redis` | `memory` | container → in-process |
|
|
679
|
+
|
|
680
|
+
| Variable | When |
|
|
681
|
+
| ------------------ | -------------------------------- |
|
|
682
|
+
| `REDIS_URL` | Required for the `redis` driver |
|
|
683
|
+
| `OKE_STORE_KV_URL` | Override if `REDIS_URL` is unset |
|
|
684
|
+
| `OKE_KV_DRIVER` | Force a driver id |
|
|
685
|
+
|
|
686
|
+
Missing Redis URL fails boot loudly: `oke boot: redis driver needs REDIS_URL`.
|
|
687
|
+
|
|
728
688
|
## Files
|
|
729
689
|
|
|
730
690
|
<StoreFacetMark facet="files" />
|
|
731
691
|
|
|
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.
|
|
692
|
+
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
693
|
|
|
736
694
|
### Quick start
|
|
737
695
|
|
|
@@ -829,8 +787,7 @@ images: {
|
|
|
829
787
|
|
|
830
788
|
### Images — `image` / `putImage`
|
|
831
789
|
|
|
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:
|
|
790
|
+
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
791
|
|
|
835
792
|
<StoreFilesVariants />
|
|
836
793
|
|
|
@@ -876,8 +833,7 @@ const result = await fx.store(uploads).putImage("photos/x.jpg", bytes, {
|
|
|
876
833
|
|
|
877
834
|
<StoreFacetMark facet="index" />
|
|
878
835
|
|
|
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.
|
|
836
|
+
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
837
|
|
|
882
838
|
### Quick start
|
|
883
839
|
|
|
@@ -934,8 +890,7 @@ One declaration shape — two search physics. The handle is a **discriminated un
|
|
|
934
890
|
probe. Meilisearch uses full-text `q`.
|
|
935
891
|
</Callout>
|
|
936
892
|
|
|
937
|
-
TypeScript rejects a vector query against a text index (and vice versa) at compile time — never at
|
|
938
|
-
runtime.
|
|
893
|
+
TypeScript rejects a vector query against a text index (and vice versa) at compile time — never at runtime.
|
|
939
894
|
|
|
940
895
|
### Vector ANN
|
|
941
896
|
|
|
@@ -944,16 +899,13 @@ runtime.
|
|
|
944
899
|
| `memory` | — (in-process, default) | Full scan cosine |
|
|
945
900
|
| `pgvector` | `postgres` or `pglite` sql | HNSW + `cosineDistance` |
|
|
946
901
|
|
|
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`.
|
|
902
|
+
**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
903
|
|
|
951
904
|
Pair SQL + index under [Advanced — composing facets together](#advanced--composing-facets-together).
|
|
952
905
|
|
|
953
906
|
### Full-text search — `meilisearch` (opt-in)
|
|
954
907
|
|
|
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.
|
|
908
|
+
`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
909
|
|
|
958
910
|
```typescript
|
|
959
911
|
export const kb = store.index("kb", { description: "Help articles" });
|
|
@@ -1012,9 +964,7 @@ drivers: {
|
|
|
1012
964
|
|
|
1013
965
|
### With AI embeddings
|
|
1014
966
|
|
|
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.
|
|
967
|
+
`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
968
|
|
|
1019
969
|
## Advanced — composing facets together
|
|
1020
970
|
|
|
@@ -1049,84 +999,82 @@ Container images come from the `images` map — change the vendor by changing th
|
|
|
1049
999
|
|
|
1050
1000
|
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
1001
|
|
|
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).
|
|
1002
|
+
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
1003
|
|
|
1056
1004
|
## Troubleshooting
|
|
1057
1005
|
|
|
1058
1006
|
<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
1007
|
|
|
1008
|
+
<Accordion title="Unable to resolve table name from value">
|
|
1009
|
+
A `store.schema.table` column named `name` used to shadow the SQL table name. Inserts and selects
|
|
1010
|
+
now resolve it from the declaration. Keep the `name` column.
|
|
1064
1011
|
</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
1012
|
|
|
1013
|
+
<Accordion title="OKE1101 — missing table in prod">
|
|
1014
|
+
Schema DDL never runs automatically in `prod`. Under `oke dev`, `db.autoPush` (default `true`)
|
|
1015
|
+
runs `oke db push`. For production use `oke db generate` + `oke db migrate`.
|
|
1069
1016
|
</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
1017
|
|
|
1018
|
+
<Accordion title="oke db push: missing_hints">
|
|
1019
|
+
New tables are created automatically. Push only manages `public` — it will not drop schema `oke`
|
|
1020
|
+
(RLS helpers) or `oke_console`. Remaining `missing_hints` are destructive (drop a non-empty public
|
|
1021
|
+
table, type change). Review with `oke db generate`.
|
|
1074
1022
|
</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
1023
|
|
|
1024
|
+
<Accordion title="I need a join — with: is not supported">
|
|
1025
|
+
`fx.store` is one table per call by design. Read each table separately and compose in the flow (or
|
|
1026
|
+
extract a shared flow and `fx.call` it). Every table then appears explicitly in the Manifest's
|
|
1027
|
+
effect graph — which is what powers cache invalidation and PII masking.
|
|
1079
1028
|
</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
1029
|
|
|
1030
|
+
<Accordion title="Pagination shows duplicates when rows are inserted">
|
|
1031
|
+
You are on offset paging. Switch `list` to keyset by setting `cursor` columns with a stable order
|
|
1032
|
+
(e.g. `[createdAt, id]`) — the page boundary becomes a row predicate, not a row count.
|
|
1084
1033
|
</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
1034
|
|
|
1035
|
+
<Accordion title="meta.total is slow on a big table">
|
|
1036
|
+
`count: "exact"` runs `COUNT(*)` per page. Set `count: "none"` in the `list` options to skip it,
|
|
1037
|
+
or use keyset mode where totals are rarely needed.
|
|
1089
1038
|
</Accordion>
|
|
1090
|
-
<Accordion title="List data is { items, count } instead of an array">
|
|
1091
1039
|
|
|
1040
|
+
<Accordion title="List data is { items, count } instead of an array">
|
|
1092
1041
|
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
1042
|
</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
1043
|
|
|
1044
|
+
<Accordion title="KV TTL never expires in tests">
|
|
1045
|
+
The `test` default is `memory`, which ignores TTL on `set`. Under `oke dev`, KV is `redis` by
|
|
1046
|
+
default — use that when you need real expiry.
|
|
1099
1047
|
</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
1048
|
|
|
1049
|
+
<Accordion title="oke boot: redis / meilisearch / S3 URL missing">
|
|
1050
|
+
Driver-backed facets fail boot when their URL is absent — Redis needs `REDIS_URL`, meilisearch
|
|
1051
|
+
needs `OKE_STORE_INDEX_URL`, S3 needs `S3_BUCKET`. `oke dev` writes these into the Compose env.
|
|
1104
1052
|
</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
1053
|
|
|
1054
|
+
<Accordion title="vector dims X !== index dims Y">
|
|
1055
|
+
The vector length must match `store.index(…, {dims})` (runtime default `3` if unset). Align `dims`
|
|
1056
|
+
with your embedder, or fix the vector you pass to `upsert` / `search`.
|
|
1109
1057
|
</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
1058
|
|
|
1059
|
+
<Accordion title="Index stays on memory under oke dev">
|
|
1060
|
+
Unlike KV/Files, unset `drivers.store.index` does **not** promote to `pgvector` in `dev` — it
|
|
1061
|
+
stays `memory`. Set the map explicitly (and pair SQL for `pgvector`).
|
|
1114
1062
|
</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
1063
|
|
|
1064
|
+
<Accordion title="Console warns Non-ASCII object key">
|
|
1065
|
+
`fx.store(files).put` stores the key as-is. Console uploads mint an ASCII key and keep the
|
|
1066
|
+
original name in `.oke/catalog.json`. Re-upload to rewrite an existing object.
|
|
1119
1067
|
</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
1068
|
|
|
1069
|
+
<Accordion title="oke dev skipped the seed prompt">
|
|
1070
|
+
`.oke/state.json` stores `seededAt` plus this app's `defineSeed({name})` — notes vs keel are
|
|
1071
|
+
different ids. A new name re-asks. Clear those keys to prompt again.
|
|
1124
1072
|
</Accordion>
|
|
1125
|
-
<Accordion title="Console KV shows No rows after oke db seed">
|
|
1126
1073
|
|
|
1074
|
+
<Accordion title="Console KV shows No rows after oke db seed">
|
|
1127
1075
|
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
1076
|
</Accordion>
|
|
1077
|
+
|
|
1130
1078
|
</Accordions>
|
|
1131
1079
|
|
|
1132
1080
|
## Learn more
|