okengine 0.18.5 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +18 -10
- package/README.md +2 -2
- package/package.json +51 -50
- package/site/content/docs/ai/llms-txt.mdx +1 -1
- package/site/content/docs/ai/meta.json +1 -1
- package/site/content/docs/ai/skills.mdx +4 -1
- package/site/content/docs/ai/try-it.mdx +58 -0
- package/site/content/docs/client/auth.mdx +208 -0
- package/site/content/docs/client/calling.mdx +451 -0
- package/site/content/docs/client/index.mdx +209 -0
- package/site/content/docs/client/live.mdx +234 -0
- package/site/content/docs/client/meta.json +5 -0
- package/site/content/docs/client/react.mdx +249 -0
- package/site/content/docs/elements/ai/agents.mdx +232 -0
- package/site/content/docs/elements/ai/index.mdx +341 -0
- package/site/content/docs/elements/ai/mcp.mdx +275 -0
- package/site/content/docs/elements/ai/meta.json +5 -0
- package/site/content/docs/elements/ai/models.mdx +283 -0
- package/site/content/docs/elements/ai/prompts.mdx +256 -0
- package/site/content/docs/elements/channel/email.mdx +266 -0
- package/site/content/docs/elements/channel/index.mdx +344 -0
- package/site/content/docs/elements/channel/meta.json +5 -0
- package/site/content/docs/elements/channel/push.mdx +218 -0
- package/site/content/docs/elements/channel/receipts.mdx +206 -0
- package/site/content/docs/elements/channel/sms.mdx +264 -0
- package/site/content/docs/elements/channel/whatsapp.mdx +220 -0
- package/site/content/docs/elements/clock/index.mdx +300 -0
- package/site/content/docs/elements/clock/meta.json +5 -0
- package/site/content/docs/elements/clock/schedules.mdx +508 -0
- package/site/content/docs/elements/clock/sleep.mdx +445 -0
- package/site/content/docs/elements/flow/consumers.mdx +707 -0
- package/site/content/docs/elements/flow/http.mdx +1080 -0
- package/site/content/docs/elements/flow/index.mdx +618 -0
- package/site/content/docs/elements/flow/meta.json +5 -0
- package/site/content/docs/elements/flow/routing.mdx +559 -0
- package/site/content/docs/elements/flow/workflows.mdx +640 -0
- package/site/content/docs/elements/gate/auth.mdx +371 -0
- package/site/content/docs/elements/gate/authorization.mdx +298 -0
- package/site/content/docs/elements/gate/index.mdx +418 -0
- package/site/content/docs/elements/gate/meta.json +5 -0
- package/site/content/docs/elements/gate/rate-limits.mdx +365 -0
- package/site/content/docs/elements/gate/rls.mdx +303 -0
- package/site/content/docs/elements/gate/tenancy.mdx +372 -0
- package/site/content/docs/elements/index.mdx +20 -10
- package/site/content/docs/elements/signal/broadcast.mdx +517 -0
- package/site/content/docs/elements/signal/index.mdx +352 -0
- package/site/content/docs/elements/signal/live.mdx +590 -0
- package/site/content/docs/elements/signal/meta.json +5 -0
- package/site/content/docs/elements/signal/once.mdx +596 -0
- package/site/content/docs/elements/store/files.mdx +659 -0
- package/site/content/docs/elements/store/index.mdx +310 -0
- package/site/content/docs/elements/store/kv.mdx +629 -0
- package/site/content/docs/elements/store/meta.json +5 -0
- package/site/content/docs/elements/store/search.mdx +946 -0
- package/site/content/docs/elements/store/sql.mdx +937 -0
- package/site/content/docs/elements/vault/config.mdx +256 -0
- package/site/content/docs/elements/vault/index.mdx +356 -0
- package/site/content/docs/elements/vault/meta.json +5 -0
- package/site/content/docs/elements/vault/rotation.mdx +273 -0
- package/site/content/docs/elements/vault/secrets.mdx +285 -0
- package/site/content/docs/index.mdx +63 -36
- package/site/content/docs/meta.json +8 -7
- package/site/content/docs/plugins/cors.mdx +2 -1
- package/site/content/docs/plugins/csrf.mdx +4 -1
- package/site/content/docs/plugins/headers.mdx +2 -1
- package/site/content/docs/plugins/ip-allowlist.mdx +2 -1
- package/site/content/docs/plugins/magic-link.mdx +6 -0
- package/site/content/docs/plugins/maintenance-mode.mdx +2 -1
- package/site/content/docs/plugins/oauth.mdx +1 -1
- package/site/content/docs/plugins/otp.mdx +12 -1
- package/site/content/docs/plugins/passkey.mdx +39 -15
- package/site/content/docs/plugins/two-factor.mdx +91 -34
- package/site/content/docs/plugins/username.mdx +1 -1
- package/site/content/docs/recipes/caddy.mdx +3 -3
- package/site/content/docs/recipes/index.mdx +18 -28
- package/site/content/docs/recipes/mailpit.mdx +1 -1
- package/site/content/docs/recipes/meilisearch.mdx +3 -3
- package/site/content/docs/recipes/meta.json +1 -4
- package/site/content/docs/recipes/nginx.mdx +2 -2
- package/site/content/docs/recipes/openrouter.mdx +283 -0
- package/site/content/docs/recipes/pgdog.mdx +3 -3
- package/site/content/docs/recipes/postgres.mdx +1 -2
- package/site/content/docs/recipes/rustfs.mdx +2 -2
- package/site/content/docs/recipes/traefik.mdx +3 -3
- package/site/content/docs/reference/cli.mdx +231 -0
- package/site/content/docs/reference/configuration.mdx +30 -10
- package/site/content/docs/reference/environment-variables.mdx +93 -20
- package/site/content/docs/reference/errors.mdx +132 -29
- package/site/content/docs/reference/fx.mdx +137 -73
- package/site/content/docs/reference/index.mdx +13 -21
- package/site/content/docs/reference/meta.json +4 -5
- package/site/content/docs/reference/okid.mdx +42 -17
- package/site/content/docs/reference/plugins.mdx +4 -3
- package/site/content/docs/reference/security.mdx +197 -0
- package/site/content/docs/understand/meta.json +5 -0
- package/site/content/docs/understand/the-anatomy.mdx +132 -0
- package/site/content/docs/understand/the-model.mdx +32 -0
- package/site/content/docs/understand/the-problem.mdx +74 -0
- package/site/content/docs/understand/the-vocabulary.mdx +26 -0
- package/src/auth/api-keys.ts +2 -1
- package/src/auth/bindings.ts +64 -16
- package/src/auth/gate-auth.test.ts +6 -1
- package/src/auth/identity.ts +152 -5
- package/src/auth/index.ts +29 -0
- package/src/auth/invites.ts +2 -1
- package/src/auth/method-context.ts +41 -0
- package/src/auth/oauth-as/crypto.ts +2 -1
- package/src/auth/oauth-as/stores.ts +2 -1
- package/src/auth/operator.ts +2 -1
- package/src/auth/sessions-jwt.test.ts +77 -0
- package/src/auth/sessions.ts +20 -4
- package/src/auth/tenants.ts +3 -2
- package/src/auth/two-factor-challenge.test.ts +61 -0
- package/src/auth/two-factor-challenge.ts +284 -0
- package/src/auth/verification.ts +5 -0
- package/src/bench/REPORT.md +49 -0
- package/src/bench/g03-signal-once.bench.ts +1 -1
- package/src/bench/g10-observability-contention.bench.ts +1 -1
- package/src/bench/g17-hybrid-search.bench.ts +572 -0
- package/src/bench/load-app.ts +2 -10
- package/src/cli/ai-setup/ai-setup.test.ts +331 -42
- package/src/cli/ai-setup/apply.ts +294 -52
- package/src/cli/ai-setup/catalog.ts +238 -1342
- package/src/cli/ai-setup/index.ts +14 -99
- package/src/cli/ai-setup/prompts.ts +60 -443
- package/src/cli/ask-seed.test.ts +96 -0
- package/src/cli/ask-seed.ts +82 -0
- package/src/cli/ask-vault-gaps.test.ts +84 -0
- package/src/cli/ask-vault-gaps.ts +146 -0
- package/src/cli/build.ts +2 -2
- package/src/cli/client-add.test.ts +26 -1
- package/src/cli/client-add.ts +106 -27
- package/src/cli/db-seed.ts +2 -0
- package/src/cli/db.ts +150 -11
- package/src/cli/dev.ts +119 -211
- package/src/cli/docker-clean.ts +2 -2
- package/src/cli/docker-cli.test.ts +1 -1
- package/src/cli/doctor-diff.ts +4 -2
- package/src/cli/doctor-pii.test.ts +1 -1
- package/src/cli/doctor.ts +14 -1
- package/src/cli/hero-meta.test.ts +4 -2
- package/src/cli/load-config.images.test.ts +8 -10
- package/src/cli/load-config.ts +1 -1
- package/src/cli/registry.ts +22 -10
- package/src/cli/replay.ts +3 -1
- package/src/cli/start.ts +1 -1
- package/src/cli/tui/keys.ts +2 -3
- package/src/client/auth/cookies.ts +74 -0
- package/src/client/auth/create-auth-client.ts +636 -0
- package/src/client/auth/denials.ts +99 -0
- package/src/client/auth/session.ts +283 -0
- package/src/client/auth.test.ts +251 -0
- package/src/client/auth.ts +39 -114
- package/src/client/create-with-session.ts +245 -0
- package/src/client/create.ts +51 -5
- package/src/client/index.ts +11 -1
- package/src/client/live.ts +11 -79
- package/src/client/notes-contract.test.ts +5 -10
- package/src/client/sse.ts +137 -0
- package/src/client/stream.ts +147 -0
- package/src/client/transport.test.ts +32 -0
- package/src/client/transport.ts +123 -26
- package/src/client/types.ts +118 -14
- package/src/client-react/index.ts +185 -24
- package/src/client-react/use-live-query.ts +13 -3
- package/src/compiler/aot.test.ts +7 -4
- package/src/compiler/effects-embed.test.ts +56 -0
- package/src/compiler/effects-fetch.test.ts +43 -0
- package/src/compiler/effects-infer.ts +58 -2
- package/src/compiler/extract.test.ts +257 -43
- package/src/compiler/extract.ts +566 -48
- package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +7 -4
- package/src/compiler/fixtures/skyport/src/flows/bookings/signals.ts +2 -8
- package/src/compiler/fixtures/skyport.expected.json +4 -4
- package/src/compiler/fixtures/triggers/five-triggers.ts +14 -10
- package/src/compiler/response.ts +2 -2
- package/src/compiler/search-writer-isolation.test.ts +40 -0
- package/src/config/index.ts +11 -0
- package/src/console/server/app.ts +17 -23
- package/src/console/server/bind.ts +4 -0
- package/src/console/server/console.test.ts +2 -2
- package/src/console/server/flows-invoke.test.ts +50 -34
- package/src/console/server/flows.ts +436 -255
- package/src/console/server/runs-ingest.test.ts +6 -3
- package/src/console/server/serve.ts +1 -1
- package/src/console/server/signals.test.ts +1 -5
- package/src/console/server/signals.ts +8 -6
- package/src/console/server/state.ts +6 -1
- package/src/console/server/store.ts +1 -1
- package/src/console/ui-next/dist/assets/FileExportIcon-Ck-5od4R.js +1 -0
- package/src/console/ui-next/dist/assets/MoreHorizontalCircle01Icon-gMNGsE37.js +1 -0
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +1 -0
- package/src/console/ui-next/dist/assets/UnavailableIcon-D9cvHVPr.js +1 -0
- package/src/console/ui-next/dist/assets/UserIcon-DaE2PB5_.js +1 -0
- package/src/console/ui-next/dist/assets/access-page-DFeymU07.js +4 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-U1rdfblp.js +1 -0
- package/src/console/ui-next/dist/assets/cache-glyph-BeFJeqBG.js +1 -0
- package/src/console/ui-next/dist/assets/call-pii-button-CVAONPii.js +1 -0
- package/src/console/ui-next/dist/assets/collapsible-D2A6NJ-3.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +1 -0
- package/src/console/ui-next/dist/assets/dagre.esm-B1_XeuLP.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-Cgk_h5ja.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-Dss7941e.js +1 -0
- package/src/console/ui-next/dist/assets/highlighted-json-MYZQtRnw.js +154 -0
- package/src/console/ui-next/dist/assets/http-method-Jrh39p7A.js +1 -0
- package/src/console/ui-next/dist/assets/index-D0zS5rKO.css +2 -0
- package/src/console/ui-next/dist/assets/index-DXP2dBIF.js +63 -0
- package/src/console/ui-next/dist/assets/observability-page-HvolXxTI.js +4 -0
- package/src/console/ui-next/dist/assets/react-dom-Ddte4I-Q.js +9 -0
- package/src/console/ui-next/dist/assets/replica-lag-CSh2dzrb.js +18 -0
- package/src/console/ui-next/dist/assets/request-meta-BatF8KrK.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +1 -0
- package/src/console/ui-next/dist/assets/{sql-BskegiFM.js → sql-BsFa4tDR.js} +1 -1
- package/src/console/ui-next/dist/assets/store-page-eiKiHnNe.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CZkMeKS-.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CW8y5A2h.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-B_RWJrEO.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-uk4WVboC.js → use-vault-list-CT4-gajj.js} +1 -1
- package/src/console/ui-next/dist/assets/vault-page-CWrg-A68.js +2 -0
- package/src/console/ui-next/dist/assets/xyflow-CSyC6ryz.css +1 -0
- package/src/console/ui-next/dist/assets/xyflow-yApv7D4e.js +7 -0
- package/src/console/ui-next/dist/index.html +6 -13
- package/src/console/ui-next/seed-invoke-host.ts +21 -18
- package/src/console/ui-next/src/client.ts +7 -2
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +2 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +12 -2
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +4 -0
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +8 -0
- package/src/console/ui-next/src/features/flows/traces/waterfall-bars.ts +4 -0
- package/src/console/ui-next/src/features/flows/traces/waterfall-tooltip.ts +9 -3
- package/src/console/ui-next/src/features/store/detail/reveal-cell.tsx +7 -11
- package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +2 -2
- package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +2 -0
- package/src/console/ui-next/src/features/store/lib/grid-model.test.ts +25 -1
- package/src/console/ui-next/src/features/store/lib/grid-model.ts +36 -1
- package/src/console/ui-next/src/features/store/query/query-results.tsx +3 -3
- package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +2 -0
- package/src/console/ui-next/src/features/units/lib/call-read-safe.ts +3 -1
- package/src/console/ui-next/src/features/units/lib/fields-from-schema.ts +6 -0
- package/src/docker/ai-model-status.test.ts +1 -12
- package/src/docker/ai-model-status.ts +10 -38
- package/src/docker/compose-up.test.ts +100 -0
- package/src/docker/compose-up.ts +292 -0
- package/src/docker/compose.ts +4 -14
- package/src/docker/derive.ts +9 -34
- package/src/docker/docker.test.ts +16 -261
- package/src/docker/images-config.test.ts +13 -25
- package/src/docker/index.ts +1 -24
- package/src/docker/recipes/index.ts +0 -23
- package/src/docker/recipes/pgdog.ts +6 -4
- package/src/docker/stack-id.test.ts +2 -2
- package/src/docker/stack-id.ts +0 -1
- package/src/docker/types.ts +2 -2
- package/src/drivers/ai-anthropic.ts +10 -0
- package/src/drivers/ai-openai-compatible.ts +34 -7
- package/src/drivers/ai-providers.test.ts +0 -107
- package/src/drivers/ai-stream.test.ts +0 -34
- package/src/drivers/ai-types.ts +17 -8
- package/src/drivers/channel-fcm.ts +6 -1
- package/src/drivers/channel-msegat.ts +6 -1
- package/src/drivers/channel-resend.ts +5 -1
- package/src/drivers/channel-smtp.ts +5 -1
- package/src/drivers/channel-sndr.ts +10 -1
- package/src/drivers/channel-taqnyat-mail.ts +5 -1
- package/src/drivers/channel-taqnyat-whatsapp.ts +6 -1
- package/src/drivers/channel-taqnyat.ts +6 -1
- package/src/drivers/channel-types.ts +10 -0
- package/src/drivers/channel-unifonic.ts +6 -1
- package/src/drivers/channel-wa-cloud.ts +6 -1
- package/src/drivers/channel-webpush.ts +6 -1
- package/src/drivers/external.ts +33 -0
- package/src/drivers/index.ts +0 -10
- package/src/drivers/meilisearch.ts +6 -0
- package/src/drivers/oauth-types.ts +4 -0
- package/src/drivers/postgres.test.ts +74 -3
- package/src/drivers/postgres.ts +175 -6
- package/src/drivers/signal-types.ts +5 -5
- package/src/drivers/types.ts +7 -0
- package/src/drivers/vault-types.ts +5 -0
- package/src/elements/ai/declare.ts +17 -4
- package/src/elements/ai/eval.ts +1 -1
- package/src/elements/ai/mcp-http.ts +1 -1
- package/src/elements/ai/pii.ts +1 -1
- package/src/elements/ai/providers.test.ts +289 -0
- package/src/elements/ai/providers.ts +169 -0
- package/src/elements/ai/runtime.ts +68 -5
- package/src/elements/ai/schema.ts +2 -2
- package/src/elements/ai.test.ts +7 -7
- package/src/elements/ai.ts +17 -1
- package/src/elements/channel/runtime.ts +105 -12
- package/src/elements/clock/cron-fields.test.ts +144 -0
- package/src/elements/clock/cron-fields.ts +185 -0
- package/src/elements/clock/declare.ts +247 -3
- package/src/elements/clock.ts +21 -1
- package/src/elements/index.ts +15 -0
- package/src/elements/signal/chaos-child.ts +1 -2
- package/src/elements/signal/declare.ts +65 -37
- package/src/elements/signal/delivery-modes.test.ts +11 -31
- package/src/elements/signal/dry-run-replay.test.ts +1 -7
- package/src/elements/signal/dry-run-write-isolation.test.ts +1 -7
- package/src/elements/signal/key-ordering.test.ts +6 -26
- package/src/elements/signal/lease-reclaim.test.ts +2 -6
- package/src/elements/signal/optional-emit.test.ts +6 -9
- package/src/elements/signal/order-lifecycle.test.ts +4 -17
- package/src/elements/signal/orphan-messages.test.ts +4 -15
- package/src/elements/signal/reconcile.test.ts +2 -2
- package/src/elements/signal/runtime.ts +1 -1
- package/src/elements/signal/schema-emit.test.ts +4 -8
- package/src/elements/signal.test.ts +24 -49
- package/src/elements/signal.ts +9 -2
- package/src/elements/store/domain-ddl.test.ts +1 -1
- package/src/elements/store/live-http.test.ts +9 -4
- package/src/elements/store/prepare-row.test.ts +123 -0
- package/src/elements/store/resource.ts +43 -12
- package/src/elements/store/schema-decl.ts +117 -0
- package/src/elements/store/search-backfill.ts +205 -0
- package/src/elements/store/search-bind.ts +66 -0
- package/src/elements/store/search-bm25.ts +78 -0
- package/src/elements/store/search-ddl.ts +156 -0
- package/src/elements/store/search-embed-flow.ts +141 -0
- package/src/elements/store/search-errors.ts +34 -0
- package/src/elements/store/search-fusion.ts +112 -0
- package/src/elements/store/search-lsh.ts +179 -0
- package/src/elements/store/search-runtime.ts +300 -0
- package/src/elements/store/search.test.ts +144 -0
- package/src/elements/store/sql-session.test.ts +1 -0
- package/src/elements/store/sql-session.ts +83 -3
- package/src/elements/store/table.ts +23 -2
- package/src/elements/store/upsert-app.test.ts +1 -3
- package/src/elements/store.ts +50 -0
- package/src/full.ts +4 -2
- package/src/http.ts +4 -1
- package/src/i18n/catalogs/ar.ts +14 -6
- package/src/i18n/catalogs/en.ts +14 -6
- package/src/index.ts +6 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +7 -7
- package/src/kernel/adopt-routes.ts +9 -1
- package/src/kernel/app-auth.ts +5 -0
- package/src/kernel/app.ts +102 -9
- package/src/kernel/auto-cache.test.ts +6 -12
- package/src/kernel/auto-registry.test.ts +3 -3
- package/src/kernel/boot-bind/ai.test.ts +22 -19
- package/src/kernel/boot-bind/ai.ts +5 -43
- package/src/kernel/boot-bind/clock.ts +23 -5
- package/src/kernel/boot-bind/honor-config.test.ts +4 -4
- package/src/kernel/boot.test.ts +11 -6
- package/src/kernel/boot.ts +66 -16
- package/src/kernel/boundary-contract.ts +93 -0
- package/src/kernel/budget.test.ts +1 -1
- package/src/kernel/call.ts +89 -0
- package/src/kernel/capability.ts +6 -0
- package/src/kernel/client-descriptor.ts +123 -0
- package/src/kernel/clock-timezone.test.ts +80 -0
- package/src/kernel/correlation.test.ts +1 -1
- package/src/kernel/dry-run.ts +11 -8
- package/src/kernel/effects-stamping.test.ts +42 -9
- package/src/kernel/effects.test.ts +4 -2
- package/src/kernel/effects.ts +51 -9
- package/src/kernel/errors-channel.ts +17 -0
- package/src/kernel/errors-live-resume.ts +3 -2
- package/src/kernel/errors-tenant.ts +7 -4
- package/src/kernel/errors.registry-helpers.ts +96 -0
- package/src/kernel/errors.registry.test.ts +139 -25
- package/src/kernel/errors.ts +102 -23
- package/src/kernel/external-effects.test.ts +196 -0
- package/src/kernel/flow.test.ts +13 -4
- package/src/kernel/flow.ts +69 -71
- package/src/kernel/fx-ask-telemetry.test.ts +2 -2
- package/src/kernel/fx-dead-letters.test.ts +3 -3
- package/src/kernel/fx-fetch.ts +55 -0
- package/src/kernel/fx-live-stream.ts +2 -2
- package/src/kernel/fx-live.test.ts +8 -8
- package/src/kernel/fx.test.ts +17 -11
- package/src/kernel/fx.ts +180 -57
- package/src/kernel/horizontal-child.ts +1 -1
- package/src/kernel/http-query.test.ts +3 -6
- package/src/kernel/index.ts +13 -3
- package/src/kernel/instance-id.ts +1 -1
- package/src/kernel/instances.test.ts +40 -0
- package/src/kernel/instances.ts +47 -9
- package/src/kernel/live-http.test.ts +6 -7
- package/src/kernel/live-http.ts +6 -2
- package/src/kernel/live-resume.test.ts +1 -1
- package/src/kernel/mcp-tool.test.ts +3 -3
- package/src/kernel/on.ts +98 -4
- package/src/kernel/pipeline.test.ts +12 -15
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/stamp-http.test.ts +3 -3
- package/src/kernel/triggers.ts +210 -95
- package/src/kernel-entry.ts +0 -1
- package/src/manifest/diff.ts +11 -1
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/types.ts +46 -2
- package/src/mcp/docs-index.ts +3 -3
- package/src/mcp/docs-mcp.test.ts +2 -2
- package/src/mcp/docs-tools.ts +1 -1
- package/src/okid.test.ts +56 -0
- package/src/okid.ts +55 -11
- package/src/plugins/anonymous.ts +8 -4
- package/src/plugins/auth/shared.ts +27 -3
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/auth-methods.security.test.ts +561 -23
- package/src/plugins/config-source.ts +1 -1
- package/src/plugins/csrf.test.ts +65 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/magic-link.ts +36 -16
- package/src/plugins/oauth/shared.ts +3 -1
- package/src/plugins/oauth.ts +22 -14
- package/src/plugins/otp.ts +37 -21
- package/src/plugins/passkey-webauthn.ts +9 -6
- package/src/plugins/passkey.ts +98 -26
- package/src/plugins/pre-account-hijack.test.ts +223 -0
- package/src/plugins/two-factor.ts +466 -37
- package/src/plugins/username.ts +19 -7
- package/src/release/build-lib.ts +1 -0
- package/src/release/limits.ts +2 -2
- package/src/release/measure.ts +1 -1
- package/src/runs/collect.test.ts +0 -2
- package/src/runtime/json-code-block.test.ts +250 -20
- package/src/runtime/json-code-block.ts +1261 -41
- package/src/term.test.ts +1 -1
- package/src/test/create-test-app.test.ts +12 -12
- package/src/test/live-signals.test.ts +6 -6
- package/src/test/provisions.integration.test.ts +10 -14
- package/src/test/reset-element-registries.ts +1 -1
- package/src/test/tenant-isolation.test.ts +12 -6
- package/site/content/docs/deployment/docker-swarm.mdx +0 -164
- package/site/content/docs/deployment/docker.mdx +0 -227
- package/site/content/docs/deployment/index.mdx +0 -83
- package/site/content/docs/deployment/kubernetes.mdx +0 -176
- package/site/content/docs/deployment/meta.json +0 -5
- package/site/content/docs/deployment/reverse-proxy.mdx +0 -234
- package/site/content/docs/elements/ai.mdx +0 -385
- package/site/content/docs/elements/channel.mdx +0 -346
- package/site/content/docs/elements/clock.mdx +0 -244
- package/site/content/docs/elements/flow.mdx +0 -420
- package/site/content/docs/elements/gate.mdx +0 -436
- package/site/content/docs/elements/signal.mdx +0 -380
- package/site/content/docs/elements/store.mdx +0 -1099
- package/site/content/docs/elements/vault.mdx +0 -405
- package/site/content/docs/get-started/basic-usage.mdx +0 -173
- package/site/content/docs/get-started/index.mdx +0 -43
- package/site/content/docs/get-started/installation.mdx +0 -220
- package/site/content/docs/get-started/introduction.mdx +0 -144
- package/site/content/docs/get-started/meta.json +0 -13
- package/site/content/docs/get-started/project-structure.mdx +0 -925
- package/site/content/docs/get-started/testing.mdx +0 -328
- package/site/content/docs/get-started/why.mdx +0 -114
- package/site/content/docs/recipes/llama-cpp.mdx +0 -151
- package/site/content/docs/recipes/ollama.mdx +0 -142
- package/site/content/docs/recipes/sglang.mdx +0 -105
- package/site/content/docs/recipes/vllm.mdx +0 -106
- package/site/content/docs/reference/cli.md +0 -232
- package/site/content/docs/reference/client.mdx +0 -469
- package/site/content/docs/reference/security.md +0 -74
- package/src/cli/ai-setup/detect-ollama.ts +0 -213
- package/src/cli/ai-setup/recommend.test.ts +0 -225
- package/src/cli/ai-setup/recommend.ts +0 -225
- package/src/cli/dev-controls.test.ts +0 -122
- package/src/cli/dev-controls.ts +0 -164
- package/src/cli/tui/DevLive.tsx +0 -126
- package/src/console/ui-next/dist/assets/access-page-3-EFj-2G.js +0 -4
- package/src/console/ui-next/dist/assets/agent-disclosure-BP0Y0Sux.js +0 -1
- package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +0 -1
- package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +0 -1
- package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +0 -1
- package/src/console/ui-next/dist/assets/dagre.esm-ZwcdTuZZ.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-sC3lGABz.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-cVFnA4HH.js +0 -1
- package/src/console/ui-next/dist/assets/highlighted-json-Awq7gYdu.js +0 -154
- package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +0 -1
- package/src/console/ui-next/dist/assets/index-CMIgUbD0.js +0 -66
- package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +0 -2
- package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +0 -1
- package/src/console/ui-next/dist/assets/observability-page-CQ3p34ip.js +0 -4
- package/src/console/ui-next/dist/assets/preload-helper-oH4irX4C.js +0 -1
- package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +0 -1
- package/src/console/ui-next/dist/assets/react-dom-Bph1y7z7.js +0 -9
- package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +0 -18
- package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-02xOiqIK.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-l8FeKfnP.js +0 -1
- package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +0 -1
- package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-CL-d_mLE.js +0 -2
- package/src/console/ui-next/dist/assets/xyflow-D7n4g6go.js +0 -7
- package/src/console/ui-next/dist/assets/xyflow-DZ0Ws1xk.css +0 -1
- package/src/docker/ollama-pull.ts +0 -232
- package/src/docker/recipes/llama-cpp.ts +0 -298
- package/src/docker/recipes/ollama.ts +0 -44
- package/src/docker/recipes/sglang.ts +0 -55
- package/src/docker/recipes/vllm.ts +0 -44
- package/src/drivers/ai-ollama-tools.integration.test.ts +0 -109
- package/src/drivers/ai-ollama.integration.test.ts +0 -184
- package/src/drivers/ai-ollama.ts +0 -389
- package/src/drivers/ollama.ts +0 -14
|
@@ -1,1099 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: "Store"
|
|
3
|
-
description: "Data at rest behind one handle and four independent facets — sql · kv · files · index — swapped per environment by driver."
|
|
4
|
-
icon: "Database"
|
|
5
|
-
source: "docs/spec/unified-theory.md"
|
|
6
|
-
---
|
|
7
|
-
|
|
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).
|
|
9
|
-
|
|
10
|
-
<Callout title="The one rule">
|
|
11
|
-
Drivers are named after **protocols**, not vendors (`postgres`, `redis`, `s3` — never `neon` or
|
|
12
|
-
`minio`). Vendor choice lives in the `images` map of `oke.config.ts`.
|
|
13
|
-
</Callout>
|
|
14
|
-
|
|
15
|
-
<StoreFacets />
|
|
16
|
-
|
|
17
|
-
## Quick start
|
|
18
|
-
|
|
19
|
-
<Steps>
|
|
20
|
-
|
|
21
|
-
<Step>
|
|
22
|
-
### Declare a table
|
|
23
|
-
|
|
24
|
-
In `src/schema.decl.ts`, describe your tables with plain field builders — no ORM syntax:
|
|
25
|
-
|
|
26
|
-
```typescript title="src/schema.decl.ts"
|
|
27
|
-
import { store, field } from "okengine";
|
|
28
|
-
|
|
29
|
-
export const notes = store.schema.table("notes", {
|
|
30
|
-
id: field.id().primaryKey(),
|
|
31
|
-
title: field.text().notNull(),
|
|
32
|
-
body: field.text().notNull(),
|
|
33
|
-
createdAt: field.integer().notNull().now(),
|
|
34
|
-
});
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
</Step>
|
|
38
|
-
|
|
39
|
-
<Step>
|
|
40
|
-
### Declare the store and push the schema
|
|
41
|
-
|
|
42
|
-
Bind the schema to a SQL store in `src/core.ts`. `oke dev` auto-pushes on save — or run it by hand:
|
|
43
|
-
|
|
44
|
-
```typescript title="src/core.ts"
|
|
45
|
-
import { store } from "okengine";
|
|
46
|
-
import { notes } from "./schema.decl";
|
|
47
|
-
|
|
48
|
-
export const db = store.sql("notes", { schema: { notes } });
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
oke db push # dev — applies the schema to your local database
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
</Step>
|
|
56
|
-
|
|
57
|
-
<Step>
|
|
58
|
-
### Read and write in a Flow
|
|
59
|
-
|
|
60
|
-
All data access goes through `fx.store(db)` — a typed session, one table at a time:
|
|
61
|
-
|
|
62
|
-
```typescript title="src/flows/notes/create.ts"
|
|
63
|
-
export const createNote = on(
|
|
64
|
-
http.post("/notes"),
|
|
65
|
-
flow("notes.create", {
|
|
66
|
-
in: z.object({ title: z.string(), body: z.string() }),
|
|
67
|
-
out: z.object({ id: z.string() }),
|
|
68
|
-
do: async (input, fx) => {
|
|
69
|
-
const id = fx.id();
|
|
70
|
-
await fx
|
|
71
|
-
.store(db)
|
|
72
|
-
.insert(notes)
|
|
73
|
-
.values({ id, ...input, createdAt: Date.now() });
|
|
74
|
-
return { id };
|
|
75
|
-
},
|
|
76
|
-
}),
|
|
77
|
-
);
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
</Step>
|
|
81
|
-
|
|
82
|
-
</Steps>
|
|
83
|
-
|
|
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`
|
|
91
|
-
|
|
92
|
-
Five conventional endpoints (list, create, get, update, remove) expand from one declaration. Each is an ordinary Flow underneath — same contracts, same `fx`:
|
|
93
|
-
|
|
94
|
-
```typescript
|
|
95
|
-
const notesR = store.resource(db, notes, {
|
|
96
|
-
in: NewNote, // create/update input schema
|
|
97
|
-
out: Note, // response schema
|
|
98
|
-
update: NewNote.partial(),
|
|
99
|
-
list: {
|
|
100
|
-
cursor: [notes.createdAt, notes.id], // keyset pagination columns
|
|
101
|
-
direction: "desc",
|
|
102
|
-
search: [notes.title], // ?search= / ?q=
|
|
103
|
-
filter: "all", // ?col=op.value — "all" | Column[] | "none"
|
|
104
|
-
order: "all", // ?order=col.desc
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
const mounted = on(http.resource("/notes", notesR.all()).public());
|
|
109
|
-
```
|
|
110
|
-
|
|
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.
|
|
112
|
-
|
|
113
|
-
The list endpoint's URL is the whole query language:
|
|
114
|
-
|
|
115
|
-
| Param | Meaning | Example |
|
|
116
|
-
| ----------------------------------- | --------------------------------------------------------- | ------------------------------------- |
|
|
117
|
-
| `?cursor=` / `?offset=` / `?limit=` | paginate (keyset when `cursor` columns are set) | `?limit=20&cursor=eyJ…` |
|
|
118
|
-
| `?search=` (`?q=`) | substring match over `search` columns | `?q=invoice` |
|
|
119
|
-
| `?col=op.value` | filter — `eq neq gt gte lt lte like ilike in is` + `not.` | `?title=like.*draft*` |
|
|
120
|
-
| `?or=(…)` / `?and=(…)` | grouped filters, nested `not.and` / `not.or` | `?or=(a.eq.1,not.and(b.gt.2,c.lt.9))` |
|
|
121
|
-
| `?order=` | sort (`col.desc`, comma-separated) | `?order=createdAt.desc` |
|
|
122
|
-
| `?select=` | project columns | `?select=id,title` |
|
|
123
|
-
|
|
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.
|
|
125
|
-
|
|
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:
|
|
135
|
-
|
|
136
|
-
```typescript
|
|
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
|
-
);
|
|
165
|
-
```
|
|
166
|
-
|
|
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 } })`
|
|
173
|
-
|
|
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
|
-
```
|
|
182
|
-
|
|
183
|
-
```typescript
|
|
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));
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
Three rules keep the flip predictable:
|
|
199
|
-
|
|
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.
|
|
203
|
-
|
|
204
|
-
```typescript
|
|
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
|
-
|
|
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.)
|
|
238
|
-
|
|
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`.
|
|
245
|
-
|
|
246
|
-
### Handwritten HTTP lists
|
|
247
|
-
|
|
248
|
-
`store.resource` is the usual CRUD path. When you write the list Flow yourself, prefer the same Stripe envelope — `out` is the item array, pagination sits on `meta`:
|
|
249
|
-
|
|
250
|
-
```typescript
|
|
251
|
-
export const list = on(
|
|
252
|
-
http.get("/notes").public(),
|
|
253
|
-
flow("notes.list", {
|
|
254
|
-
out: z.array(Note),
|
|
255
|
-
do: async (input, fx) => {
|
|
256
|
-
const rows = await fx.store(db).select().from(notes);
|
|
257
|
-
return fx.json.withQuery(rows, input);
|
|
258
|
-
},
|
|
259
|
-
}),
|
|
260
|
-
);
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
`list.ts` + `http.get()` fills `GET /notes` named `notes.list` — explicit strings still win. See [Project structure](/docs/get-started/project-structure).
|
|
264
|
-
|
|
265
|
-
Zero-config: `?q=` searches every string field, extra keys auto-eq except path `id`, and PostgREST `?col=op.value` / `?order=` / `?select=` are open. Nested lists filter the parent in the load — `input.id` is not a column eq.
|
|
266
|
-
|
|
267
|
-
<Callout title="Preferred, not required">
|
|
268
|
-
Any declared `out` is valid — `{ notes: […] }` still works. The array + `fx.json.withQuery`
|
|
269
|
-
shape matches `store.resource` and the typed client (`data` is the page, `meta` is the pager).
|
|
270
|
-
</Callout>
|
|
271
|
-
|
|
272
|
-
### Querying by hand
|
|
273
|
-
|
|
274
|
-
When the list is not conventional CRUD, `fx.store(db)` is the full single-table session:
|
|
275
|
-
|
|
276
|
-
```typescript
|
|
277
|
-
// select — chain where / orderBy / limit / offset in any order
|
|
278
|
-
const latest = await fx
|
|
279
|
-
.store(db)
|
|
280
|
-
.select()
|
|
281
|
-
.from(notes)
|
|
282
|
-
.where(like(notes.title, `%${input.q}%`))
|
|
283
|
-
.orderBy(desc(notes.createdAt))
|
|
284
|
-
.limit(20);
|
|
285
|
-
|
|
286
|
-
// findById, insert, update, delete
|
|
287
|
-
const one = await fx.store(db).findById(notes, input.id);
|
|
288
|
-
await fx.store(db).update(notes).set({ title: input.title }).where(eq(notes.id, input.id));
|
|
289
|
-
await fx.store(db).delete(notes).where(lt(notes.createdAt, cutoff));
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
`select().from(notes)` infers the declared row (`string` / `number` / `null` from `field.*`). A `select({ title: notes.title })` projection stays untyped.
|
|
293
|
-
|
|
294
|
-
<Callout title="One table per call — no relational with:">
|
|
295
|
-
`fx.store` is deliberately **single-table**: Drizzle's relational `findMany({ with: … })` is not
|
|
296
|
-
available through `fx`. Compose joins as separate single-table reads (or `fx.call`) so every
|
|
297
|
-
table shows up explicitly in the Manifest's `reads` / `writes` — powering caching and PII masking.
|
|
298
|
-
</Callout>
|
|
299
|
-
|
|
300
|
-
### Schema — declare once, generate per dialect
|
|
301
|
-
|
|
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`.
|
|
303
|
-
|
|
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 |
|
|
335
|
-
|
|
336
|
-
#### Foreign keys and relations
|
|
337
|
-
|
|
338
|
-
Declare FKs on fields, and relation metadata once per schema:
|
|
339
|
-
|
|
340
|
-
```typescript
|
|
341
|
-
export const daily = store.schema.table("daily", {
|
|
342
|
-
id: field.text().primaryKey(),
|
|
343
|
-
code: field
|
|
344
|
-
.text()
|
|
345
|
-
.notNull()
|
|
346
|
-
.references(() => links.code),
|
|
347
|
-
day: field.text().notNull(),
|
|
348
|
-
clicks: field.integer().notNull().default(0),
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
export const relations = store.schema.relations({ links, daily }, (r) => ({
|
|
352
|
-
links: { daily: r.many.daily({ from: r.links.code, to: r.daily.code }) },
|
|
353
|
-
daily: { link: r.one.links({ from: r.daily.code, to: r.links.code, optional: false }) },
|
|
354
|
-
}));
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
<Callout title="Many-to-many">
|
|
358
|
-
A junction table with two foreign keys plus two `one` / `many` relations composes many-to-many.
|
|
359
|
-
There is no separate API and no `.through()` — the junction is an ordinary table.
|
|
360
|
-
</Callout>
|
|
361
|
-
|
|
362
|
-
#### Syncing the schema
|
|
363
|
-
|
|
364
|
-
| Command | When |
|
|
365
|
-
| ----------------- | -------------------------------------------------------------- |
|
|
366
|
-
| `oke db push` | Dev — apply directly to the live local DB (no migration files) |
|
|
367
|
-
| `oke db generate` | Write versioned SQL under `drizzle/` for review |
|
|
368
|
-
| `oke db migrate` | Apply those files — human or CI, **never** at boot |
|
|
369
|
-
|
|
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.
|
|
371
|
-
|
|
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`).
|
|
373
|
-
|
|
374
|
-
#### Multiple environments
|
|
375
|
-
|
|
376
|
-
| Environment | Sync |
|
|
377
|
-
| -------------- | -------------------------------------------------------------- |
|
|
378
|
-
| `dev` | `oke db push` (or auto-push from `oke dev`) |
|
|
379
|
-
| Staging / prod | `oke db generate` → review files → `oke db migrate` on that DB |
|
|
380
|
-
|
|
381
|
-
<Callout title="Connection pooling is infrastructure, not app code" type="info">
|
|
382
|
-
One shared Bun.SQL pool (**8** connections) per URL. `dev`/`prod` put **PgDog** in front
|
|
383
|
-
(`DATABASE_URL` → port `6432`, `pool_size` 20). RLS stamps `reserve` one slot, then `SET LOCAL` in
|
|
384
|
-
that transaction — a leaked `BEGIN` exhausts checkout.
|
|
385
|
-
</Callout>
|
|
386
|
-
|
|
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).
|
|
388
|
-
|
|
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.
|
|
390
|
-
|
|
391
|
-
#### Seeding
|
|
392
|
-
|
|
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).
|
|
394
|
-
|
|
395
|
-
<StoreSeeding />
|
|
396
|
-
|
|
397
|
-
| env | `essential` | `dev` | `prod` |
|
|
398
|
-
| ------ | ----------- | ----- | ------ |
|
|
399
|
-
| `dev` | yes | yes | no |
|
|
400
|
-
| `test` | yes | no | no |
|
|
401
|
-
| `prod` | yes | no | yes |
|
|
402
|
-
|
|
403
|
-
Outside `test`, exactly one of seed-category `dev` / `prod` runs with `essential`.
|
|
404
|
-
|
|
405
|
-
**Simple form** — everything inline:
|
|
406
|
-
|
|
407
|
-
```ts
|
|
408
|
-
import { defineSeed, type Fx } from "okengine";
|
|
409
|
-
import { db } from "../core";
|
|
410
|
-
import { notes } from "../schema.decl";
|
|
411
|
-
|
|
412
|
-
export default defineSeed({
|
|
413
|
-
name: "notes",
|
|
414
|
-
description: "Welcome + sample notes",
|
|
415
|
-
essential: async (fx: Fx) => {
|
|
416
|
-
await fx
|
|
417
|
-
.store(db)
|
|
418
|
-
.upsert(
|
|
419
|
-
notes,
|
|
420
|
-
{ id: "welcome" },
|
|
421
|
-
{ id: "welcome", title: "Welcome", body: "…", createdAt: 1 },
|
|
422
|
-
);
|
|
423
|
-
},
|
|
424
|
-
dev: async (fx: Fx) => {
|
|
425
|
-
await fx
|
|
426
|
-
.store(db)
|
|
427
|
-
.upsert(
|
|
428
|
-
notes,
|
|
429
|
-
{ id: "sample-ideas" },
|
|
430
|
-
{ id: "sample-ideas", title: "Ideas", body: "…", createdAt: 2 },
|
|
431
|
-
);
|
|
432
|
-
},
|
|
433
|
-
// prod: async (fx) => { /* e.g. register a real external webhook URL */ },
|
|
434
|
-
});
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
**Complex form** — one function per file, compose with arrays (order = run order):
|
|
438
|
-
|
|
439
|
-
```ts
|
|
440
|
-
import { defineSeed } from "okengine";
|
|
441
|
-
import { seedWelcome } from "./essential/welcome";
|
|
442
|
-
import { seedSamples } from "./dev/samples";
|
|
443
|
-
import { seedWebhook } from "./prod/webhook";
|
|
444
|
-
|
|
445
|
-
export default defineSeed({
|
|
446
|
-
name: "notes",
|
|
447
|
-
description: "Welcome + sample notes",
|
|
448
|
-
essential: [seedWelcome],
|
|
449
|
-
dev: [seedSamples],
|
|
450
|
-
prod: [seedWebhook],
|
|
451
|
-
});
|
|
452
|
-
```
|
|
453
|
-
|
|
454
|
-
| `upsert` outcome | When |
|
|
455
|
-
| ----------------- | ------------------------------------------ |
|
|
456
|
-
| `upserted` | no row matched `matchOn` — insert |
|
|
457
|
-
| `already-existed` | match found; default leaves it alone |
|
|
458
|
-
| `changed` | match found and `{ onExisting: "update" }` |
|
|
459
|
-
|
|
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.
|
|
461
|
-
|
|
462
|
-
```ts
|
|
463
|
-
// Safe bootstrap — second seed run leaves the row alone
|
|
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
|
-
);
|
|
471
|
-
|
|
472
|
-
// Refresh copy on every seed — match stays; title/body are rewritten
|
|
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
|
-
);
|
|
481
|
-
```
|
|
482
|
-
|
|
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.
|
|
484
|
-
|
|
485
|
-
| Env | Confirm |
|
|
486
|
-
| -------------- | ----------------------------------------------------- |
|
|
487
|
-
| `test` | none |
|
|
488
|
-
| `dev` / `prod` | print DB target; type the env name (`--force` for CI) |
|
|
489
|
-
|
|
490
|
-
```bash
|
|
491
|
-
oke db seed # resolve env like other oke db commands
|
|
492
|
-
oke db seed --env dev # explicit override
|
|
493
|
-
oke db seed --env prod --force
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
<Callout title="Seed vs migrate">
|
|
497
|
-
Permanent reference-data corrections go through `oke db generate` → `migrate`, not repeated seed.
|
|
498
|
-
Optional `drizzle-seed` may be used inside a `dev` function body only — never as the seed system
|
|
499
|
-
backbone.
|
|
500
|
-
</Callout>
|
|
501
|
-
|
|
502
|
-
<Callout title="Escape hatch">
|
|
503
|
-
Hand-written Drizzle in `src/schema.ts` stays supported — if there is nothing to emit, the emit
|
|
504
|
-
step is skipped and your file is used as-is. Plugins may contribute **whole new tables**;
|
|
505
|
-
extending an app-owned table with plugin columns is not supported in v1.
|
|
506
|
-
</Callout>
|
|
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
|
-
|
|
536
|
-
### Per-environment SQL drivers
|
|
537
|
-
|
|
538
|
-
Same flow code, different backends — configured once in `oke.config.ts`:
|
|
539
|
-
|
|
540
|
-
| Facet | `dev` / `prod` | `test` | Runs as |
|
|
541
|
-
| ----- | -------------- | -------- | --------------------------------------------- |
|
|
542
|
-
| `sql` | `postgres` | `pglite` | Compose / deploy Postgres → in-process PGLite |
|
|
543
|
-
|
|
544
|
-
Defaults: `postgres` for `dev`/`prod`, `pglite` for `test`. SQLite-family drivers (`sqlite`, `libsql`) are removed. Container images come from the `images` map — change the vendor by changing the pin, never the driver id.
|
|
545
|
-
|
|
546
|
-
#### Opt-in SQL drivers
|
|
547
|
-
|
|
548
|
-
| Driver | Install | Wire dialect | Choose for |
|
|
549
|
-
| -------- | -------------------------------------------------------- | ------------ | --------------------------------------------------------- |
|
|
550
|
-
| `pglite` | `@electric-sql/pglite` + `@electric-sql/pglite-pgvector` | postgresql | **required** for `store.sql.test`; also usable in-process |
|
|
551
|
-
|
|
552
|
-
<Callout title="PGlite is the test SQL driver">
|
|
553
|
-
`defineConfig` requires `drivers.store.sql.test` to be `pglite` when set. Use `oke test` (sets
|
|
554
|
-
`OKE_PGLITE_URL=memory://`). Choose PGLite for Postgres correctness in tests — not as a Compose
|
|
555
|
-
substitute for `oke dev`.
|
|
556
|
-
</Callout>
|
|
557
|
-
|
|
558
|
-
SQL-backed index driver `pgvector` shares this facet's connection — see [Advanced — composing facets together](#advanced--composing-facets-together).
|
|
559
|
-
|
|
560
|
-
## KV
|
|
561
|
-
|
|
562
|
-
<StoreFacetMark facet="kv" />
|
|
563
|
-
|
|
564
|
-
The key-value facet: short-lived cache and session data behind `fx.store`. One namespace per declaration; drivers swap `memory` ↔ `redis` per environment.
|
|
565
|
-
|
|
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.
|
|
567
|
-
|
|
568
|
-
The same band's **Performance** control opens engine telemetry on the `redis` driver. In-process `memory` is unsupported (`KvStatsUnsupported`).
|
|
569
|
-
|
|
570
|
-
### Quick start
|
|
571
|
-
|
|
572
|
-
<Steps>
|
|
573
|
-
|
|
574
|
-
<Step>
|
|
575
|
-
### Declare a namespace
|
|
576
|
-
|
|
577
|
-
```typescript
|
|
578
|
-
import { store } from "okengine";
|
|
579
|
-
|
|
580
|
-
export const sessions = store.kv("sessions", { description: "Session cache" });
|
|
581
|
-
// ref → kv:sessions
|
|
582
|
-
```
|
|
583
|
-
|
|
584
|
-
</Step>
|
|
585
|
-
|
|
586
|
-
<Step>
|
|
587
|
-
### Read and write in a Flow
|
|
588
|
-
|
|
589
|
-
```typescript
|
|
590
|
-
do: async ({ userId, data }, fx) => {
|
|
591
|
-
const kv = fx.store(sessions);
|
|
592
|
-
await kv.set(`user:${userId}`, data, "30m");
|
|
593
|
-
return await kv.get(`user:${userId}`);
|
|
594
|
-
},
|
|
595
|
-
```
|
|
596
|
-
|
|
597
|
-
</Step>
|
|
598
|
-
|
|
599
|
-
</Steps>
|
|
600
|
-
|
|
601
|
-
### Handle API
|
|
602
|
-
|
|
603
|
-
| Call | Effect | Returns | Notes |
|
|
604
|
-
| ----------------------- | ------- | ---------------- | ------------------------------------------------------------------------ |
|
|
605
|
-
| `get(key)` | `read` | `unknown` | Missing keys resolve per driver |
|
|
606
|
-
| `set(key, value, ttl?)` | `write` | `void` | Values should be JSON-serializable |
|
|
607
|
-
| `delete(key)` | `write` | `boolean` | Whether a key was removed |
|
|
608
|
-
| `list(prefix?)` | `read` | `string[]` | Prefix filter; Console browse shows TTL and size |
|
|
609
|
-
| `ttlMs(key)` | `read` | `number \| null` | Remaining expiry; `null` when the key has none. Console command is `ttl` |
|
|
610
|
-
|
|
611
|
-
### TTL
|
|
612
|
-
|
|
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.
|
|
614
|
-
|
|
615
|
-
<StoreKvTtl />
|
|
616
|
-
|
|
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.
|
|
618
|
-
|
|
619
|
-
### Durable KV
|
|
620
|
-
|
|
621
|
-
Default `store.kv("sessions")` is cache-shaped — a Redis recreate drops keys.
|
|
622
|
-
|
|
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.
|
|
624
|
-
|
|
625
|
-
```typescript
|
|
626
|
-
export const sessions = store.kv("sessions", { description: "Session cache" });
|
|
627
|
-
export const ledger = store.kv("ledger", { durable: true, description: "Idempotency keys" });
|
|
628
|
-
export const drafts = store.kv("drafts", { durable: true, description: "Compose drafts" });
|
|
629
|
-
```
|
|
630
|
-
|
|
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 }`.
|
|
632
|
-
|
|
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 })`.
|
|
634
|
-
|
|
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`.
|
|
636
|
-
|
|
637
|
-
<Callout title="Not per-key, not Flow durable, not auto-cache">
|
|
638
|
-
Durable KV is a JSONB table on the same Postgres as `store.sql`. Flow `durable` journals steps.
|
|
639
|
-
Tier-1 auto-cache is an in-process Map and never reads `store.kv`.
|
|
640
|
-
</Callout>
|
|
641
|
-
|
|
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.
|
|
643
|
-
|
|
644
|
-
| Image | Pin | Why pick it | License |
|
|
645
|
-
| ------------- | --------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
646
|
-
| **Redis** | `redis:8-alpine` (default) | Most mature / battle-tested | RSALv2 / SSPLv1 / AGPLv3 (Redis ≥8) — service limits under RSAL/SSPL apply if you offer Redis as a managed service to third parties |
|
|
647
|
-
| **Valkey** | `valkey/valkey:8-alpine` | BSD fork (Linux Foundation) | BSD-3-Clause — permissive; no managed-service restriction |
|
|
648
|
-
| **Dragonfly** | `docker.dragonflydb.io/dragonflydb/dragonfly` | Multi-threaded Redis-wire runtime | BSL 1.1 (converts to Apache 2.0 on a published change date) — free for self-hosting; restricts offering Dragonfly as a commercial managed service |
|
|
649
|
-
|
|
650
|
-
<Callout title="Licenses bite managed-service resellers — read this once" type="warn">
|
|
651
|
-
RSAL, SSPL, and BSL service restrictions apply when you offer that specific datastore **as a
|
|
652
|
-
service** to third parties — not when you run it for your own app. Pick for your situation; OKE
|
|
653
|
-
does not pick a "safer" default for you.
|
|
654
|
-
</Callout>
|
|
655
|
-
|
|
656
|
-
<Callout title="Dry-run refuses KV writes">
|
|
657
|
-
`set` and `delete` throw `DryRunWriteIsolationError` during dry-run — the runtime will not risk a
|
|
658
|
-
double-write against a shared Redis. Reads (`get` / `list`) still run.
|
|
659
|
-
</Callout>
|
|
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
|
-
|
|
688
|
-
## Files
|
|
689
|
-
|
|
690
|
-
<StoreFacetMark facet="files" />
|
|
691
|
-
|
|
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.
|
|
693
|
-
|
|
694
|
-
### Quick start
|
|
695
|
-
|
|
696
|
-
<Steps>
|
|
697
|
-
|
|
698
|
-
<Step>
|
|
699
|
-
### Declare a bucket
|
|
700
|
-
|
|
701
|
-
```typescript
|
|
702
|
-
import { store } from "okengine";
|
|
703
|
-
|
|
704
|
-
export const uploads = store.files("attachments", {
|
|
705
|
-
description: "User uploads",
|
|
706
|
-
});
|
|
707
|
-
// ref → files:attachments
|
|
708
|
-
```
|
|
709
|
-
|
|
710
|
-
</Step>
|
|
711
|
-
|
|
712
|
-
<Step>
|
|
713
|
-
### Put and get in a Flow
|
|
714
|
-
|
|
715
|
-
```typescript
|
|
716
|
-
do: async ({ key, body }, fx) => {
|
|
717
|
-
const files = fx.store(uploads);
|
|
718
|
-
await files.put(key, body);
|
|
719
|
-
return await files.get(key); // Uint8Array | null
|
|
720
|
-
},
|
|
721
|
-
```
|
|
722
|
-
|
|
723
|
-
</Step>
|
|
724
|
-
|
|
725
|
-
</Steps>
|
|
726
|
-
|
|
727
|
-
### Blob CRUD — `put` / `get` / `delete` / `list`
|
|
728
|
-
|
|
729
|
-
| Call | Effect | Returns | Notes |
|
|
730
|
-
| ---------------- | ------- | -------------------- | -------------------------------------------- |
|
|
731
|
-
| `put(key, data)` | `write` | `void` | `data` is `Uint8Array` or `string` |
|
|
732
|
-
| `get(key)` | `read` | `Uint8Array \| null` | Missing key → `null` |
|
|
733
|
-
| `delete(key)` | `write` | `boolean` | Whether an object was removed |
|
|
734
|
-
| `list(prefix?)` | `read` | `string[]` | Prefix filter; Console folder browser source |
|
|
735
|
-
|
|
736
|
-
```typescript
|
|
737
|
-
const files = fx.store(uploads);
|
|
738
|
-
await files.put("docs/readme.txt", "hello");
|
|
739
|
-
const bytes = await files.get("docs/readme.txt");
|
|
740
|
-
const keys = await files.list("docs/");
|
|
741
|
-
await files.delete("docs/readme.txt");
|
|
742
|
-
```
|
|
743
|
-
|
|
744
|
-
### Keys
|
|
745
|
-
|
|
746
|
-
Keys are opaque strings the driver stores as-is. Prefer path-like prefixes (`photos/`, `exports/`).
|
|
747
|
-
|
|
748
|
-
| Helper / rule | Meaning |
|
|
749
|
-
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
750
|
-
| `contentAddressedKey(data)` | From `okengine/store` — stable sha256 hex of the bytes (dedupe-friendly) |
|
|
751
|
-
| `safeFileObjectKey(name, prefix?)` | URL-safe ASCII key (`file-a1b2c3d4.pdf`). Keep the original name in the catalog. |
|
|
752
|
-
| Leading `/` or `..` | Rejected by the `fs` driver (`Invalid object key`) |
|
|
753
|
-
| Non-ASCII keys | Console warns `non_ascii_key`. Console puts rewrite the key and store `originalName` in `.oke/catalog.json` (and `file_objects` when that table exists). |
|
|
754
|
-
|
|
755
|
-
### Per-environment Files drivers
|
|
756
|
-
|
|
757
|
-
```typescript title="oke.config.ts"
|
|
758
|
-
drivers: {
|
|
759
|
-
store: {
|
|
760
|
-
files: { dev: "s3", test: "memory", prod: "s3" },
|
|
761
|
-
},
|
|
762
|
-
},
|
|
763
|
-
images: {
|
|
764
|
-
store: { files: "rustfs/rustfs:1.0.0-rc.2" },
|
|
765
|
-
},
|
|
766
|
-
```
|
|
767
|
-
|
|
768
|
-
| Facet | `dev` / `prod` | `test` | Runs as |
|
|
769
|
-
| ------- | -------------- | -------- | ------------------------ |
|
|
770
|
-
| `files` | `s3` | `memory` | RustFS / S3 → in-process |
|
|
771
|
-
|
|
772
|
-
| Variable | When |
|
|
773
|
-
| ----------------------------- | -------------------------------------- |
|
|
774
|
-
| `S3_BUCKET` | Required bucket for the `s3` driver |
|
|
775
|
-
| `OKE_STORE_FILES_DB` | Override bucket name |
|
|
776
|
-
| `S3_ENDPOINT` · keys · region | S3-compatible endpoint and credentials |
|
|
777
|
-
| `OKE_FILES_DRIVER` | Force a driver id |
|
|
778
|
-
|
|
779
|
-
`memory` is the test default. Local `fs` writes under a temp root when no binding `root` is set.
|
|
780
|
-
|
|
781
|
-
**Known limit:** `fs` is single-host. Under horizontal scale each replica sees its own filesystem (silently inconsistent object views). Boot prints a one-shot warning when `drivers.store.files` is `fs`. Use `s3` for `dev`/`prod` (create-oke templates already do).
|
|
782
|
-
|
|
783
|
-
<Callout title="Console Files is a folder browser">
|
|
784
|
-
Inspect the **bucket**. Click a file to preview in a sheet. Uploads use a URL-safe key; the
|
|
785
|
-
original name lives in the object catalog. Bytes live on `memory`, `fs`, or `s3`.
|
|
786
|
-
</Callout>
|
|
787
|
-
|
|
788
|
-
### Images — `image` / `putImage`
|
|
789
|
-
|
|
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:
|
|
791
|
-
|
|
792
|
-
<StoreFilesVariants />
|
|
793
|
-
|
|
794
|
-
```typescript
|
|
795
|
-
const meta = await fx.store(uploads).image(key).metadata();
|
|
796
|
-
// → { width, height, format }
|
|
797
|
-
|
|
798
|
-
await fx
|
|
799
|
-
.store(uploads)
|
|
800
|
-
.image(key)
|
|
801
|
-
.resize(400, 400, { fit: "inside" })
|
|
802
|
-
.webp({ quality: 80 })
|
|
803
|
-
.put(`thumbs/${key}.webp`);
|
|
804
|
-
|
|
805
|
-
const result = await fx.store(uploads).putImage("photos/x.jpg", bytes, {
|
|
806
|
-
variants: {
|
|
807
|
-
thumb: { resize: [128, 128, { fit: "inside" }], webp: { quality: 80 } },
|
|
808
|
-
medium: { resize: [800], webp: { quality: 85 } },
|
|
809
|
-
},
|
|
810
|
-
placeholder: true, // ThumbHash LQIP data URL
|
|
811
|
-
});
|
|
812
|
-
// variants: photos/x.thumb.webp, photos/x.medium.webp
|
|
813
|
-
```
|
|
814
|
-
|
|
815
|
-
| Call | Effect | Notes |
|
|
816
|
-
| ----------------------------------------------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
817
|
-
| `image(key\|bytes).metadata()` / `bytes()` / `blob()` / `placeholder()` | `read` when source is a key | Chain `resize` · `rotate` · `flip` · `flop` · `modulate`, then `jpeg` / `png` / `webp` (portable); `heic` / `avif` fall back to WebP when the OS codec is missing |
|
|
818
|
-
| `image(…).put(outKey)` | `read` + `write` (key source) | Persist the encoded result |
|
|
819
|
-
| `putImage(key, data, opts?)` | `write` | Original + named variants; keys are `{stem}.{variant}.{ext}` |
|
|
820
|
-
|
|
821
|
-
#### Image options
|
|
822
|
-
|
|
823
|
-
| Option | Default | Meaning |
|
|
824
|
-
| ------------- | ----------------------- | ---------------------------------------------------- |
|
|
825
|
-
| `maxPixels` | **16 MP** (`4096×4096`) | Decode ceiling — raise it or pass `false` to opt out |
|
|
826
|
-
| `autoOrient` | `true` | Apply JPEG EXIF orientation before transforms |
|
|
827
|
-
| `variants` | — | Named `{ resize?, jpeg\|png\|webp\|… }` specs |
|
|
828
|
-
| `placeholder` | — | When `true`, return a ThumbHash LQIP data URL |
|
|
829
|
-
|
|
830
|
-
`putImage` returns `{ key, meta, variants, placeholder? }` — `variants` maps name → object key.
|
|
831
|
-
|
|
832
|
-
## Index
|
|
833
|
-
|
|
834
|
-
<StoreFacetMark facet="index" />
|
|
835
|
-
|
|
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.
|
|
837
|
-
|
|
838
|
-
### Quick start
|
|
839
|
-
|
|
840
|
-
<Steps>
|
|
841
|
-
|
|
842
|
-
<Step>
|
|
843
|
-
### Declare a vector index
|
|
844
|
-
|
|
845
|
-
```typescript
|
|
846
|
-
import { store } from "okengine";
|
|
847
|
-
|
|
848
|
-
export const embeddings = store.index("docs", {
|
|
849
|
-
dims: 1536,
|
|
850
|
-
description: "Document embeddings",
|
|
851
|
-
});
|
|
852
|
-
// ref → index:docs
|
|
853
|
-
```
|
|
854
|
-
|
|
855
|
-
`dims` defaults to `3` at open if omitted — set it to match your embedder.
|
|
856
|
-
|
|
857
|
-
</Step>
|
|
858
|
-
|
|
859
|
-
<Step>
|
|
860
|
-
### Upsert and search in a Flow
|
|
861
|
-
|
|
862
|
-
```typescript
|
|
863
|
-
do: async ({ id, vector, query }, fx) => {
|
|
864
|
-
const idx = fx.store(embeddings);
|
|
865
|
-
await idx.upsert(id, vector, { title: "Intro" });
|
|
866
|
-
return await idx.search(query, 5);
|
|
867
|
-
// → [{ id, score, meta? }, …] score = cosine similarity
|
|
868
|
-
},
|
|
869
|
-
```
|
|
870
|
-
|
|
871
|
-
</Step>
|
|
872
|
-
|
|
873
|
-
</Steps>
|
|
874
|
-
|
|
875
|
-
### Handle API — vector vs text
|
|
876
|
-
|
|
877
|
-
One declaration shape — two search physics. The handle is a **discriminated union** on `driverId`:
|
|
878
|
-
|
|
879
|
-
<StoreIndexModes />
|
|
880
|
-
|
|
881
|
-
| Call (both) | Effect | Returns |
|
|
882
|
-
| -------------- | ------- | --------------------- |
|
|
883
|
-
| `upsert(…)` | `write` | `void` |
|
|
884
|
-
| `search(…)` | `read` | hits / result |
|
|
885
|
-
| `list(limit?)` | `read` | documents (id + meta) |
|
|
886
|
-
| `delete(id)` | `write` | `boolean` |
|
|
887
|
-
|
|
888
|
-
<Callout title="Console Index is a search box">
|
|
889
|
-
Type a phrase to find documents by id and stored meta. A comma-separated vector still runs an ANN
|
|
890
|
-
probe. Meilisearch uses full-text `q`.
|
|
891
|
-
</Callout>
|
|
892
|
-
|
|
893
|
-
TypeScript rejects a vector query against a text index (and vice versa) at compile time — never at runtime.
|
|
894
|
-
|
|
895
|
-
### Vector ANN
|
|
896
|
-
|
|
897
|
-
| `store.index` id | Shares connection from | Real ANN via |
|
|
898
|
-
| ---------------- | -------------------------- | ----------------------- |
|
|
899
|
-
| `memory` | — (in-process, default) | Full scan cosine |
|
|
900
|
-
| `pgvector` | `postgres` or `pglite` sql | HNSW + `cosineDistance` |
|
|
901
|
-
|
|
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`.
|
|
903
|
-
|
|
904
|
-
Pair SQL + index under [Advanced — composing facets together](#advanced--composing-facets-together).
|
|
905
|
-
|
|
906
|
-
### Full-text search — `meilisearch` (opt-in)
|
|
907
|
-
|
|
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.
|
|
909
|
-
|
|
910
|
-
```typescript
|
|
911
|
-
export const kb = store.index("kb", { description: "Help articles" });
|
|
912
|
-
|
|
913
|
-
do: async ({ id, doc, q }, fx) => {
|
|
914
|
-
const idx = fx.store(kb); // driverId: "meilisearch"
|
|
915
|
-
await idx.upsert(id, { id, ...doc });
|
|
916
|
-
return await idx.search(q, {
|
|
917
|
-
topK: 10,
|
|
918
|
-
filter: "category = guides",
|
|
919
|
-
facets: ["category"],
|
|
920
|
-
});
|
|
921
|
-
// → { hits, facetDistribution? }
|
|
922
|
-
},
|
|
923
|
-
```
|
|
924
|
-
|
|
925
|
-
| Search option | Meaning |
|
|
926
|
-
| ------------- | ------------------------------------------------ |
|
|
927
|
-
| `topK` | Max hits (Meilisearch `limit`, default 10) |
|
|
928
|
-
| `filter` | Filter expression — attribute must be filterable |
|
|
929
|
-
| `facets` | Attributes to aggregate into `facetDistribution` |
|
|
930
|
-
|
|
931
|
-
```typescript title="oke.config.ts"
|
|
932
|
-
drivers: {
|
|
933
|
-
store: {
|
|
934
|
-
index: { dev: "meilisearch", test: "memory", prod: "meilisearch" },
|
|
935
|
-
},
|
|
936
|
-
},
|
|
937
|
-
images: {
|
|
938
|
-
store: { index: "getmeili/meilisearch:v1.53" },
|
|
939
|
-
},
|
|
940
|
-
```
|
|
941
|
-
|
|
942
|
-
- **`oke dev` / prod** use the image recipe; the app gets `OKE_STORE_INDEX_URL` + `OKE_STORE_INDEX_KEY` from Compose — a standalone HTTP service, never the shared `sqlUrl`.
|
|
943
|
-
- **Fail-loud:** a configured meilisearch that is unreachable or unhealthy throws `MeilisearchUnavailableError` — never a silent memory fallback.
|
|
944
|
-
- **Not for vectors:** `ai.embed` / `fx.search` stay vector-only; pointing an embed `into` a meilisearch index fails loud (embeddings don't apply).
|
|
945
|
-
|
|
946
|
-
### Per-environment Index drivers
|
|
947
|
-
|
|
948
|
-
Unlike KV/Files, an unset `drivers.store.index` map stays **`memory` in every environment** — there is no automatic fallback to `pgvector`. Opt in explicitly:
|
|
949
|
-
|
|
950
|
-
```typescript title="oke.config.ts"
|
|
951
|
-
drivers: {
|
|
952
|
-
store: {
|
|
953
|
-
sql: { dev: "postgres", test: "pglite", prod: "postgres" },
|
|
954
|
-
index: { dev: "pgvector", test: "memory", prod: "pgvector" },
|
|
955
|
-
},
|
|
956
|
-
},
|
|
957
|
-
```
|
|
958
|
-
|
|
959
|
-
| Variable | When |
|
|
960
|
-
| --------------------- | ------------------------------------------------- |
|
|
961
|
-
| `OKE_INDEX_DRIVER` | Force a driver id |
|
|
962
|
-
| `OKE_STORE_INDEX_URL` | Required for `meilisearch` |
|
|
963
|
-
| `OKE_STORE_INDEX_KEY` | Meilisearch API key (`MEILI_MASTER_KEY` fallback) |
|
|
964
|
-
|
|
965
|
-
### With AI embeddings
|
|
966
|
-
|
|
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.
|
|
968
|
-
|
|
969
|
-
## Advanced — composing facets together
|
|
970
|
-
|
|
971
|
-
Facets are independent declarations. A Flow may touch several in one `do` — for example store a files key on a SQL row, or `upsert` into an index after a SQL write. There is no special sync API: `store.resource` `search:` is SQL substring match, not meilisearch.
|
|
972
|
-
|
|
973
|
-
One real coupling: `pgvector` shares the sql facet's already-open connection, so configure the pair together:
|
|
974
|
-
|
|
975
|
-
```typescript title="oke.config.ts"
|
|
976
|
-
drivers: {
|
|
977
|
-
store: {
|
|
978
|
-
sql: { dev: "postgres", test: "pglite", prod: "postgres" },
|
|
979
|
-
index: { dev: "pgvector", test: "memory", prod: "pgvector" },
|
|
980
|
-
},
|
|
981
|
-
},
|
|
982
|
-
```
|
|
983
|
-
|
|
984
|
-
The full per-environment map looks like this:
|
|
985
|
-
|
|
986
|
-
```typescript title="oke.config.ts"
|
|
987
|
-
drivers: {
|
|
988
|
-
store: {
|
|
989
|
-
sql: { dev: "postgres", test: "pglite", prod: "postgres" },
|
|
990
|
-
kv: { dev: "redis", test: "memory", prod: "redis" },
|
|
991
|
-
files: { dev: "s3", test: "memory", prod: "s3" },
|
|
992
|
-
},
|
|
993
|
-
},
|
|
994
|
-
```
|
|
995
|
-
|
|
996
|
-
Container images come from the `images` map — change the vendor by changing the pin, never the driver id.
|
|
997
|
-
|
|
998
|
-
## Advanced — with other elements
|
|
999
|
-
|
|
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.
|
|
1001
|
-
|
|
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).
|
|
1003
|
-
|
|
1004
|
-
## Troubleshooting
|
|
1005
|
-
|
|
1006
|
-
<Accordions>
|
|
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.
|
|
1011
|
-
</Accordion>
|
|
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`.
|
|
1016
|
-
</Accordion>
|
|
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`.
|
|
1022
|
-
</Accordion>
|
|
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.
|
|
1028
|
-
</Accordion>
|
|
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.
|
|
1033
|
-
</Accordion>
|
|
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.
|
|
1038
|
-
</Accordion>
|
|
1039
|
-
|
|
1040
|
-
<Accordion title="List data is { items, count } instead of an array">
|
|
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.
|
|
1042
|
-
</Accordion>
|
|
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.
|
|
1047
|
-
</Accordion>
|
|
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.
|
|
1052
|
-
</Accordion>
|
|
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`.
|
|
1057
|
-
</Accordion>
|
|
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`).
|
|
1062
|
-
</Accordion>
|
|
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.
|
|
1067
|
-
</Accordion>
|
|
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.
|
|
1072
|
-
</Accordion>
|
|
1073
|
-
|
|
1074
|
-
<Accordion title="Console KV shows No rows after oke db seed">
|
|
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.
|
|
1076
|
-
</Accordion>
|
|
1077
|
-
|
|
1078
|
-
</Accordions>
|
|
1079
|
-
|
|
1080
|
-
## Learn more
|
|
1081
|
-
|
|
1082
|
-
- [Project structure](/docs/get-started/project-structure) — `list.ts` + `http.get()` is `GET /notes` named `notes.list`
|
|
1083
|
-
- [Flow](/docs/elements/flow) — the `fx.store` session inside `do`
|
|
1084
|
-
- [AI](/docs/elements/ai) — `ai.embed` into a vector `store.index`, searched via `fx.search`
|
|
1085
|
-
- [Gate](/docs/elements/gate) — `pii:reveal`, `gate.auth.tenant`, and `oke.tenant()`
|
|
1086
|
-
- [CLI Reference](/docs/reference/cli) — `oke db push` · `generate` · `migrate`
|
|
1087
|
-
- [Configuration](/docs/reference/configuration) — `drivers.store` maps and `images` pins
|
|
1088
|
-
- [Environment variables](/docs/reference/environment-variables) — Redis · S3 · meilisearch URLs
|
|
1089
|
-
|
|
1090
|
-
## Next
|
|
1091
|
-
|
|
1092
|
-
<Cards>
|
|
1093
|
-
<Card title="Clock" description="Continue to Clock." href="/docs/elements/clock" />
|
|
1094
|
-
<Card
|
|
1095
|
-
title="Introduction"
|
|
1096
|
-
description="Eight elements overview."
|
|
1097
|
-
href="/docs/get-started/introduction"
|
|
1098
|
-
/>
|
|
1099
|
-
</Cards>
|