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.
Files changed (510) hide show
  1. package/AGENTS.md +18 -10
  2. package/README.md +2 -2
  3. package/package.json +51 -50
  4. package/site/content/docs/ai/llms-txt.mdx +1 -1
  5. package/site/content/docs/ai/meta.json +1 -1
  6. package/site/content/docs/ai/skills.mdx +4 -1
  7. package/site/content/docs/ai/try-it.mdx +58 -0
  8. package/site/content/docs/client/auth.mdx +208 -0
  9. package/site/content/docs/client/calling.mdx +451 -0
  10. package/site/content/docs/client/index.mdx +209 -0
  11. package/site/content/docs/client/live.mdx +234 -0
  12. package/site/content/docs/client/meta.json +5 -0
  13. package/site/content/docs/client/react.mdx +249 -0
  14. package/site/content/docs/elements/ai/agents.mdx +232 -0
  15. package/site/content/docs/elements/ai/index.mdx +341 -0
  16. package/site/content/docs/elements/ai/mcp.mdx +275 -0
  17. package/site/content/docs/elements/ai/meta.json +5 -0
  18. package/site/content/docs/elements/ai/models.mdx +283 -0
  19. package/site/content/docs/elements/ai/prompts.mdx +256 -0
  20. package/site/content/docs/elements/channel/email.mdx +266 -0
  21. package/site/content/docs/elements/channel/index.mdx +344 -0
  22. package/site/content/docs/elements/channel/meta.json +5 -0
  23. package/site/content/docs/elements/channel/push.mdx +218 -0
  24. package/site/content/docs/elements/channel/receipts.mdx +206 -0
  25. package/site/content/docs/elements/channel/sms.mdx +264 -0
  26. package/site/content/docs/elements/channel/whatsapp.mdx +220 -0
  27. package/site/content/docs/elements/clock/index.mdx +300 -0
  28. package/site/content/docs/elements/clock/meta.json +5 -0
  29. package/site/content/docs/elements/clock/schedules.mdx +508 -0
  30. package/site/content/docs/elements/clock/sleep.mdx +445 -0
  31. package/site/content/docs/elements/flow/consumers.mdx +707 -0
  32. package/site/content/docs/elements/flow/http.mdx +1080 -0
  33. package/site/content/docs/elements/flow/index.mdx +618 -0
  34. package/site/content/docs/elements/flow/meta.json +5 -0
  35. package/site/content/docs/elements/flow/routing.mdx +559 -0
  36. package/site/content/docs/elements/flow/workflows.mdx +640 -0
  37. package/site/content/docs/elements/gate/auth.mdx +371 -0
  38. package/site/content/docs/elements/gate/authorization.mdx +298 -0
  39. package/site/content/docs/elements/gate/index.mdx +418 -0
  40. package/site/content/docs/elements/gate/meta.json +5 -0
  41. package/site/content/docs/elements/gate/rate-limits.mdx +365 -0
  42. package/site/content/docs/elements/gate/rls.mdx +303 -0
  43. package/site/content/docs/elements/gate/tenancy.mdx +372 -0
  44. package/site/content/docs/elements/index.mdx +20 -10
  45. package/site/content/docs/elements/signal/broadcast.mdx +517 -0
  46. package/site/content/docs/elements/signal/index.mdx +352 -0
  47. package/site/content/docs/elements/signal/live.mdx +590 -0
  48. package/site/content/docs/elements/signal/meta.json +5 -0
  49. package/site/content/docs/elements/signal/once.mdx +596 -0
  50. package/site/content/docs/elements/store/files.mdx +659 -0
  51. package/site/content/docs/elements/store/index.mdx +310 -0
  52. package/site/content/docs/elements/store/kv.mdx +629 -0
  53. package/site/content/docs/elements/store/meta.json +5 -0
  54. package/site/content/docs/elements/store/search.mdx +946 -0
  55. package/site/content/docs/elements/store/sql.mdx +937 -0
  56. package/site/content/docs/elements/vault/config.mdx +256 -0
  57. package/site/content/docs/elements/vault/index.mdx +356 -0
  58. package/site/content/docs/elements/vault/meta.json +5 -0
  59. package/site/content/docs/elements/vault/rotation.mdx +273 -0
  60. package/site/content/docs/elements/vault/secrets.mdx +285 -0
  61. package/site/content/docs/index.mdx +63 -36
  62. package/site/content/docs/meta.json +8 -7
  63. package/site/content/docs/plugins/cors.mdx +2 -1
  64. package/site/content/docs/plugins/csrf.mdx +4 -1
  65. package/site/content/docs/plugins/headers.mdx +2 -1
  66. package/site/content/docs/plugins/ip-allowlist.mdx +2 -1
  67. package/site/content/docs/plugins/magic-link.mdx +6 -0
  68. package/site/content/docs/plugins/maintenance-mode.mdx +2 -1
  69. package/site/content/docs/plugins/oauth.mdx +1 -1
  70. package/site/content/docs/plugins/otp.mdx +12 -1
  71. package/site/content/docs/plugins/passkey.mdx +39 -15
  72. package/site/content/docs/plugins/two-factor.mdx +91 -34
  73. package/site/content/docs/plugins/username.mdx +1 -1
  74. package/site/content/docs/recipes/caddy.mdx +3 -3
  75. package/site/content/docs/recipes/index.mdx +18 -28
  76. package/site/content/docs/recipes/mailpit.mdx +1 -1
  77. package/site/content/docs/recipes/meilisearch.mdx +3 -3
  78. package/site/content/docs/recipes/meta.json +1 -4
  79. package/site/content/docs/recipes/nginx.mdx +2 -2
  80. package/site/content/docs/recipes/openrouter.mdx +283 -0
  81. package/site/content/docs/recipes/pgdog.mdx +3 -3
  82. package/site/content/docs/recipes/postgres.mdx +1 -2
  83. package/site/content/docs/recipes/rustfs.mdx +2 -2
  84. package/site/content/docs/recipes/traefik.mdx +3 -3
  85. package/site/content/docs/reference/cli.mdx +231 -0
  86. package/site/content/docs/reference/configuration.mdx +30 -10
  87. package/site/content/docs/reference/environment-variables.mdx +93 -20
  88. package/site/content/docs/reference/errors.mdx +132 -29
  89. package/site/content/docs/reference/fx.mdx +137 -73
  90. package/site/content/docs/reference/index.mdx +13 -21
  91. package/site/content/docs/reference/meta.json +4 -5
  92. package/site/content/docs/reference/okid.mdx +42 -17
  93. package/site/content/docs/reference/plugins.mdx +4 -3
  94. package/site/content/docs/reference/security.mdx +197 -0
  95. package/site/content/docs/understand/meta.json +5 -0
  96. package/site/content/docs/understand/the-anatomy.mdx +132 -0
  97. package/site/content/docs/understand/the-model.mdx +32 -0
  98. package/site/content/docs/understand/the-problem.mdx +74 -0
  99. package/site/content/docs/understand/the-vocabulary.mdx +26 -0
  100. package/src/auth/api-keys.ts +2 -1
  101. package/src/auth/bindings.ts +64 -16
  102. package/src/auth/gate-auth.test.ts +6 -1
  103. package/src/auth/identity.ts +152 -5
  104. package/src/auth/index.ts +29 -0
  105. package/src/auth/invites.ts +2 -1
  106. package/src/auth/method-context.ts +41 -0
  107. package/src/auth/oauth-as/crypto.ts +2 -1
  108. package/src/auth/oauth-as/stores.ts +2 -1
  109. package/src/auth/operator.ts +2 -1
  110. package/src/auth/sessions-jwt.test.ts +77 -0
  111. package/src/auth/sessions.ts +20 -4
  112. package/src/auth/tenants.ts +3 -2
  113. package/src/auth/two-factor-challenge.test.ts +61 -0
  114. package/src/auth/two-factor-challenge.ts +284 -0
  115. package/src/auth/verification.ts +5 -0
  116. package/src/bench/REPORT.md +49 -0
  117. package/src/bench/g03-signal-once.bench.ts +1 -1
  118. package/src/bench/g10-observability-contention.bench.ts +1 -1
  119. package/src/bench/g17-hybrid-search.bench.ts +572 -0
  120. package/src/bench/load-app.ts +2 -10
  121. package/src/cli/ai-setup/ai-setup.test.ts +331 -42
  122. package/src/cli/ai-setup/apply.ts +294 -52
  123. package/src/cli/ai-setup/catalog.ts +238 -1342
  124. package/src/cli/ai-setup/index.ts +14 -99
  125. package/src/cli/ai-setup/prompts.ts +60 -443
  126. package/src/cli/ask-seed.test.ts +96 -0
  127. package/src/cli/ask-seed.ts +82 -0
  128. package/src/cli/ask-vault-gaps.test.ts +84 -0
  129. package/src/cli/ask-vault-gaps.ts +146 -0
  130. package/src/cli/build.ts +2 -2
  131. package/src/cli/client-add.test.ts +26 -1
  132. package/src/cli/client-add.ts +106 -27
  133. package/src/cli/db-seed.ts +2 -0
  134. package/src/cli/db.ts +150 -11
  135. package/src/cli/dev.ts +119 -211
  136. package/src/cli/docker-clean.ts +2 -2
  137. package/src/cli/docker-cli.test.ts +1 -1
  138. package/src/cli/doctor-diff.ts +4 -2
  139. package/src/cli/doctor-pii.test.ts +1 -1
  140. package/src/cli/doctor.ts +14 -1
  141. package/src/cli/hero-meta.test.ts +4 -2
  142. package/src/cli/load-config.images.test.ts +8 -10
  143. package/src/cli/load-config.ts +1 -1
  144. package/src/cli/registry.ts +22 -10
  145. package/src/cli/replay.ts +3 -1
  146. package/src/cli/start.ts +1 -1
  147. package/src/cli/tui/keys.ts +2 -3
  148. package/src/client/auth/cookies.ts +74 -0
  149. package/src/client/auth/create-auth-client.ts +636 -0
  150. package/src/client/auth/denials.ts +99 -0
  151. package/src/client/auth/session.ts +283 -0
  152. package/src/client/auth.test.ts +251 -0
  153. package/src/client/auth.ts +39 -114
  154. package/src/client/create-with-session.ts +245 -0
  155. package/src/client/create.ts +51 -5
  156. package/src/client/index.ts +11 -1
  157. package/src/client/live.ts +11 -79
  158. package/src/client/notes-contract.test.ts +5 -10
  159. package/src/client/sse.ts +137 -0
  160. package/src/client/stream.ts +147 -0
  161. package/src/client/transport.test.ts +32 -0
  162. package/src/client/transport.ts +123 -26
  163. package/src/client/types.ts +118 -14
  164. package/src/client-react/index.ts +185 -24
  165. package/src/client-react/use-live-query.ts +13 -3
  166. package/src/compiler/aot.test.ts +7 -4
  167. package/src/compiler/effects-embed.test.ts +56 -0
  168. package/src/compiler/effects-fetch.test.ts +43 -0
  169. package/src/compiler/effects-infer.ts +58 -2
  170. package/src/compiler/extract.test.ts +257 -43
  171. package/src/compiler/extract.ts +566 -48
  172. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +7 -4
  173. package/src/compiler/fixtures/skyport/src/flows/bookings/signals.ts +2 -8
  174. package/src/compiler/fixtures/skyport.expected.json +4 -4
  175. package/src/compiler/fixtures/triggers/five-triggers.ts +14 -10
  176. package/src/compiler/response.ts +2 -2
  177. package/src/compiler/search-writer-isolation.test.ts +40 -0
  178. package/src/config/index.ts +11 -0
  179. package/src/console/server/app.ts +17 -23
  180. package/src/console/server/bind.ts +4 -0
  181. package/src/console/server/console.test.ts +2 -2
  182. package/src/console/server/flows-invoke.test.ts +50 -34
  183. package/src/console/server/flows.ts +436 -255
  184. package/src/console/server/runs-ingest.test.ts +6 -3
  185. package/src/console/server/serve.ts +1 -1
  186. package/src/console/server/signals.test.ts +1 -5
  187. package/src/console/server/signals.ts +8 -6
  188. package/src/console/server/state.ts +6 -1
  189. package/src/console/server/store.ts +1 -1
  190. package/src/console/ui-next/dist/assets/FileExportIcon-Ck-5od4R.js +1 -0
  191. package/src/console/ui-next/dist/assets/MoreHorizontalCircle01Icon-gMNGsE37.js +1 -0
  192. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +1 -0
  193. package/src/console/ui-next/dist/assets/UnavailableIcon-D9cvHVPr.js +1 -0
  194. package/src/console/ui-next/dist/assets/UserIcon-DaE2PB5_.js +1 -0
  195. package/src/console/ui-next/dist/assets/access-page-DFeymU07.js +4 -0
  196. package/src/console/ui-next/dist/assets/agent-disclosure-U1rdfblp.js +1 -0
  197. package/src/console/ui-next/dist/assets/cache-glyph-BeFJeqBG.js +1 -0
  198. package/src/console/ui-next/dist/assets/call-pii-button-CVAONPii.js +1 -0
  199. package/src/console/ui-next/dist/assets/collapsible-D2A6NJ-3.js +1 -0
  200. package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +1 -0
  201. package/src/console/ui-next/dist/assets/dagre.esm-B1_XeuLP.js +1 -0
  202. package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +1 -0
  203. package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +1 -0
  204. package/src/console/ui-next/dist/assets/duration-tone-Cgk_h5ja.js +9 -0
  205. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +1 -0
  206. package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +1 -0
  207. package/src/console/ui-next/dist/assets/flows-page-Dss7941e.js +1 -0
  208. package/src/console/ui-next/dist/assets/highlighted-json-MYZQtRnw.js +154 -0
  209. package/src/console/ui-next/dist/assets/http-method-Jrh39p7A.js +1 -0
  210. package/src/console/ui-next/dist/assets/index-D0zS5rKO.css +2 -0
  211. package/src/console/ui-next/dist/assets/index-DXP2dBIF.js +63 -0
  212. package/src/console/ui-next/dist/assets/observability-page-HvolXxTI.js +4 -0
  213. package/src/console/ui-next/dist/assets/react-dom-Ddte4I-Q.js +9 -0
  214. package/src/console/ui-next/dist/assets/replica-lag-CSh2dzrb.js +18 -0
  215. package/src/console/ui-next/dist/assets/request-meta-BatF8KrK.js +1 -0
  216. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +1 -0
  217. package/src/console/ui-next/dist/assets/{sql-BskegiFM.js → sql-BsFa4tDR.js} +1 -1
  218. package/src/console/ui-next/dist/assets/store-page-eiKiHnNe.js +41 -0
  219. package/src/console/ui-next/dist/assets/trace-detail-sheet-CZkMeKS-.js +2 -0
  220. package/src/console/ui-next/dist/assets/tree-expand-toggle-CW8y5A2h.js +55 -0
  221. package/src/console/ui-next/dist/assets/units-page-B_RWJrEO.js +1 -0
  222. package/src/console/ui-next/dist/assets/{use-vault-list-uk4WVboC.js → use-vault-list-CT4-gajj.js} +1 -1
  223. package/src/console/ui-next/dist/assets/vault-page-CWrg-A68.js +2 -0
  224. package/src/console/ui-next/dist/assets/xyflow-CSyC6ryz.css +1 -0
  225. package/src/console/ui-next/dist/assets/xyflow-yApv7D4e.js +7 -0
  226. package/src/console/ui-next/dist/index.html +6 -13
  227. package/src/console/ui-next/seed-invoke-host.ts +21 -18
  228. package/src/console/ui-next/src/client.ts +7 -2
  229. package/src/console/ui-next/src/features/flows/graph/element-map.ts +2 -0
  230. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +12 -2
  231. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +4 -0
  232. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +8 -0
  233. package/src/console/ui-next/src/features/flows/traces/waterfall-bars.ts +4 -0
  234. package/src/console/ui-next/src/features/flows/traces/waterfall-tooltip.ts +9 -3
  235. package/src/console/ui-next/src/features/store/detail/reveal-cell.tsx +7 -11
  236. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +2 -2
  237. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +2 -0
  238. package/src/console/ui-next/src/features/store/lib/grid-model.test.ts +25 -1
  239. package/src/console/ui-next/src/features/store/lib/grid-model.ts +36 -1
  240. package/src/console/ui-next/src/features/store/query/query-results.tsx +3 -3
  241. package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +2 -0
  242. package/src/console/ui-next/src/features/units/lib/call-read-safe.ts +3 -1
  243. package/src/console/ui-next/src/features/units/lib/fields-from-schema.ts +6 -0
  244. package/src/docker/ai-model-status.test.ts +1 -12
  245. package/src/docker/ai-model-status.ts +10 -38
  246. package/src/docker/compose-up.test.ts +100 -0
  247. package/src/docker/compose-up.ts +292 -0
  248. package/src/docker/compose.ts +4 -14
  249. package/src/docker/derive.ts +9 -34
  250. package/src/docker/docker.test.ts +16 -261
  251. package/src/docker/images-config.test.ts +13 -25
  252. package/src/docker/index.ts +1 -24
  253. package/src/docker/recipes/index.ts +0 -23
  254. package/src/docker/recipes/pgdog.ts +6 -4
  255. package/src/docker/stack-id.test.ts +2 -2
  256. package/src/docker/stack-id.ts +0 -1
  257. package/src/docker/types.ts +2 -2
  258. package/src/drivers/ai-anthropic.ts +10 -0
  259. package/src/drivers/ai-openai-compatible.ts +34 -7
  260. package/src/drivers/ai-providers.test.ts +0 -107
  261. package/src/drivers/ai-stream.test.ts +0 -34
  262. package/src/drivers/ai-types.ts +17 -8
  263. package/src/drivers/channel-fcm.ts +6 -1
  264. package/src/drivers/channel-msegat.ts +6 -1
  265. package/src/drivers/channel-resend.ts +5 -1
  266. package/src/drivers/channel-smtp.ts +5 -1
  267. package/src/drivers/channel-sndr.ts +10 -1
  268. package/src/drivers/channel-taqnyat-mail.ts +5 -1
  269. package/src/drivers/channel-taqnyat-whatsapp.ts +6 -1
  270. package/src/drivers/channel-taqnyat.ts +6 -1
  271. package/src/drivers/channel-types.ts +10 -0
  272. package/src/drivers/channel-unifonic.ts +6 -1
  273. package/src/drivers/channel-wa-cloud.ts +6 -1
  274. package/src/drivers/channel-webpush.ts +6 -1
  275. package/src/drivers/external.ts +33 -0
  276. package/src/drivers/index.ts +0 -10
  277. package/src/drivers/meilisearch.ts +6 -0
  278. package/src/drivers/oauth-types.ts +4 -0
  279. package/src/drivers/postgres.test.ts +74 -3
  280. package/src/drivers/postgres.ts +175 -6
  281. package/src/drivers/signal-types.ts +5 -5
  282. package/src/drivers/types.ts +7 -0
  283. package/src/drivers/vault-types.ts +5 -0
  284. package/src/elements/ai/declare.ts +17 -4
  285. package/src/elements/ai/eval.ts +1 -1
  286. package/src/elements/ai/mcp-http.ts +1 -1
  287. package/src/elements/ai/pii.ts +1 -1
  288. package/src/elements/ai/providers.test.ts +289 -0
  289. package/src/elements/ai/providers.ts +169 -0
  290. package/src/elements/ai/runtime.ts +68 -5
  291. package/src/elements/ai/schema.ts +2 -2
  292. package/src/elements/ai.test.ts +7 -7
  293. package/src/elements/ai.ts +17 -1
  294. package/src/elements/channel/runtime.ts +105 -12
  295. package/src/elements/clock/cron-fields.test.ts +144 -0
  296. package/src/elements/clock/cron-fields.ts +185 -0
  297. package/src/elements/clock/declare.ts +247 -3
  298. package/src/elements/clock.ts +21 -1
  299. package/src/elements/index.ts +15 -0
  300. package/src/elements/signal/chaos-child.ts +1 -2
  301. package/src/elements/signal/declare.ts +65 -37
  302. package/src/elements/signal/delivery-modes.test.ts +11 -31
  303. package/src/elements/signal/dry-run-replay.test.ts +1 -7
  304. package/src/elements/signal/dry-run-write-isolation.test.ts +1 -7
  305. package/src/elements/signal/key-ordering.test.ts +6 -26
  306. package/src/elements/signal/lease-reclaim.test.ts +2 -6
  307. package/src/elements/signal/optional-emit.test.ts +6 -9
  308. package/src/elements/signal/order-lifecycle.test.ts +4 -17
  309. package/src/elements/signal/orphan-messages.test.ts +4 -15
  310. package/src/elements/signal/reconcile.test.ts +2 -2
  311. package/src/elements/signal/runtime.ts +1 -1
  312. package/src/elements/signal/schema-emit.test.ts +4 -8
  313. package/src/elements/signal.test.ts +24 -49
  314. package/src/elements/signal.ts +9 -2
  315. package/src/elements/store/domain-ddl.test.ts +1 -1
  316. package/src/elements/store/live-http.test.ts +9 -4
  317. package/src/elements/store/prepare-row.test.ts +123 -0
  318. package/src/elements/store/resource.ts +43 -12
  319. package/src/elements/store/schema-decl.ts +117 -0
  320. package/src/elements/store/search-backfill.ts +205 -0
  321. package/src/elements/store/search-bind.ts +66 -0
  322. package/src/elements/store/search-bm25.ts +78 -0
  323. package/src/elements/store/search-ddl.ts +156 -0
  324. package/src/elements/store/search-embed-flow.ts +141 -0
  325. package/src/elements/store/search-errors.ts +34 -0
  326. package/src/elements/store/search-fusion.ts +112 -0
  327. package/src/elements/store/search-lsh.ts +179 -0
  328. package/src/elements/store/search-runtime.ts +300 -0
  329. package/src/elements/store/search.test.ts +144 -0
  330. package/src/elements/store/sql-session.test.ts +1 -0
  331. package/src/elements/store/sql-session.ts +83 -3
  332. package/src/elements/store/table.ts +23 -2
  333. package/src/elements/store/upsert-app.test.ts +1 -3
  334. package/src/elements/store.ts +50 -0
  335. package/src/full.ts +4 -2
  336. package/src/http.ts +4 -1
  337. package/src/i18n/catalogs/ar.ts +14 -6
  338. package/src/i18n/catalogs/en.ts +14 -6
  339. package/src/index.ts +6 -1
  340. package/src/kernel/adopt-barrel-fresh.test.ts +7 -7
  341. package/src/kernel/adopt-routes.ts +9 -1
  342. package/src/kernel/app-auth.ts +5 -0
  343. package/src/kernel/app.ts +102 -9
  344. package/src/kernel/auto-cache.test.ts +6 -12
  345. package/src/kernel/auto-registry.test.ts +3 -3
  346. package/src/kernel/boot-bind/ai.test.ts +22 -19
  347. package/src/kernel/boot-bind/ai.ts +5 -43
  348. package/src/kernel/boot-bind/clock.ts +23 -5
  349. package/src/kernel/boot-bind/honor-config.test.ts +4 -4
  350. package/src/kernel/boot.test.ts +11 -6
  351. package/src/kernel/boot.ts +66 -16
  352. package/src/kernel/boundary-contract.ts +93 -0
  353. package/src/kernel/budget.test.ts +1 -1
  354. package/src/kernel/call.ts +89 -0
  355. package/src/kernel/capability.ts +6 -0
  356. package/src/kernel/client-descriptor.ts +123 -0
  357. package/src/kernel/clock-timezone.test.ts +80 -0
  358. package/src/kernel/correlation.test.ts +1 -1
  359. package/src/kernel/dry-run.ts +11 -8
  360. package/src/kernel/effects-stamping.test.ts +42 -9
  361. package/src/kernel/effects.test.ts +4 -2
  362. package/src/kernel/effects.ts +51 -9
  363. package/src/kernel/errors-channel.ts +17 -0
  364. package/src/kernel/errors-live-resume.ts +3 -2
  365. package/src/kernel/errors-tenant.ts +7 -4
  366. package/src/kernel/errors.registry-helpers.ts +96 -0
  367. package/src/kernel/errors.registry.test.ts +139 -25
  368. package/src/kernel/errors.ts +102 -23
  369. package/src/kernel/external-effects.test.ts +196 -0
  370. package/src/kernel/flow.test.ts +13 -4
  371. package/src/kernel/flow.ts +69 -71
  372. package/src/kernel/fx-ask-telemetry.test.ts +2 -2
  373. package/src/kernel/fx-dead-letters.test.ts +3 -3
  374. package/src/kernel/fx-fetch.ts +55 -0
  375. package/src/kernel/fx-live-stream.ts +2 -2
  376. package/src/kernel/fx-live.test.ts +8 -8
  377. package/src/kernel/fx.test.ts +17 -11
  378. package/src/kernel/fx.ts +180 -57
  379. package/src/kernel/horizontal-child.ts +1 -1
  380. package/src/kernel/http-query.test.ts +3 -6
  381. package/src/kernel/index.ts +13 -3
  382. package/src/kernel/instance-id.ts +1 -1
  383. package/src/kernel/instances.test.ts +40 -0
  384. package/src/kernel/instances.ts +47 -9
  385. package/src/kernel/live-http.test.ts +6 -7
  386. package/src/kernel/live-http.ts +6 -2
  387. package/src/kernel/live-resume.test.ts +1 -1
  388. package/src/kernel/mcp-tool.test.ts +3 -3
  389. package/src/kernel/on.ts +98 -4
  390. package/src/kernel/pipeline.test.ts +12 -15
  391. package/src/kernel/plugin-elements.test.ts +1 -1
  392. package/src/kernel/stamp-http.test.ts +3 -3
  393. package/src/kernel/triggers.ts +210 -95
  394. package/src/kernel-entry.ts +0 -1
  395. package/src/manifest/diff.ts +11 -1
  396. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  397. package/src/manifest/types.ts +46 -2
  398. package/src/mcp/docs-index.ts +3 -3
  399. package/src/mcp/docs-mcp.test.ts +2 -2
  400. package/src/mcp/docs-tools.ts +1 -1
  401. package/src/okid.test.ts +56 -0
  402. package/src/okid.ts +55 -11
  403. package/src/plugins/anonymous.ts +8 -4
  404. package/src/plugins/auth/shared.ts +27 -3
  405. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  406. package/src/plugins/auth-methods.security.test.ts +561 -23
  407. package/src/plugins/config-source.ts +1 -1
  408. package/src/plugins/csrf.test.ts +65 -0
  409. package/src/plugins/index.ts +2 -0
  410. package/src/plugins/magic-link.ts +36 -16
  411. package/src/plugins/oauth/shared.ts +3 -1
  412. package/src/plugins/oauth.ts +22 -14
  413. package/src/plugins/otp.ts +37 -21
  414. package/src/plugins/passkey-webauthn.ts +9 -6
  415. package/src/plugins/passkey.ts +98 -26
  416. package/src/plugins/pre-account-hijack.test.ts +223 -0
  417. package/src/plugins/two-factor.ts +466 -37
  418. package/src/plugins/username.ts +19 -7
  419. package/src/release/build-lib.ts +1 -0
  420. package/src/release/limits.ts +2 -2
  421. package/src/release/measure.ts +1 -1
  422. package/src/runs/collect.test.ts +0 -2
  423. package/src/runtime/json-code-block.test.ts +250 -20
  424. package/src/runtime/json-code-block.ts +1261 -41
  425. package/src/term.test.ts +1 -1
  426. package/src/test/create-test-app.test.ts +12 -12
  427. package/src/test/live-signals.test.ts +6 -6
  428. package/src/test/provisions.integration.test.ts +10 -14
  429. package/src/test/reset-element-registries.ts +1 -1
  430. package/src/test/tenant-isolation.test.ts +12 -6
  431. package/site/content/docs/deployment/docker-swarm.mdx +0 -164
  432. package/site/content/docs/deployment/docker.mdx +0 -227
  433. package/site/content/docs/deployment/index.mdx +0 -83
  434. package/site/content/docs/deployment/kubernetes.mdx +0 -176
  435. package/site/content/docs/deployment/meta.json +0 -5
  436. package/site/content/docs/deployment/reverse-proxy.mdx +0 -234
  437. package/site/content/docs/elements/ai.mdx +0 -385
  438. package/site/content/docs/elements/channel.mdx +0 -346
  439. package/site/content/docs/elements/clock.mdx +0 -244
  440. package/site/content/docs/elements/flow.mdx +0 -420
  441. package/site/content/docs/elements/gate.mdx +0 -436
  442. package/site/content/docs/elements/signal.mdx +0 -380
  443. package/site/content/docs/elements/store.mdx +0 -1099
  444. package/site/content/docs/elements/vault.mdx +0 -405
  445. package/site/content/docs/get-started/basic-usage.mdx +0 -173
  446. package/site/content/docs/get-started/index.mdx +0 -43
  447. package/site/content/docs/get-started/installation.mdx +0 -220
  448. package/site/content/docs/get-started/introduction.mdx +0 -144
  449. package/site/content/docs/get-started/meta.json +0 -13
  450. package/site/content/docs/get-started/project-structure.mdx +0 -925
  451. package/site/content/docs/get-started/testing.mdx +0 -328
  452. package/site/content/docs/get-started/why.mdx +0 -114
  453. package/site/content/docs/recipes/llama-cpp.mdx +0 -151
  454. package/site/content/docs/recipes/ollama.mdx +0 -142
  455. package/site/content/docs/recipes/sglang.mdx +0 -105
  456. package/site/content/docs/recipes/vllm.mdx +0 -106
  457. package/site/content/docs/reference/cli.md +0 -232
  458. package/site/content/docs/reference/client.mdx +0 -469
  459. package/site/content/docs/reference/security.md +0 -74
  460. package/src/cli/ai-setup/detect-ollama.ts +0 -213
  461. package/src/cli/ai-setup/recommend.test.ts +0 -225
  462. package/src/cli/ai-setup/recommend.ts +0 -225
  463. package/src/cli/dev-controls.test.ts +0 -122
  464. package/src/cli/dev-controls.ts +0 -164
  465. package/src/cli/tui/DevLive.tsx +0 -126
  466. package/src/console/ui-next/dist/assets/access-page-3-EFj-2G.js +0 -4
  467. package/src/console/ui-next/dist/assets/agent-disclosure-BP0Y0Sux.js +0 -1
  468. package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +0 -1
  469. package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +0 -1
  470. package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +0 -1
  471. package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +0 -1
  472. package/src/console/ui-next/dist/assets/dagre.esm-ZwcdTuZZ.js +0 -1
  473. package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +0 -1
  474. package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +0 -1
  475. package/src/console/ui-next/dist/assets/duration-tone-sC3lGABz.js +0 -9
  476. package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +0 -1
  477. package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +0 -1
  478. package/src/console/ui-next/dist/assets/flows-page-cVFnA4HH.js +0 -1
  479. package/src/console/ui-next/dist/assets/highlighted-json-Awq7gYdu.js +0 -154
  480. package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +0 -1
  481. package/src/console/ui-next/dist/assets/index-CMIgUbD0.js +0 -66
  482. package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +0 -2
  483. package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +0 -1
  484. package/src/console/ui-next/dist/assets/observability-page-CQ3p34ip.js +0 -4
  485. package/src/console/ui-next/dist/assets/preload-helper-oH4irX4C.js +0 -1
  486. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +0 -1
  487. package/src/console/ui-next/dist/assets/react-dom-Bph1y7z7.js +0 -9
  488. package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +0 -18
  489. package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +0 -1
  490. package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +0 -1
  491. package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +0 -1
  492. package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +0 -1
  493. package/src/console/ui-next/dist/assets/store-page-02xOiqIK.js +0 -41
  494. package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +0 -2
  495. package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +0 -55
  496. package/src/console/ui-next/dist/assets/units-page-l8FeKfnP.js +0 -1
  497. package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +0 -1
  498. package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +0 -1
  499. package/src/console/ui-next/dist/assets/vault-page-CL-d_mLE.js +0 -2
  500. package/src/console/ui-next/dist/assets/xyflow-D7n4g6go.js +0 -7
  501. package/src/console/ui-next/dist/assets/xyflow-DZ0Ws1xk.css +0 -1
  502. package/src/docker/ollama-pull.ts +0 -232
  503. package/src/docker/recipes/llama-cpp.ts +0 -298
  504. package/src/docker/recipes/ollama.ts +0 -44
  505. package/src/docker/recipes/sglang.ts +0 -55
  506. package/src/docker/recipes/vllm.ts +0 -44
  507. package/src/drivers/ai-ollama-tools.integration.test.ts +0 -109
  508. package/src/drivers/ai-ollama.integration.test.ts +0 -184
  509. package/src/drivers/ai-ollama.ts +0 -389
  510. 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>