okengine 0.2.6 → 0.2.8

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 (584) hide show
  1. package/AGENTS.md +24 -23
  2. package/README.md +52 -346
  3. package/docs/spec/console.md +165 -142
  4. package/docs/spec/example.md +490 -312
  5. package/docs/spec/four-applications.md +495 -314
  6. package/docs/spec/unified-theory.md +184 -175
  7. package/package.json +45 -37
  8. package/spec/manifest.v1.schema.json +40 -9
  9. package/src/auth/api-keys.ts +2 -5
  10. package/src/auth/attenuation.ts +2 -6
  11. package/src/auth/auth.test.ts +53 -31
  12. package/src/auth/invoke-as.ts +1 -3
  13. package/src/auth/operator.ts +8 -26
  14. package/src/auth/planes.ts +2 -7
  15. package/src/auth/plugin.ts +4 -7
  16. package/src/auth/roles.ts +1 -5
  17. package/src/auth/sessions.ts +10 -39
  18. package/src/cli/args.ts +1 -5
  19. package/src/cli/ask-dev-mode.ts +40 -0
  20. package/src/cli/build.ts +1 -2
  21. package/src/cli/client-add.test.ts +8 -15
  22. package/src/cli/client-add.ts +5 -14
  23. package/src/cli/completion.test.ts +1 -5
  24. package/src/cli/completion.ts +4 -17
  25. package/src/cli/db-auto-push.test.ts +64 -0
  26. package/src/cli/db-auto-push.ts +72 -0
  27. package/src/cli/db.test.ts +189 -0
  28. package/src/cli/db.ts +386 -0
  29. package/src/cli/dev-app-runner.ts +2 -8
  30. package/src/cli/dev-db-push.test.ts +163 -0
  31. package/src/cli/dev-mode.test.ts +45 -0
  32. package/src/cli/dev-mode.ts +78 -0
  33. package/src/cli/dev.test.ts +148 -21
  34. package/src/cli/dev.ts +208 -134
  35. package/src/cli/doc-drift.test.ts +3 -2
  36. package/src/cli/doc-drift.ts +21 -50
  37. package/src/cli/doc-staleness.test.ts +69 -0
  38. package/src/cli/docker-cli.test.ts +7 -8
  39. package/src/cli/docker.ts +1 -3
  40. package/src/cli/doctor-diff-examples.ts +1 -3
  41. package/src/cli/doctor-diff.test.ts +1 -5
  42. package/src/cli/doctor-diff.ts +17 -46
  43. package/src/cli/doctor-pii.ts +2 -7
  44. package/src/cli/doctor.test.ts +26 -4
  45. package/src/cli/doctor.ts +35 -11
  46. package/src/cli/eval.ts +4 -14
  47. package/src/cli/gates-list.ts +1 -4
  48. package/src/cli/hero-meta.test.ts +17 -19
  49. package/src/cli/hero-meta.ts +20 -30
  50. package/src/cli/images.ts +3 -8
  51. package/src/cli/index.ts +11 -2
  52. package/src/cli/load-config.images.test.ts +14 -14
  53. package/src/cli/load-config.ts +41 -47
  54. package/src/cli/mcp-from-console.ts +1 -4
  55. package/src/cli/mode.ts +45 -0
  56. package/src/cli/ports.test.ts +4 -13
  57. package/src/cli/ports.ts +1 -3
  58. package/src/cli/privacy-erase.ts +1 -3
  59. package/src/cli/registry.ts +75 -25
  60. package/src/cli/schema.ts +3 -10
  61. package/src/cli/stack.ts +2 -7
  62. package/src/cli/start.ts +4 -15
  63. package/src/cli/upgrade.ts +1 -4
  64. package/src/client/budget-entry.ts +1 -6
  65. package/src/client/create.test.ts +6 -12
  66. package/src/client/create.ts +2 -8
  67. package/src/client/errors.ts +1 -4
  68. package/src/client/index.ts +1 -6
  69. package/src/client/notes-contract.test.ts +4 -13
  70. package/src/client/transport.ts +9 -30
  71. package/src/client/types.ts +8 -29
  72. package/src/compiler/aot.test.ts +2 -9
  73. package/src/compiler/aot.ts +1 -4
  74. package/src/compiler/differential.test.ts +3 -11
  75. package/src/compiler/dynamic.ts +1 -4
  76. package/src/compiler/effects-infer.ts +8 -34
  77. package/src/compiler/emit.ts +1 -3
  78. package/src/compiler/extract.test.ts +96 -30
  79. package/src/compiler/extract.ts +362 -156
  80. package/src/compiler/fixtures/skyport/oke.config.ts +1 -1
  81. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +7 -3
  82. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -3
  83. package/src/compiler/fixtures/skyport/src/flows/support/index.ts +4 -1
  84. package/src/compiler/fixtures/skyport.expected.json +24 -83
  85. package/src/compiler/fixtures/triggers/five-triggers.ts +1 -2
  86. package/src/compiler/http-parse.ts +2 -9
  87. package/src/compiler/index.ts +1 -5
  88. package/src/compiler/response.ts +4 -7
  89. package/src/compiler/sucrose.ts +11 -31
  90. package/src/config/define-config.test.ts +57 -34
  91. package/src/config/index.ts +181 -37
  92. package/src/config/resolve-driver.test.ts +28 -17
  93. package/src/console/budget.test.ts +1 -4
  94. package/src/console/index.ts +1 -4
  95. package/src/console/server/access.test.ts +15 -43
  96. package/src/console/server/access.ts +19 -49
  97. package/src/console/server/ai.test.ts +3 -11
  98. package/src/console/server/ai.ts +23 -46
  99. package/src/console/server/app.ts +14 -45
  100. package/src/console/server/auth-rate.test.ts +4 -12
  101. package/src/console/server/bind.ts +1 -5
  102. package/src/console/server/channels.test.ts +2 -6
  103. package/src/console/server/channels.ts +10 -35
  104. package/src/console/server/claim.ts +2 -8
  105. package/src/console/server/clock.test.ts +2 -6
  106. package/src/console/server/clock.ts +8 -26
  107. package/src/console/server/console.test.ts +1 -3
  108. package/src/console/server/diff.test.ts +5 -17
  109. package/src/console/server/diff.ts +10 -31
  110. package/src/console/server/dry-run.audit.test.ts +2 -8
  111. package/src/console/server/flows.ts +64 -233
  112. package/src/console/server/gates.test.ts +14 -50
  113. package/src/console/server/gates.ts +13 -48
  114. package/src/console/server/index.ts +2 -9
  115. package/src/console/server/live.ts +3 -12
  116. package/src/console/server/operator-db.test.ts +4 -15
  117. package/src/console/server/operator-db.ts +13 -32
  118. package/src/console/server/plugin.ts +50 -53
  119. package/src/console/server/plugins.test.ts +2 -6
  120. package/src/console/server/plugins.ts +17 -56
  121. package/src/console/server/runs-pii.ts +2 -7
  122. package/src/console/server/security.gate.test.ts +18 -40
  123. package/src/console/server/serve.ts +10 -25
  124. package/src/console/server/signals.test.ts +1 -3
  125. package/src/console/server/signals.ts +17 -40
  126. package/src/console/server/state.ts +21 -59
  127. package/src/console/server/store.test.ts +20 -22
  128. package/src/console/server/store.ts +31 -76
  129. package/src/console/server/vault.test.ts +12 -29
  130. package/src/console/server/vault.ts +23 -60
  131. package/src/console/ui/access/AccessA11yView.tsx +6 -21
  132. package/src/console/ui/access/a11y.test.tsx +3 -12
  133. package/src/console/ui/access/acknowledgement.test.ts +1 -4
  134. package/src/console/ui/access/acknowledgement.ts +1 -3
  135. package/src/console/ui/access/blast-radius.ts +1 -3
  136. package/src/console/ui/access/confirmation.test.ts +1 -5
  137. package/src/console/ui/access/confirmation.ts +1 -4
  138. package/src/console/ui/access/fixture.ts +1 -5
  139. package/src/console/ui/access/grantable.test.ts +1 -3
  140. package/src/console/ui/access/grantable.ts +2 -7
  141. package/src/console/ui/access/index.ts +2 -9
  142. package/src/console/ui/access/provenance.test.ts +1 -3
  143. package/src/console/ui/access/provenance.ts +2 -6
  144. package/src/console/ui/access/search.test.ts +1 -5
  145. package/src/console/ui/access/search.ts +2 -6
  146. package/src/console/ui/ai/AiA11yView.tsx +6 -17
  147. package/src/console/ui/ai/a11y.test.tsx +2 -5
  148. package/src/console/ui/ai/fixture.ts +2 -7
  149. package/src/console/ui/ai/format.ts +1 -3
  150. package/src/console/ui/ai/group.test.ts +1 -3
  151. package/src/console/ui/ai/group.ts +4 -12
  152. package/src/console/ui/ai/index.ts +2 -9
  153. package/src/console/ui/ai/promotion.test.ts +2 -7
  154. package/src/console/ui/ai/promotion.ts +4 -13
  155. package/src/console/ui/ai/search.test.ts +2 -11
  156. package/src/console/ui/ai/search.ts +3 -13
  157. package/src/console/ui/ai/types.ts +1 -8
  158. package/src/console/ui/architecture/ArchitectureA11yView.tsx +8 -29
  159. package/src/console/ui/architecture/a11y.test.tsx +1 -2
  160. package/src/console/ui/architecture/declared.ts +1 -5
  161. package/src/console/ui/architecture/index.ts +7 -35
  162. package/src/console/ui/architecture/layout.ts +1 -3
  163. package/src/console/ui/architecture/pathologies.ts +10 -19
  164. package/src/console/ui/architecture/search.ts +6 -15
  165. package/src/console/ui/architecture/traffic.ts +4 -14
  166. package/src/console/ui/architecture/types.ts +2 -10
  167. package/src/console/ui/architecture/view.test.ts +7 -19
  168. package/src/console/ui/architecture/view.ts +9 -36
  169. package/src/console/ui/channels/ChannelsA11yView.tsx +4 -13
  170. package/src/console/ui/channels/a11y.test.tsx +1 -2
  171. package/src/console/ui/channels/confirmation.test.ts +1 -4
  172. package/src/console/ui/channels/confirmation.ts +1 -4
  173. package/src/console/ui/channels/findings.ts +1 -5
  174. package/src/console/ui/channels/index.ts +2 -8
  175. package/src/console/ui/channels/locale.test.ts +1 -3
  176. package/src/console/ui/channels/locale.ts +2 -8
  177. package/src/console/ui/channels/search.ts +4 -13
  178. package/src/console/ui/channels/taxonomy.ts +1 -3
  179. package/src/console/ui/clock/ClockA11yView.tsx +9 -26
  180. package/src/console/ui/clock/a11y.test.tsx +2 -5
  181. package/src/console/ui/clock/confirmation.test.ts +1 -3
  182. package/src/console/ui/clock/confirmation.ts +1 -4
  183. package/src/console/ui/clock/findings.ts +1 -4
  184. package/src/console/ui/clock/health.ts +2 -7
  185. package/src/console/ui/clock/index.ts +2 -10
  186. package/src/console/ui/clock/search.test.ts +1 -3
  187. package/src/console/ui/clock/search.ts +2 -6
  188. package/src/console/ui/clock/timeline.test.ts +1 -3
  189. package/src/console/ui/clock/waiting-on.test.ts +1 -5
  190. package/src/console/ui/clock/waiting-on.ts +2 -7
  191. package/src/console/ui/diff/DiffA11yView.tsx +8 -17
  192. package/src/console/ui/diff/a11y.test.tsx +2 -5
  193. package/src/console/ui/diff/group.test.ts +3 -13
  194. package/src/console/ui/diff/group.ts +2 -8
  195. package/src/console/ui/diff/index.ts +2 -8
  196. package/src/console/ui/diff/search.test.ts +1 -5
  197. package/src/console/ui/diff/search.ts +4 -16
  198. package/src/console/ui/diff/summary.ts +1 -2
  199. package/src/console/ui/dist/assets/{index-Dy4jht9P.js → index-BWo8R7NR.js} +2 -2
  200. package/src/console/ui/dist/assets/{panel-access-Dd37LU2c.js → panel-access-C0J2D-a2.js} +1 -1
  201. package/src/console/ui/dist/assets/{panel-ai-CC7LR6-J.js → panel-ai-D_m6WQI8.js} +1 -1
  202. package/src/console/ui/dist/assets/{panel-architecture-B5b3iKCz.js → panel-architecture-CKnXFyUx.js} +1 -1
  203. package/src/console/ui/dist/assets/{panel-channels-CeNjTKXp.js → panel-channels-BOmQ-onL.js} +1 -1
  204. package/src/console/ui/dist/assets/{panel-clock-DgFTLoHV.js → panel-clock-giAq0Ccv.js} +1 -1
  205. package/src/console/ui/dist/assets/{panel-diff-DxehccqB.js → panel-diff-cdonmH8c.js} +1 -1
  206. package/src/console/ui/dist/assets/{panel-flows-BtrVn-Eg.js → panel-flows-DlCU5zjA.js} +2 -2
  207. package/src/console/ui/dist/assets/{panel-gates-Z9MKRGdH.js → panel-gates-XclZxWD5.js} +1 -1
  208. package/src/console/ui/dist/assets/{panel-overview-Dt_AeXgd.js → panel-overview-BznEOTnb.js} +1 -1
  209. package/src/console/ui/dist/assets/{panel-plugins-DWd0TowH.js → panel-plugins-CcGM1g64.js} +1 -1
  210. package/src/console/ui/dist/assets/{panel-runs-DGstFHeq.js → panel-runs-CGWNHLR4.js} +1 -1
  211. package/src/console/ui/dist/assets/{panel-signals-DzEa2Fnt.js → panel-signals-CNywkdak.js} +1 -1
  212. package/src/console/ui/dist/assets/{panel-store-BJkbNgxx.js → panel-store-KmTbFHMH.js} +1 -1
  213. package/src/console/ui/dist/assets/{panel-traces-BaRVO3gM.js → panel-traces-DBLx2ilD.js} +1 -1
  214. package/src/console/ui/dist/assets/panel-vault-CEnFc0dk.js +1 -0
  215. package/src/console/ui/dist/index.html +1 -1
  216. package/src/console/ui/flows/FlowsA11yView.tsx +7 -22
  217. package/src/console/ui/flows/a11y.test.tsx +2 -8
  218. package/src/console/ui/flows/buffer.test.ts +3 -11
  219. package/src/console/ui/flows/buffer.ts +4 -11
  220. package/src/console/ui/flows/confirmation.ts +1 -3
  221. package/src/console/ui/flows/contract.test.ts +3 -7
  222. package/src/console/ui/flows/contract.ts +12 -36
  223. package/src/console/ui/flows/graph.test.ts +3 -11
  224. package/src/console/ui/flows/graph.ts +5 -15
  225. package/src/console/ui/flows/index.ts +1 -6
  226. package/src/console/ui/flows/save-as-test.ts +1 -3
  227. package/src/console/ui/flows/search.ts +8 -25
  228. package/src/console/ui/flows/tiers.ts +2 -9
  229. package/src/console/ui/gates/GatesA11yView.tsx +4 -13
  230. package/src/console/ui/gates/a11y.test.tsx +2 -5
  231. package/src/console/ui/gates/audit.ts +1 -3
  232. package/src/console/ui/gates/denial.test.ts +1 -3
  233. package/src/console/ui/gates/denial.ts +4 -12
  234. package/src/console/ui/gates/findings.ts +1 -3
  235. package/src/console/ui/gates/group.test.ts +3 -3
  236. package/src/console/ui/gates/group.ts +3 -11
  237. package/src/console/ui/gates/index.ts +2 -10
  238. package/src/console/ui/gates/search.ts +3 -10
  239. package/src/console/ui/overview/OverviewA11yView.tsx +9 -27
  240. package/src/console/ui/overview/a11y.test.tsx +1 -2
  241. package/src/console/ui/overview/busiest.ts +1 -3
  242. package/src/console/ui/overview/compose.ts +1 -13
  243. package/src/console/ui/overview/cost.ts +34 -38
  244. package/src/console/ui/overview/fixture.ts +21 -26
  245. package/src/console/ui/overview/golden.ts +4 -14
  246. package/src/console/ui/overview/index.ts +2 -9
  247. package/src/console/ui/overview/rank.test.ts +3 -10
  248. package/src/console/ui/overview/rank.ts +2 -7
  249. package/src/console/ui/overview/slo.test.ts +5 -9
  250. package/src/console/ui/overview/slo.ts +6 -23
  251. package/src/console/ui/overview/verdict.ts +3 -10
  252. package/src/console/ui/plugins/PluginsA11yView.tsx +8 -18
  253. package/src/console/ui/plugins/a11y.test.tsx +1 -2
  254. package/src/console/ui/plugins/fixture.ts +2 -4
  255. package/src/console/ui/plugins/group.test.ts +1 -3
  256. package/src/console/ui/plugins/group.ts +1 -6
  257. package/src/console/ui/plugins/index.ts +3 -13
  258. package/src/console/ui/plugins/search.ts +7 -19
  259. package/src/console/ui/runs/RunsA11yView.tsx +5 -20
  260. package/src/console/ui/runs/a11y.test.tsx +2 -5
  261. package/src/console/ui/runs/explain.ts +2 -6
  262. package/src/console/ui/runs/group.ts +3 -12
  263. package/src/console/ui/runs/histogram.test.ts +2 -11
  264. package/src/console/ui/runs/histogram.ts +1 -4
  265. package/src/console/ui/runs/index.ts +4 -18
  266. package/src/console/ui/runs/project.ts +2 -6
  267. package/src/console/ui/runs/query.test.ts +1 -3
  268. package/src/console/ui/runs/query.ts +12 -35
  269. package/src/console/ui/runs/search.ts +5 -19
  270. package/src/console/ui/runs/trace-link.test.ts +2 -6
  271. package/src/console/ui/runs/trace-link.ts +4 -16
  272. package/src/console/ui/runs/types.ts +1 -3
  273. package/src/console/ui/shell/App.tsx +2 -10
  274. package/src/console/ui/shell/client.ts +22 -115
  275. package/src/console/ui/shell/components/ui.tsx +1 -4
  276. package/src/console/ui/shell/layout/Shell.tsx +2 -4
  277. package/src/console/ui/shell/main.tsx +31 -96
  278. package/src/console/ui/shell/panels/access/AccessPanel.tsx +34 -98
  279. package/src/console/ui/shell/panels/ai/AiPanel.tsx +35 -106
  280. package/src/console/ui/shell/panels/architecture/ArchitecturePanel.tsx +18 -66
  281. package/src/console/ui/shell/panels/channels/ChannelsPanel.tsx +20 -57
  282. package/src/console/ui/shell/panels/clock/ClockPanel.tsx +22 -54
  283. package/src/console/ui/shell/panels/diff/DiffPanel.tsx +20 -61
  284. package/src/console/ui/shell/panels/flows/ContractEditor.tsx +6 -23
  285. package/src/console/ui/shell/panels/flows/FlowDrawer.tsx +13 -28
  286. package/src/console/ui/shell/panels/flows/FlowsPanel.tsx +9 -29
  287. package/src/console/ui/shell/panels/gates/GatesPanel.tsx +24 -80
  288. package/src/console/ui/shell/panels/overview/OverviewPanel.tsx +20 -58
  289. package/src/console/ui/shell/panels/plugins/PluginsPanel.tsx +16 -41
  290. package/src/console/ui/shell/panels/runs/RunsPanel.tsx +31 -99
  291. package/src/console/ui/shell/panels/signals/SignalsPanel.tsx +38 -101
  292. package/src/console/ui/shell/panels/store/StorePanel.tsx +27 -66
  293. package/src/console/ui/shell/panels/traces/TracesPanel.tsx +28 -89
  294. package/src/console/ui/shell/panels/vault/VaultPanel.tsx +22 -53
  295. package/src/console/ui/shell/setup/Wizard.tsx +3 -5
  296. package/src/console/ui/shell/styles.css +1 -2
  297. package/src/console/ui/signals/SignalsA11yView.tsx +9 -33
  298. package/src/console/ui/signals/a11y.test.tsx +2 -5
  299. package/src/console/ui/signals/confirmation.test.ts +1 -5
  300. package/src/console/ui/signals/confirmation.ts +2 -6
  301. package/src/console/ui/signals/dry-run.ts +1 -3
  302. package/src/console/ui/signals/durable.ts +1 -3
  303. package/src/console/ui/signals/findings.ts +1 -3
  304. package/src/console/ui/signals/group.test.ts +2 -8
  305. package/src/console/ui/signals/group.ts +2 -10
  306. package/src/console/ui/signals/index.ts +1 -4
  307. package/src/console/ui/signals/monitor.ts +2 -7
  308. package/src/console/ui/signals/schema-form.test.ts +3 -14
  309. package/src/console/ui/signals/schema-form.ts +1 -4
  310. package/src/console/ui/signals/search.ts +4 -13
  311. package/src/console/ui/store/StoreA11yView.tsx +3 -9
  312. package/src/console/ui/store/a11y.test.tsx +2 -5
  313. package/src/console/ui/store/confirmation.test.ts +1 -3
  314. package/src/console/ui/store/confirmation.ts +1 -4
  315. package/src/console/ui/store/dry-run.ts +2 -5
  316. package/src/console/ui/store/fixture.ts +1 -2
  317. package/src/console/ui/store/group.test.ts +1 -6
  318. package/src/console/ui/store/group.ts +1 -4
  319. package/src/console/ui/store/index.ts +1 -5
  320. package/src/console/ui/store/search.ts +2 -6
  321. package/src/console/ui/store/will-not-fire.ts +1 -2
  322. package/src/console/ui/traces/TracesA11yView.tsx +10 -29
  323. package/src/console/ui/traces/a11y.test.tsx +1 -2
  324. package/src/console/ui/traces/chain.test.ts +5 -19
  325. package/src/console/ui/traces/chain.ts +1 -3
  326. package/src/console/ui/traces/critical-path.test.ts +1 -3
  327. package/src/console/ui/traces/critical-path.ts +2 -6
  328. package/src/console/ui/traces/filter.test.ts +2 -6
  329. package/src/console/ui/traces/filter.ts +5 -13
  330. package/src/console/ui/traces/fold.test.ts +4 -12
  331. package/src/console/ui/traces/fold.ts +1 -3
  332. package/src/console/ui/traces/mini.ts +1 -3
  333. package/src/console/ui/traces/replay.test.ts +1 -3
  334. package/src/console/ui/traces/replay.ts +1 -3
  335. package/src/console/ui/traces/sampling.ts +1 -4
  336. package/src/console/ui/traces/search.ts +4 -17
  337. package/src/console/ui/traces/tier.ts +1 -3
  338. package/src/console/ui/vault/VaultA11yView.tsx +5 -17
  339. package/src/console/ui/vault/a11y.test.tsx +1 -2
  340. package/src/console/ui/vault/blast-radius.ts +1 -3
  341. package/src/console/ui/vault/confirmation.test.ts +1 -5
  342. package/src/console/ui/vault/confirmation.ts +1 -4
  343. package/src/console/ui/vault/dormant.test.ts +1 -4
  344. package/src/console/ui/vault/export-safe.test.ts +1 -5
  345. package/src/console/ui/vault/export-safe.ts +1 -4
  346. package/src/console/ui/vault/fixture.ts +3 -3
  347. package/src/console/ui/vault/group.ts +1 -2
  348. package/src/console/ui/vault/index.ts +2 -10
  349. package/src/console/ui/vault/search.ts +2 -6
  350. package/src/console/ui/vault/types.ts +1 -1
  351. package/src/console/xss.gate.test.ts +1 -3
  352. package/src/docker/compose.ts +112 -20
  353. package/src/docker/derive.ts +5 -19
  354. package/src/docker/docker.test.ts +15 -15
  355. package/src/docker/dockerfile.integration.test.ts +12 -21
  356. package/src/docker/helpers.ts +5 -4
  357. package/src/docker/index.ts +4 -11
  358. package/src/docker/pin.ts +1 -9
  359. package/src/docker/recipes/index.ts +5 -6
  360. package/src/docker/recipes/mailpit.ts +22 -0
  361. package/src/docker/recipes/postgres.ts +17 -6
  362. package/src/docker/recipes/redis.ts +6 -1
  363. package/src/docker/recipes/rustfs.ts +33 -0
  364. package/src/docker/stack-id.test.ts +33 -0
  365. package/src/docker/stack-id.ts +25 -13
  366. package/src/docker/stack.integration.test.ts +6 -21
  367. package/src/docker/types.ts +13 -4
  368. package/src/drivers/ai-anthropic.ts +5 -14
  369. package/src/drivers/ai-mock.ts +2 -5
  370. package/src/drivers/ai-openai-compatible.ts +4 -10
  371. package/src/drivers/ai-providers.test.ts +6 -13
  372. package/src/drivers/bun-native-completeness.test.ts +6 -15
  373. package/src/drivers/channel-console.ts +6 -7
  374. package/src/drivers/channel-fcm.ts +20 -30
  375. package/src/drivers/channel-resend.ts +1 -3
  376. package/src/drivers/channel-smtp.ts +2 -6
  377. package/src/drivers/channel-types.ts +2 -12
  378. package/src/drivers/channel-unifonic.ts +2 -6
  379. package/src/drivers/channel-wa-cloud.ts +2 -6
  380. package/src/drivers/channel-webpush.ts +21 -60
  381. package/src/drivers/conformance.test.ts +2 -10
  382. package/src/drivers/conformance.ts +8 -21
  383. package/src/drivers/fs.ts +2 -8
  384. package/src/drivers/index.ts +12 -56
  385. package/src/drivers/memory.ts +14 -41
  386. package/src/drivers/pgvector.ts +7 -11
  387. package/src/drivers/postgres.ts +7 -13
  388. package/src/drivers/redis.ts +13 -37
  389. package/src/drivers/s3.ts +5 -17
  390. package/src/drivers/signal-engine.ts +12 -49
  391. package/src/drivers/signal-memory.ts +2 -8
  392. package/src/drivers/signal-nats.ts +4 -9
  393. package/src/drivers/signal-postgres.ts +62 -129
  394. package/src/drivers/signal-redis.ts +7 -21
  395. package/src/drivers/signal-types.ts +1 -4
  396. package/src/drivers/sqlite.ts +3 -12
  397. package/src/drivers/types.ts +4 -23
  398. package/src/drivers/vault-env.ts +2 -6
  399. package/src/drivers/vault-infisical.ts +1 -5
  400. package/src/drivers/vault-managed.ts +1 -5
  401. package/src/drivers/vault-memory.ts +1 -5
  402. package/src/drivers/vault-openbao.ts +1 -5
  403. package/src/drivers/vault-sops.ts +10 -35
  404. package/src/drivers/vault-types.ts +1 -7
  405. package/src/elements/ai/declare.ts +4 -14
  406. package/src/elements/ai/eval.ts +2 -6
  407. package/src/elements/ai/pii.ts +1 -3
  408. package/src/elements/ai/runtime.ts +12 -50
  409. package/src/elements/ai/schema.ts +4 -15
  410. package/src/elements/ai.test.ts +13 -24
  411. package/src/elements/ai.ts +1 -5
  412. package/src/elements/channel/consent.ts +3 -12
  413. package/src/elements/channel/costs.ts +4 -15
  414. package/src/elements/channel/declare.ts +1 -4
  415. package/src/elements/channel/email-auth.ts +1 -4
  416. package/src/elements/channel/locale.ts +2 -10
  417. package/src/elements/channel/mask.ts +1 -3
  418. package/src/elements/channel/mime.ts +2 -12
  419. package/src/elements/channel/outcomes.test.ts +1 -5
  420. package/src/elements/channel/outcomes.ts +5 -15
  421. package/src/elements/channel/receipts.ts +2 -7
  422. package/src/elements/channel/runtime.ts +28 -74
  423. package/src/elements/channel/suppression.ts +8 -14
  424. package/src/elements/channel.test.ts +8 -31
  425. package/src/elements/channel.ts +3 -14
  426. package/src/elements/clock/actions.ts +6 -18
  427. package/src/elements/clock/declare.ts +1 -3
  428. package/src/elements/clock/dst.ts +1 -5
  429. package/src/elements/clock/durable.ts +4 -16
  430. package/src/elements/clock/health.ts +2 -7
  431. package/src/elements/clock/leader.ts +2 -9
  432. package/src/elements/clock/reconcile.ts +4 -15
  433. package/src/elements/clock/runtime.ts +2 -5
  434. package/src/elements/clock/schedule.ts +4 -20
  435. package/src/elements/clock.test.ts +2 -7
  436. package/src/elements/clock.ts +5 -23
  437. package/src/elements/gate/permissions.ts +1 -4
  438. package/src/elements/gate/runtime.ts +7 -28
  439. package/src/elements/gate.ts +1 -5
  440. package/src/elements/index.ts +9 -7
  441. package/src/elements/signal/declare.ts +2 -7
  442. package/src/elements/signal/reconcile.test.ts +1 -4
  443. package/src/elements/signal/runtime.ts +1 -3
  444. package/src/elements/signal.test.ts +3 -15
  445. package/src/elements/signal.ts +2 -8
  446. package/src/elements/store/cache.test.ts +46 -0
  447. package/src/elements/store/cache.ts +3 -11
  448. package/src/elements/store/classify.ts +5 -22
  449. package/src/elements/store/declare.ts +8 -17
  450. package/src/elements/store/domain-ddl.test.ts +86 -0
  451. package/src/elements/store/emit-drizzle.ts +363 -0
  452. package/src/elements/store/files-policy.test.ts +1 -5
  453. package/src/elements/store/files-policy.ts +4 -12
  454. package/src/elements/store/load-plugin-tables.ts +103 -0
  455. package/src/elements/store/missing-relation.test.ts +83 -0
  456. package/src/elements/store/missing-relation.ts +38 -0
  457. package/src/elements/store/replica.ts +1 -4
  458. package/src/elements/store/runtime.ts +25 -28
  459. package/src/elements/store/schema-decl.test.ts +227 -0
  460. package/src/elements/store/schema-decl.ts +516 -0
  461. package/src/elements/store/sql-condition.ts +11 -21
  462. package/src/elements/store/sql-session.test.ts +50 -0
  463. package/src/elements/store/sql-session.ts +60 -63
  464. package/src/elements/store/table.ts +35 -19
  465. package/src/elements/store.test.ts +17 -39
  466. package/src/elements/store.ts +37 -31
  467. package/src/elements/vault/chain.ts +34 -0
  468. package/src/elements/vault/declare.ts +22 -32
  469. package/src/elements/vault/fingerprint.ts +2 -7
  470. package/src/elements/vault/redact.ts +2 -6
  471. package/src/elements/vault/runtime.ts +18 -32
  472. package/src/elements/vault.test.ts +12 -28
  473. package/src/elements/vault.ts +8 -12
  474. package/src/index.ts +5 -2
  475. package/src/kernel/adopt-routes.ts +15 -29
  476. package/src/kernel/app.ts +34 -94
  477. package/src/kernel/auth-resolve.ts +2 -9
  478. package/src/kernel/boot-bind/ai.ts +1 -4
  479. package/src/kernel/boot-bind/channel.ts +2 -8
  480. package/src/kernel/boot-bind/clock.ts +2 -6
  481. package/src/kernel/boot-bind/gate.ts +2 -8
  482. package/src/kernel/boot-bind/runs.ts +1 -3
  483. package/src/kernel/boot-bind/signal.ts +3 -10
  484. package/src/kernel/boot-bind/store.test.ts +17 -17
  485. package/src/kernel/boot-bind/store.ts +44 -50
  486. package/src/kernel/boot-bind/vault.ts +2 -8
  487. package/src/kernel/boot.test.ts +4 -9
  488. package/src/kernel/boot.ts +29 -76
  489. package/src/kernel/budget.test.ts +1 -3
  490. package/src/kernel/capability.ts +3 -10
  491. package/src/kernel/dry-run.test.ts +1 -4
  492. package/src/kernel/dry-run.ts +3 -11
  493. package/src/kernel/effects.test.ts +1 -5
  494. package/src/kernel/effects.ts +4 -11
  495. package/src/kernel/errors.registry.test.ts +2 -8
  496. package/src/kernel/errors.ts +23 -32
  497. package/src/kernel/flow.test.ts +12 -25
  498. package/src/kernel/flow.ts +5 -25
  499. package/src/kernel/fx.test.ts +15 -44
  500. package/src/kernel/fx.ts +27 -79
  501. package/src/kernel/hook-timing.test.ts +3 -6
  502. package/src/kernel/hook-timing.ts +5 -14
  503. package/src/kernel/hooks.test.ts +10 -10
  504. package/src/kernel/hooks.ts +3 -12
  505. package/src/kernel/index.ts +4 -17
  506. package/src/kernel/journal.ts +7 -31
  507. package/src/kernel/on.ts +1 -6
  508. package/src/kernel/pipeline.test.ts +10 -19
  509. package/src/kernel/pipeline.ts +5 -17
  510. package/src/kernel/plug.ts +4 -11
  511. package/src/kernel/plugin/conflicts.test.ts +13 -25
  512. package/src/kernel/plugin/decorate.test.ts +8 -13
  513. package/src/kernel/plugin/scoping.test.ts +4 -11
  514. package/src/kernel/plugin.ts +48 -24
  515. package/src/kernel/registry.ts +80 -29
  516. package/src/kernel/router.test.ts +15 -13
  517. package/src/kernel/router.ts +9 -29
  518. package/src/kernel/routing-budget.test.ts +1 -3
  519. package/src/kernel/run-telemetry.ts +1 -3
  520. package/src/kernel/triggers.ts +4 -21
  521. package/src/manifest/diff.test.ts +26 -91
  522. package/src/manifest/diff.ts +95 -225
  523. package/src/manifest/index.ts +2 -9
  524. package/src/manifest/types.ts +29 -5
  525. package/src/manifest/undeclared.test.ts +4 -12
  526. package/src/manifest/undeclared.ts +1 -2
  527. package/src/manifest/validate.test.ts +2 -6
  528. package/src/manifest/validate.ts +4 -14
  529. package/src/mcp/authorization.ts +8 -27
  530. package/src/mcp/confirmation.ts +3 -10
  531. package/src/mcp/data.ts +2 -6
  532. package/src/mcp/docs-index.ts +4 -7
  533. package/src/mcp/docs-mcp.test.ts +3 -12
  534. package/src/mcp/docs-server.ts +7 -27
  535. package/src/mcp/docs-tools.ts +2 -6
  536. package/src/mcp/index.ts +1 -6
  537. package/src/mcp/injection.gate.test.ts +4 -18
  538. package/src/mcp/mcp.test.ts +18 -49
  539. package/src/mcp/protocol.ts +3 -10
  540. package/src/mcp/server.ts +11 -52
  541. package/src/mcp/session.ts +4 -14
  542. package/src/mcp/tools.ts +14 -50
  543. package/src/plugins/catalogue.test.ts +3 -7
  544. package/src/plugins/catalogue.ts +1 -3
  545. package/src/plugins/node-import-scan.ts +2 -5
  546. package/src/plugins/supply-chain.ts +5 -17
  547. package/src/release/exports.ts +3 -8
  548. package/src/release/measure.exports.test.ts +1 -3
  549. package/src/release/measure.ts +15 -43
  550. package/src/runs/bench.test.ts +1 -3
  551. package/src/runs/collect.ts +8 -25
  552. package/src/runs/drivers/clickhouse.ts +2 -9
  553. package/src/runs/drivers/files.ts +9 -31
  554. package/src/runs/drivers/memory.ts +5 -22
  555. package/src/runs/drivers/postgres.ts +8 -20
  556. package/src/runs/duckdb.ts +2 -8
  557. package/src/runs/index.ts +3 -14
  558. package/src/runs/outlier.ts +3 -10
  559. package/src/runs/parquet.ts +8 -26
  560. package/src/runs/privacy.ts +2 -8
  561. package/src/runs/runs.test.ts +3 -9
  562. package/src/runs/runtime.ts +7 -32
  563. package/src/runs/shred.ts +10 -29
  564. package/src/runs/types.ts +1 -4
  565. package/src/runtime/bun.ts +4 -15
  566. package/src/runtime/cold-start.bench.ts +1 -3
  567. package/src/runtime/dev-request-log.test.ts +1 -4
  568. package/src/runtime/dev-request-log.ts +1 -4
  569. package/src/runtime/index.ts +1 -6
  570. package/src/runtime/primitives.ts +5 -4
  571. package/src/runtime/security.ts +3 -13
  572. package/src/runtime/serve.test.ts +4 -7
  573. package/src/runtime/types.ts +1 -4
  574. package/src/runtime/web-standard.ts +3 -15
  575. package/src/term.test.ts +4 -6
  576. package/src/term.ts +49 -98
  577. package/src/test/create-test-app.test.ts +4 -14
  578. package/src/test/create-test-app.ts +13 -31
  579. package/src/test/provisions.integration.test.ts +3 -11
  580. package/src/upgrade/codemods.test.ts +25 -6
  581. package/src/upgrade/codemods.ts +116 -3
  582. package/src/validation/standard-schema.test.ts +2 -5
  583. package/src/validation/standard-schema.ts +13 -33
  584. package/src/console/ui/dist/assets/panel-vault-BbfWdox0.js +0 -1
@@ -1,21 +1,23 @@
1
1
  # OKE — Four Applications
2
+
2
3
  ### A progressive path from one flow to a full system
3
4
 
4
5
  **Package:** `okengine` · **CLI:** `oke`
5
6
 
6
7
  Four complete, runnable applications. Each one introduces the smallest possible set of new ideas, and each ends by naming the limitation that motivates the next. Read them in order and the eight elements arrive one or two at a time instead of all at once.
7
8
 
8
- | | App | Teaches | Elements | Exports | Files |
9
- |---|---|---|---|---|---|
10
- | **1 · Basic** | Notes | the one law · contracts · typed errors · the client | Flow · Store | 4 / 10 | 5 |
11
- | **2 · Intermediate** | Linkly | one species, many triggers · delivery physics · transactional emit | + Signal · Clock · Gate | 7 / 10 | 11 |
12
- | **3 · Advanced** | Provisions | durability · reaching humans · live queries · plugins | + Vault · Channel | 10 / 10 | 18 |
13
- | **4 · Complex** | Skyport | AI and agents · tenancy · SLOs · distributed topology | all eight | all ten | 24 |
9
+ | | App | Teaches | Elements | Exports | Files |
10
+ | -------------------- | ---------- | ------------------------------------------------------------------ | ----------------------- | ------- | ----- |
11
+ | **1 · Basic** | Notes | the one law · contracts · typed errors · the client | Flow · Store | 4 / 10 | 5 |
12
+ | **2 · Intermediate** | Linkly | one species, many triggers · delivery physics · transactional emit | + Signal · Clock · Gate | 7 / 10 | 11 |
13
+ | **3 · Advanced** | Provisions | durability · reaching humans · live queries · plugins | + Vault · Channel | 10 / 10 | 18 |
14
+ | **4 · Complex** | Skyport | AI and agents · tenancy · SLOs · distributed topology | all eight | all ten | 24 |
14
15
 
15
16
  **The ten exports:** `on · flow · signal · store · clock · gate · vault · channel · ai · plugin`
16
17
  **The one law:** `on(Trigger) → Effects`
17
18
 
18
19
  ---
20
+
19
21
  ---
20
22
 
21
23
  # 1 · BASIC — Notes
@@ -44,11 +46,11 @@ import { defineConfig } from "okengine/config";
44
46
  export default defineConfig({
45
47
  drivers: {
46
48
  // `stack` = `oke dev -s` (local server). Omitting it copies `prod`
47
- // (defineConfig); vault `sops` becomes stack `dotenv` for `.env.stack`.
49
+ // (defineConfig); vault `sops` becomes docker `dotenv` for `docker/.env.docker`.
48
50
  store: {
49
51
  sql: {
50
- dev: "sqlite",
51
- stack: "postgres",
52
+ local: "sqlite",
53
+ docker: "postgres",
52
54
  test: "memory",
53
55
  prod: "postgres",
54
56
  },
@@ -66,9 +68,9 @@ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
66
68
  import { id, now } from "okengine/store";
67
69
 
68
70
  export const notes = sqliteTable("notes", {
69
- id: text("id").primaryKey().$defaultFn(id),
70
- title: text("title").notNull(),
71
- body: text("body").notNull(),
71
+ id: text("id").primaryKey().$defaultFn(id),
72
+ title: text("title").notNull(),
73
+ body: text("body").notNull(),
72
74
  createdAt: integer("created_at").notNull().$defaultFn(now),
73
75
  });
74
76
  ```
@@ -96,43 +98,57 @@ import { db } from "../../core";
96
98
  import { notes } from "../../schema";
97
99
 
98
100
  // Contracts derived from the schema — one source of truth, refined where the API is stricter
99
- const NewNote = createInsertSchema(notes, { title: (s) => s.min(1).max(120) })
100
- .omit({ id: true, createdAt: true });
101
- const Note = createSelectSchema(notes);
102
- const NoteId = z.object({ id: z.string() });
101
+ const NewNote = createInsertSchema(notes, { title: (s) => s.min(1).max(120) }).omit({
102
+ id: true,
103
+ createdAt: true,
104
+ });
105
+ const Note = createSelectSchema(notes);
106
+ const NoteId = z.object({ id: z.string() });
103
107
  const NotFound = z.object({});
104
108
 
105
- export const create = on(http.post("/notes"), flow({
106
- in: NewNote,
107
- out: NoteId,
108
- do: async (input, fx) => {
109
- const [note] = await fx.store(db).insert(notes).values(input).returning();
110
- return { id: note.id };
111
- },
112
- }));
109
+ export const create = on(
110
+ http.post("/notes"),
111
+ flow({
112
+ in: NewNote,
113
+ out: NoteId,
114
+ do: async (input, fx) => {
115
+ const [note] = await fx.store(db).insert(notes).values(input).returning();
116
+ return { id: note.id };
117
+ },
118
+ }),
119
+ );
113
120
  // effects → writes[sql:notes]
114
121
 
115
- export const list = on(http.get("/notes"), flow({
116
- out: Note.array(),
117
- do: (_, fx) => fx.store(db).select().from(notes),
118
- }));
122
+ export const list = on(
123
+ http.get("/notes"),
124
+ flow({
125
+ out: Note.array(),
126
+ do: (_, fx) => fx.store(db).select().from(notes),
127
+ }),
128
+ );
119
129
  // effects → reads[sql:notes]
120
130
 
121
- export const get = on(http.get("/notes/:id"), flow({
122
- in: NoteId,
123
- out: Note,
124
- errors: { NotFound },
125
- do: async ({ id }, fx) => (await fx.store(db).findById(notes, id)) ?? fx.fail("NotFound", {}),
126
- }));
127
-
128
- export const remove = on(http.delete("/notes/:id"), flow({
129
- in: NoteId,
130
- errors: { NotFound },
131
- do: async ({ id }, fx) => {
132
- const deleted = await fx.store(db).delete(notes, id);
133
- if (!deleted) return fx.fail("NotFound", {});
134
- },
135
- }));
131
+ export const get = on(
132
+ http.get("/notes/:id"),
133
+ flow({
134
+ in: NoteId,
135
+ out: Note,
136
+ errors: { NotFound },
137
+ do: async ({ id }, fx) => (await fx.store(db).findById(notes, id)) ?? fx.fail("NotFound", {}),
138
+ }),
139
+ );
140
+
141
+ export const remove = on(
142
+ http.delete("/notes/:id"),
143
+ flow({
144
+ in: NoteId,
145
+ errors: { NotFound },
146
+ do: async ({ id }, fx) => {
147
+ const deleted = await fx.store(db).delete(notes, id);
148
+ if (!deleted) return fx.fail("NotFound", {});
149
+ },
150
+ }),
151
+ );
136
152
  ```
137
153
 
138
154
  **Four things to notice.**
@@ -153,7 +169,7 @@ import * as notes from "./flows/notes";
153
169
 
154
170
  export const app = oke({ name: "notes" }).adopt({ notes });
155
171
 
156
- export type App = typeof app; // ← the client needs nothing else
172
+ export type App = typeof app; // ← the client needs nothing else
157
173
  ```
158
174
 
159
175
  `on()` still registers each flow with the router and the Manifest — `.adopt()` exists so the type of `app` accumulates every contract in `notes`, which is what lets the client below need no hand-written types and no separate codegen step. The namespace key (`notes`) becomes the client's namespace; each export becomes a method.
@@ -171,7 +187,7 @@ const api = createClient<App>("http://localhost:6530", { $routes: app.$routes })
171
187
  const { data, error } = await api.notes.get({ id: "n_1" });
172
188
 
173
189
  if (error?.code === "NotFound") show("gone");
174
- else console.log(data.title); // ← typed, no codegen ✅
190
+ else console.log(data.title); // ← typed, no codegen ✅
175
191
  // GET /notes/n_1 — the method and path are derived from the flow's own trigger,
176
192
  // not from a separate RPC convention.
177
193
  ```
@@ -184,7 +200,7 @@ import { createTestApp } from "okengine/test";
184
200
  import { app } from "../src/app";
185
201
 
186
202
  test("create then read", async () => {
187
- const t = await createTestApp(app); // memory driver, automatic
203
+ const t = await createTestApp(app); // memory driver, automatic
188
204
  const { data } = await t.api.notes.create({ title: "First", body: "Hello" });
189
205
  const { data: note } = await t.api.notes.get({ id: data!.id });
190
206
  expect(note!.title).toBe("First");
@@ -207,9 +223,10 @@ Four exports (`oke`, `on`, `flow`, `store`), two elements, a typed client, autom
207
223
 
208
224
  ### What is missing
209
225
 
210
- Everything here is synchronous. A real application needs work that happens *later* — after the response, on a schedule, or in reaction to something. That is the next app.
226
+ Everything here is synchronous. A real application needs work that happens _later_ — after the response, on a schedule, or in reaction to something. That is the next app.
211
227
 
212
228
  ---
229
+
213
230
  ---
214
231
 
215
232
  # 2 · INTERMEDIATE — Linkly
@@ -225,6 +242,8 @@ linkly/
225
242
  │ ├── app.ts
226
243
  │ ├── core.ts
227
244
  │ ├── gates.ts
245
+ │ ├── schema.decl.ts
246
+ │ ├── schema.generated.ts
228
247
  │ ├── schema.ts
229
248
  │ └── flows/
230
249
  │ ├── links/
@@ -242,10 +261,12 @@ import { defineConfig } from "okengine/config";
242
261
 
243
262
  export default defineConfig({
244
263
  drivers: {
245
- store: { sql: { dev: "sqlite", test: "memory", prod: "postgres" },
246
- kv: { dev: "memory", test: "memory", prod: "redis" } },
247
- signal: { dev: "memory", test: "memory", prod: "postgres" },
248
- clock: { dev: "memory", test: "frozen", prod: "postgres" },
264
+ store: {
265
+ sql: { local: "sqlite", test: "memory", prod: "postgres" },
266
+ kv: { local: "memory", test: "memory", prod: "redis" },
267
+ },
268
+ signal: { local: "memory", test: "memory", prod: "postgres" },
269
+ clock: { local: "memory", test: "frozen", prod: "postgres" },
249
270
  },
250
271
  });
251
272
  ```
@@ -260,36 +281,60 @@ import { gate } from "okengine";
260
281
  export const member = gate.policy("member", ({ auth }) => !!auth?.verified);
261
282
 
262
283
  export const fair = gate.rate({
263
- strategy: "sliding-window-counter", // near-exact, two keys, no boundary bursts
264
- max: 60, per: "1m", keyBy: "ip",
284
+ strategy: "sliding-window-counter", // near-exact, two keys, no boundary bursts
285
+ max: 60,
286
+ per: "1m",
287
+ keyBy: "ip",
265
288
  });
266
289
  ```
267
290
 
268
291
  Five strategies exist (`fixed-window`, `sliding-log`, `token-bucket`, `leaky-bucket`); this one is the default because it has the best accuracy-to-cost ratio.
269
292
 
270
- ### `src/schema.ts`
293
+ ### `src/schema.decl.ts`
271
294
 
272
295
  ```typescript
273
- import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
274
-
275
- export const links = sqliteTable("links", {
276
- id: text("id").primaryKey(), // `increment` targets this column
277
- code: text("code").notNull().unique(), // the short, human-facing key
278
- url: text("url").notNull(),
279
- userId: text("user_id").notNull(),
280
- clicks: integer("clicks").notNull().default(0),
281
- createdAt: integer("created_at").notNull(),
296
+ import { store, field } from "okengine";
297
+
298
+ /** Short links `code` is the human-facing unique key. */
299
+ export const links = store.schema.table("links", {
300
+ id: field.text().primaryKey(), // `increment` targets this column
301
+ code: field.text().notNull().unique(), // the short, human-facing key
302
+ url: field.text().notNull(),
303
+ userId: field.text().notNull(),
304
+ clicks: field.integer().notNull().default(0),
305
+ createdAt: field.integer().notNull(),
282
306
  });
283
307
 
284
- export const daily = sqliteTable("daily", {
285
- id: text("id").primaryKey(),
286
- code: text("code").notNull(),
287
- day: text("day").notNull(), // "YYYY-MM-DD"
288
- clicks: integer("clicks").notNull().default(0),
308
+ /** Per-link daily click counts — FK to `links.code`. */
309
+ export const daily = store.schema.table("daily", {
310
+ id: field.text().primaryKey(),
311
+ code: field
312
+ .text()
313
+ .notNull()
314
+ .references(() => links.code),
315
+ day: field.text().notNull(), // "YYYY-MM-DD"
316
+ clicks: field.integer().notNull().default(0),
289
317
  });
318
+
319
+ /** links ↔ daily — one link has many daily rows. */
320
+ export const relations = store.schema.relations({ links, daily }, (r) => ({
321
+ links: {
322
+ daily: r.many.daily({
323
+ from: r.links.code,
324
+ to: r.daily.code,
325
+ }),
326
+ },
327
+ daily: {
328
+ link: r.one.links({
329
+ from: r.daily.code,
330
+ to: r.links.code,
331
+ optional: false,
332
+ }),
333
+ },
334
+ }));
290
335
  ```
291
336
 
292
- A generated `id` plus a separate unique `code` is the standard shape for a shortener: `increment` — see below — is a store-level primitive that targets a row by its primary key, so every table it touches needs one.
337
+ A generated `id` plus a separate unique `code` is the standard shape for a shortener: `increment` — see below — is a store-level primitive that targets a row by its primary key, so every table it touches needs one. `daily.code` references `links.code`; `store.schema.relations` mirrors Drizzle’s `defineRelations`.
293
338
 
294
339
  ### `src/flows/links/signals.ts`
295
340
 
@@ -299,13 +344,14 @@ import { z } from "zod";
299
344
 
300
345
  export const linkClicked = signal("link-clicked", {
301
346
  schema: z.object({ code: z.string(), at: z.number(), referrer: z.string().optional() }),
302
- delivery: "once", // queue physics: one consumer, retries, DLQ
303
- retries: 3, deadLetter: true,
347
+ delivery: "once", // queue physics: one consumer, retries, DLQ
348
+ retries: 3,
349
+ deadLetter: true,
304
350
  });
305
351
 
306
352
  export const linkStats = signal("link-stats", {
307
353
  schema: z.object({ code: z.string(), clicks: z.number() }),
308
- delivery: "live", // stream physics: clients subscribe
354
+ delivery: "live", // stream physics: clients subscribe
309
355
  });
310
356
  ```
311
357
 
@@ -323,52 +369,75 @@ import { NewLink, LinkCode, Link, NotFound, Taken } from "./shapes";
323
369
  import { links } from "../../schema";
324
370
 
325
371
  // ① HTTP — "an endpoint"
326
- export const shorten = on(http.post("/links").gate(member, fair), flow({
327
- in: NewLink, out: LinkCode, errors: { Taken },
328
- do: async ({ url, code }, fx) => {
329
- if (await fx.store(db).exists(links, { code })) return fx.fail("Taken", {});
330
- const id = fx.id();
331
- await fx.store(db).insert(links).values(
332
- { id, code, url, userId: fx.auth.userId, clicks: 0, createdAt: Date.now() });
333
- return { code };
334
- },
335
- }));
372
+ export const shorten = on(
373
+ http.post("/links").gate(member, fair),
374
+ flow({
375
+ in: NewLink,
376
+ out: LinkCode,
377
+ errors: { Taken },
378
+ do: async ({ url, code }, fx) => {
379
+ if (await fx.store(db).exists(links, { code })) return fx.fail("Taken", {});
380
+ const id = fx.id();
381
+ await fx
382
+ .store(db)
383
+ .insert(links)
384
+ .values({ id, code, url, userId: fx.auth.userId, clicks: 0, createdAt: Date.now() });
385
+ return { code };
386
+ },
387
+ }),
388
+ );
336
389
 
337
390
  // ② HTTP — the hot path
338
- export const redirect = on(http.get("/:code").gate(fair), flow({
339
- in: LinkCode, out: Link, errors: { NotFound },
340
- do: async ({ code }, fx) => {
341
- const [link] = await fx.store(db).select().from(links).where(eq(links.code, code)).limit(1);
342
- if (!link) return fx.fail("NotFound", {});
343
-
344
- await fx.emit(linkClicked, { code, at: Date.now() }); // same transaction as any write
345
- return link;
346
- },
347
- }));
391
+ export const redirect = on(
392
+ http.get("/:code").gate(fair),
393
+ flow({
394
+ in: LinkCode,
395
+ out: Link,
396
+ errors: { NotFound },
397
+ do: async ({ code }, fx) => {
398
+ const [link] = await fx.store(db).select().from(links).where(eq(links.code, code)).limit(1);
399
+ if (!link) return fx.fail("NotFound", {});
400
+
401
+ await fx.emit(linkClicked, { code, at: Date.now() }); // same transaction as any write
402
+ return link;
403
+ },
404
+ }),
405
+ );
348
406
 
349
407
  // ③ SIGNAL — "a queue consumer", and the same species as ① and ②
350
- on(linkClicked, flow({
351
- do: async ({ code }, fx) => {
352
- const [link] = await fx.store(db).select().from(links).where(eq(links.code, code)).limit(1);
353
- const clicks = await fx.store(db).increment(links, link.id, "clicks");
354
- await fx.emit(linkStats, { code, clicks }); // live: pushed to subscribers
355
- },
356
- }));
408
+ on(
409
+ linkClicked,
410
+ flow({
411
+ do: async ({ code }, fx) => {
412
+ const [link] = await fx.store(db).select().from(links).where(eq(links.code, code)).limit(1);
413
+ const clicks = await fx.store(db).increment(links, link.id, "clicks");
414
+ await fx.emit(linkStats, { code, clicks }); // live: pushed to subscribers
415
+ },
416
+ }),
417
+ );
357
418
 
358
419
  // ④ CLOCK — "a cron job", and still the same species
359
- on(every("1h"), flow({
360
- do: (_, fx) => {
361
- const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; // 30 days
362
- return fx.store(db).delete(links).where(lt(links.createdAt, cutoff));
363
- },
364
- }));
420
+ on(
421
+ every("1h"),
422
+ flow({
423
+ do: (_, fx) => {
424
+ const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; // 30 days
425
+ return fx.store(db).delete(links).where(lt(links.createdAt, cutoff));
426
+ },
427
+ }),
428
+ );
365
429
 
366
430
  // ⑤ A plain flow with no trigger — callable, not "private"
367
431
  export const stats = flow({
368
- in: LinkCode, out: z.object({ clicks: z.number() }),
432
+ in: LinkCode,
433
+ out: z.object({ clicks: z.number() }),
369
434
  do: async ({ code }, fx) => {
370
- const [link] = await fx.store(db).select({ clicks: links.clicks })
371
- .from(links).where(eq(links.code, code)).limit(1);
435
+ const [link] = await fx
436
+ .store(db)
437
+ .select({ clicks: links.clicks })
438
+ .from(links)
439
+ .where(eq(links.code, code))
440
+ .limit(1);
372
441
  return link ?? { clicks: 0 };
373
442
  },
374
443
  });
@@ -387,22 +456,37 @@ import { db } from "../../core";
387
456
  import { member } from "../../gates";
388
457
  import { daily } from "../../schema";
389
458
 
390
- on(linkClicked, flow({ // a second consumer of the same signal
391
- do: async ({ code, at }, fx) => {
392
- const day = new Date(at).toISOString().slice(0, 10);
393
- const [row] = await fx.store(db).select().from(daily)
394
- .where(and(eq(daily.code, code), eq(daily.day, day))).limit(1);
395
-
396
- if (row) await fx.store(db).increment(daily, row.id, "clicks");
397
- else await fx.store(db).insert(daily).values({ id: fx.id(), code, day, clicks: 1 });
398
- },
399
- }));
400
-
401
- export const report = on(http.get("/links/:code/report").gate(member), flow({
402
- out: z.array(z.object({ day: z.string(), clicks: z.number() })),
403
- do: ({ code }, fx) => fx.store(db).select({ day: daily.day, clicks: daily.clicks })
404
- .from(daily).where(eq(daily.code, code)),
405
- }));
459
+ on(
460
+ linkClicked,
461
+ flow({
462
+ // a second consumer of the same signal
463
+ do: async ({ code, at }, fx) => {
464
+ const day = new Date(at).toISOString().slice(0, 10);
465
+ const [row] = await fx
466
+ .store(db)
467
+ .select()
468
+ .from(daily)
469
+ .where(and(eq(daily.code, code), eq(daily.day, day)))
470
+ .limit(1);
471
+
472
+ if (row) await fx.store(db).increment(daily, row.id, "clicks");
473
+ else await fx.store(db).insert(daily).values({ id: fx.id(), code, day, clicks: 1 });
474
+ },
475
+ }),
476
+ );
477
+
478
+ export const report = on(
479
+ http.get("/links/:code/report").gate(member),
480
+ flow({
481
+ out: z.array(z.object({ day: z.string(), clicks: z.number() })),
482
+ do: ({ code }, fx) =>
483
+ fx
484
+ .store(db)
485
+ .select({ day: daily.day, clicks: daily.clicks })
486
+ .from(daily)
487
+ .where(eq(daily.code, code)),
488
+ }),
489
+ );
406
490
  ```
407
491
 
408
492
  This unit never imports anything from `links` except the signal declaration. Decoupling is structural, not a discipline.
@@ -431,18 +515,18 @@ api.signals.linkStats.subscribe(({ code, clicks }) => paint(code, clicks));
431
515
  ### `tests/linkly.test.ts`
432
516
 
433
517
  ```typescript
434
- const t = await createTestApp(app); // memory drivers, frozen clock
518
+ const t = await createTestApp(app); // memory drivers, frozen clock
435
519
  const u = await t.auth.loginAs({});
436
520
 
437
521
  await t.api.links.shorten({ url: "https://example.com", code: "sa" }, { as: u });
438
522
  await t.api.links.redirect({ code: "sa" });
439
- await t.signals.drain(); // run queued work deterministically
523
+ await t.signals.drain(); // run queued work deterministically
440
524
 
441
525
  const { data } = await t.api.links.report({ code: "sa" }, { as: u });
442
526
  expect(data![0].clicks).toBe(1);
443
527
 
444
528
  await t.clock.advance("31d");
445
- await t.cron.run("1h"); // time travel
529
+ await t.cron.run("1h"); // time travel
446
530
  ```
447
531
 
448
532
  ### What you have
@@ -454,6 +538,7 @@ Seven exports, five elements, and five different trigger kinds — all of them t
454
538
  Nothing here survives a deploy. A payment that must wait two minutes for confirmation, an email that must actually reach a person, an order page that updates itself — none of that is expressible yet.
455
539
 
456
540
  ---
541
+
457
542
  ---
458
543
 
459
544
  # 3 · ADVANCED — Provisions
@@ -481,7 +566,7 @@ provisions/
481
566
  └── tests/orders.test.ts
482
567
  ```
483
568
 
484
- **The layout rule from here on: whoever *produces* an element declares it; consumers import it.** Shared concerns (the database, shared gates, secrets, channels) sit at the root. The framework never forces this — the Manifest is built from the import graph — but the tree teaches the vocabulary.
569
+ **The layout rule from here on: whoever _produces_ an element declares it; consumers import it.** Shared concerns (the database, shared gates, secrets, channels) sit at the root. The framework never forces this — the Manifest is built from the import graph — but the tree teaches the vocabulary.
485
570
 
486
571
  ### `src/schema.ts`
487
572
 
@@ -489,17 +574,17 @@ provisions/
489
574
  import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
490
575
 
491
576
  export const products = sqliteTable("products", {
492
- sku: text("sku").primaryKey(),
493
- name: text("name").notNull(),
577
+ sku: text("sku").primaryKey(),
578
+ name: text("name").notNull(),
494
579
  stock: integer("stock").notNull().default(0),
495
580
  });
496
581
 
497
582
  export const orders = sqliteTable("orders", {
498
- id: text("id").primaryKey(),
499
- userId: text("user_id").notNull(),
500
- sku: text("sku").notNull(),
501
- qty: integer("qty").notNull(),
502
- status: text("status").notNull().default("pending"),
583
+ id: text("id").primaryKey(),
584
+ userId: text("user_id").notNull(),
585
+ sku: text("sku").notNull(),
586
+ qty: integer("qty").notNull(),
587
+ status: text("status").notNull().default("pending"),
503
588
  createdAt: integer("created_at").notNull(),
504
589
  });
505
590
  ```
@@ -511,7 +596,7 @@ import { vault } from "okengine";
511
596
  import { z } from "zod";
512
597
 
513
598
  // A declaration is a CONTRACT, not a value.
514
- // Resolution: process.env → .env.local → .env.stack → vault driver → dev fallback
599
+ // Resolution: process.env → .env.local → docker/.env.docker → vault driver → dev fallback
515
600
  export const stripeKey = vault.secret("STRIPE_KEY", {
516
601
  schema: z.string().startsWith("sk_"),
517
602
  description: "Payments gateway key",
@@ -521,7 +606,7 @@ export const stripeKey = vault.secret("STRIPE_KEY", {
521
606
 
522
607
  export const dbUrl = vault.secret("DATABASE_URL", {
523
608
  schema: z.string().url(),
524
- dev: vault.fromStack("store.sql"), // generated by `oke dev --stack` — zero manual setup
609
+ dev: vault.fromDocker("store.sql"), // generated by `oke dev --docker` — zero manual setup
525
610
  });
526
611
  ```
527
612
 
@@ -534,14 +619,15 @@ import { channel } from "okengine";
534
619
  import { z } from "zod";
535
620
 
536
621
  export const mail = channel.email({ from: "Provisions <no-reply@provisions.sa>" });
537
- export const sms = channel.sms({ sender: "PROVISIONS" });
538
- export const wa = channel.whatsapp();
622
+ export const sms = channel.sms({ sender: "PROVISIONS" });
623
+ export const wa = channel.whatsapp();
539
624
 
540
625
  export const orderConfirmed = mail.template("order-confirmed", {
541
626
  schema: z.object({ name: z.string(), orderId: z.string(), total: z.number() }),
542
627
  });
543
628
 
544
- export const otpCode = channel.template("otp-code", { // medium-agnostic
629
+ export const otpCode = channel.template("otp-code", {
630
+ // medium-agnostic
545
631
  schema: z.object({ code: z.string() }),
546
632
  });
547
633
  ```
@@ -562,31 +648,49 @@ import { orders, products } from "../../schema";
562
648
 
563
649
  const canOrder = gate.policy("order:create", ({ auth }) => auth.scopes.has("order:create"));
564
650
 
565
- export const create = on(http.post("/orders").gate(member, canOrder), flow({
566
- in: NewOrder, out: OrderId, errors: { OutOfStock },
567
- do: async (input, fx) => {
568
- const [product] = await fx.store(db).select({ stock: products.stock })
569
- .from(products).where(eq(products.sku, input.sku)).limit(1);
570
- if (!product || product.stock < input.qty) return fx.fail("OutOfStock",
571
- { left: product?.stock ?? 0 },
572
- { message: fx.t("order.outOfStock", { left: product?.stock ?? 0 }) });
573
-
574
- const id = fx.id();
575
- await fx.store(db).insert(orders).values(
576
- { id, userId: fx.auth.userId, ...input, status: "pending", createdAt: Date.now() });
577
- await fx.emit(orderPlaced, { orderId: id });
578
- return { id };
579
- },
580
- }));
651
+ export const create = on(
652
+ http.post("/orders").gate(member, canOrder),
653
+ flow({
654
+ in: NewOrder,
655
+ out: OrderId,
656
+ errors: { OutOfStock },
657
+ do: async (input, fx) => {
658
+ const [product] = await fx
659
+ .store(db)
660
+ .select({ stock: products.stock })
661
+ .from(products)
662
+ .where(eq(products.sku, input.sku))
663
+ .limit(1);
664
+ if (!product || product.stock < input.qty)
665
+ return fx.fail(
666
+ "OutOfStock",
667
+ { left: product?.stock ?? 0 },
668
+ { message: fx.t("order.outOfStock", { left: product?.stock ?? 0 }) },
669
+ );
670
+
671
+ const id = fx.id();
672
+ await fx
673
+ .store(db)
674
+ .insert(orders)
675
+ .values({ id, userId: fx.auth.userId, ...input, status: "pending", createdAt: Date.now() });
676
+ await fx.emit(orderPlaced, { orderId: id });
677
+ return { id };
678
+ },
679
+ }),
680
+ );
581
681
 
582
682
  // LIVE QUERY — realtime and auto-caching from one flag
583
- export const mine = on(http.get("/orders").gate(member).live(), flow({
584
- out: OrderRow.array(),
585
- do: (_, fx) => fx.store(db).select().from(orders).where(eq(orders.userId, fx.auth.userId)),
586
- }));
683
+ export const mine = on(
684
+ http.get("/orders").gate(member).live(),
685
+ flow({
686
+ out: OrderRow.array(),
687
+ do: (_, fx) => fx.store(db).select().from(orders).where(eq(orders.userId, fx.auth.userId)),
688
+ }),
689
+ );
587
690
 
588
691
  export const getOrder = flow({
589
- in: OrderId, out: OrderRow,
692
+ in: OrderId,
693
+ out: OrderRow,
590
694
  do: async ({ id }, fx) => {
591
695
  const [order] = await fx.store(db).select().from(orders).where(eq(orders.id, id)).limit(1);
592
696
  return order;
@@ -594,19 +698,28 @@ export const getOrder = flow({
594
698
  });
595
699
 
596
700
  // SIGNAL consumer
597
- on(orderPlaced, flow({
598
- do: async ({ orderId }, fx) => {
599
- const paid = await fx.call(chargeOrder, { orderId });
600
- await fx.store(db).update(orders).set({ status: paid ? "confirmed" : "failed" })
601
- .where(eq(orders.id, orderId));
602
- await fx.emit(orderNews, { orderId, status: paid ? "confirmed" : "failed" });
603
- },
604
- }));
701
+ on(
702
+ orderPlaced,
703
+ flow({
704
+ do: async ({ orderId }, fx) => {
705
+ const paid = await fx.call(chargeOrder, { orderId });
706
+ await fx
707
+ .store(db)
708
+ .update(orders)
709
+ .set({ status: paid ? "confirmed" : "failed" })
710
+ .where(eq(orders.id, orderId));
711
+ await fx.emit(orderNews, { orderId, status: paid ? "confirmed" : "failed" });
712
+ },
713
+ }),
714
+ );
605
715
 
606
716
  // CHANGE trigger — CDC, built in
607
- on(db.table(orders).changed("status"), flow({
608
- do: ({ before, after }, fx) => fx.log.info("status", { from: before.status, to: after.status }),
609
- }));
717
+ on(
718
+ db.table(orders).changed("status"),
719
+ flow({
720
+ do: ({ before, after }, fx) => fx.log.info("status", { from: before.status, to: after.status }),
721
+ }),
722
+ );
610
723
  ```
611
724
 
612
725
  **`.live()` is the whole of realtime.** The result is cached, invalidated by exactly the writes that touch those rows, and pushed to subscribed clients on exactly those writes. No cache code, no socket code.
@@ -620,13 +733,16 @@ import { stripeKey } from "../../vault";
620
733
  import { OrderRef } from "./shapes";
621
734
 
622
735
  export const chargeOrder = flow({
623
- durable: true, // every fx call below is journaled
624
- in: OrderRef, out: z.boolean(),
736
+ durable: true, // every fx call below is journaled
737
+ in: OrderRef,
738
+ out: z.boolean(),
625
739
  do: async ({ orderId }, fx) => {
626
- const intent = await fx.step("create-intent", () => // never re-runs on replay
627
- stripe(fx.vault(stripeKey)).create(orderId));
740
+ const intent = await fx.step("create-intent", () =>
741
+ // never re-runs on replay
742
+ stripe(fx.vault(stripeKey)).create(orderId),
743
+ );
628
744
 
629
- await fx.clock.sleep("verify-window", "2m"); // survives restart and deploy
745
+ await fx.clock.sleep("verify-window", "2m"); // survives restart and deploy
630
746
 
631
747
  return fx.step("confirm", () => stripe(fx.vault(stripeKey)).confirm(intent));
632
748
  },
@@ -644,13 +760,19 @@ import { orderNews } from "../orders/signals";
644
760
  import { getOrder } from "../orders";
645
761
  import { orderConfirmed, otpCode, wa, sms } from "../../channels";
646
762
 
647
- on(orderNews, flow({
648
- do: async ({ orderId, status }, fx) => {
649
- if (status !== "confirmed") return;
650
- const o = await fx.call(getOrder, { id: orderId });
651
- await fx.send(orderConfirmed, { to: o.userId, data: { name: o.userName, orderId, total: o.total } });
652
- },
653
- }));
763
+ on(
764
+ orderNews,
765
+ flow({
766
+ do: async ({ orderId, status }, fx) => {
767
+ if (status !== "confirmed") return;
768
+ const o = await fx.call(getOrder, { id: orderId });
769
+ await fx.send(orderConfirmed, {
770
+ to: o.userId,
771
+ data: { name: o.userName, orderId, total: o.total },
772
+ });
773
+ },
774
+ }),
775
+ );
654
776
 
655
777
  export const sendOtp = flow({
656
778
  in: z.object({ userId: z.string(), code: z.string() }),
@@ -692,11 +814,11 @@ import * as notifications from "./flows/notifications";
692
814
 
693
815
  export const app = oke({ name: "provisions" })
694
816
  .adopt({ orders, payments, notifications })
695
- .plug(auth()) // zero ceremony: uses your configured store
696
- .plug(audit) // app-wide
817
+ .plug(auth()) // zero ceremony: uses your configured store
818
+ .plug(audit) // app-wide
697
819
  .hook("onError", (ctx, err, fx) => fx.log.error(err));
698
820
 
699
- app.unit("orders").plug(rateLimit({ max: 30 })); // this unit only
821
+ app.unit("orders").plug(rateLimit({ max: 30 })); // this unit only
700
822
 
701
823
  export type App = typeof app;
702
824
  ```
@@ -727,16 +849,17 @@ Typed keys — a missing key is a compile error. Locale resolves per request: us
727
849
  ### `tests/orders.test.ts`
728
850
 
729
851
  ```typescript
730
- const t = await createTestApp(app); // memory drivers, frozen clock
852
+ const t = await createTestApp(app); // memory drivers, frozen clock
731
853
  const u = await t.auth.loginAs({ scopes: ["order:create"] });
732
854
 
733
855
  const { data } = await t.api.orders.create({ sku: "COFFEE", qty: 2 }, { as: u });
734
856
  await t.signals.drain();
735
- await t.clock.advance("2m"); // the durable sleep elapses instantly
857
+ await t.clock.advance("2m"); // the durable sleep elapses instantly
736
858
  await t.signals.drain();
737
859
 
738
860
  expect(t.channels.sent()).toContainEqual(
739
- expect.objectContaining({ template: "order-confirmed", to: u.id, locale: "ar" }));
861
+ expect.objectContaining({ template: "order-confirmed", to: u.id, locale: "ar" }),
862
+ );
740
863
  ```
741
864
 
742
865
  ### What you have
@@ -748,6 +871,7 @@ All ten exports and seven of the eight elements. Durable execution, human-facing
748
871
  The system serves one customer, treats every user the same, and has no way to state what "working" means. It also cannot reason about anything.
749
872
 
750
873
  ---
874
+
751
875
  ---
752
876
 
753
877
  # 4 · COMPLEX — Skyport
@@ -784,41 +908,44 @@ export default defineConfig({
784
908
  // (Bun.sql, bun:sqlite, Bun.redis, Bun.S3) — zero npm client dependencies.
785
909
  drivers: {
786
910
  store: {
787
- sql: { dev: "sqlite", test: "memory",
788
- prod: { driver: "postgres", url: dbUrl, pool: { max: 20 },
789
- replicas: [dbReplica1] } }, // read-only flows auto-route here
790
- kv: { dev: "memory", test: "memory", prod: "redis" }, // Redis · Valkey · Dragonfly
791
- files: { dev: "fs", test: "memory", prod: "s3" }, // S3 · R2 · SeaweedFS · MinIO
792
- index: { dev: "pgvector", test: "memory", prod: "pgvector" },
911
+ sql: {
912
+ local: "sqlite",
913
+ test: "memory",
914
+ prod: { driver: "postgres", url: dbUrl, pool: { max: 20 }, replicas: [dbReplica1] },
915
+ }, // read-only flows auto-route here
916
+ kv: { local: "memory", test: "memory", prod: "redis" }, // Redis · Valkey · Dragonfly
917
+ files: { local: "fs", test: "memory", prod: "s3" }, // S3 · R2 · SeaweedFS · MinIO
918
+ index: { local: "pgvector", test: "memory", prod: "pgvector" },
793
919
  },
794
- signal: { dev: "memory", test: "memory", prod: "postgres" },
795
- clock: { dev: "memory", test: "frozen", prod: "postgres" },
796
- vault: { dev: "dotenv", test: "memory", prod: "sops" }, // SOPS/age — committable
797
- runs: { dev: "files", test: "memory", prod: "files" }, // Parquet + DuckDB
920
+ signal: { local: "memory", test: "memory", prod: "postgres" },
921
+ clock: { local: "memory", test: "frozen", prod: "postgres" },
922
+ vault: { local: "dotenv", test: "memory", prod: "sops" }, // SOPS/age — committable
923
+ runs: { local: "files", test: "memory", prod: "files" }, // Parquet + DuckDB
798
924
  channel: {
799
- email: { dev: "console", prod: "smtp" },
800
- sms: { dev: "console", prod: "unifonic" },
801
- whatsapp: { dev: "console", prod: "wa-cloud" },
802
- push: { dev: "console", prod: "fcm" },
925
+ email: { local: "console", prod: "smtp" },
926
+ sms: { local: "console", prod: "unifonic" },
927
+ whatsapp: { local: "console", prod: "wa-cloud" },
928
+ push: { local: "console", prod: "fcm" },
803
929
  },
804
930
  ai: {
805
- dev: "mock", // deterministic — tests never call out
931
+ local: "mock", // deterministic — tests never call out
806
932
  prod: { driver: "anthropic", key: anthropicKey },
807
933
  // no prod default: model choice is never guessed.
808
934
  // "openai-compatible" covers vLLM · Groq · Together · LM Studio · most self-hosted
809
935
  },
810
936
  },
811
937
 
812
- images: { // vendor choice, keyed by ROLE
938
+ images: {
939
+ // vendor choice, keyed by ROLE
813
940
  "store.sql": "pgvector/pgvector:pg17",
814
- "store.kv": "valkey/valkey:8-alpine",
941
+ "store.kv": "valkey/valkey:8-alpine",
815
942
  },
816
943
 
817
- i18n: { locales: ["en", "ar"], default: "ar", dir: { ar: "rtl" } },
818
- tenancy: { resolve: (ctx) => ctx.auth.orgId, isolation: "row" },
819
- topology: "monolith", // flip to "services" — code unchanged
820
- ports: { app: 6530, console: 6533, mcp: 6535 }, // O·K·E = 6·5·3
821
- console: { prod: { enabled: true, auth: "required" } },
944
+ i18n: { locales: ["en", "ar"], default: "ar", dir: { ar: "rtl" } },
945
+ tenancy: { resolve: (ctx) => ctx.auth.orgId, isolation: "row" },
946
+ topology: "monolith", // flip to "services" — code unchanged
947
+ ports: { app: 6530, console: 6533, mcp: 6535 }, // O·K·E = 6·5·3
948
+ console: { prod: { enabled: true, auth: "required" } },
822
949
  });
823
950
  ```
824
951
 
@@ -829,22 +956,25 @@ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
829
956
  import { id } from "okengine/store";
830
957
 
831
958
  export const bookings = sqliteTable("bookings", {
832
- id: text("id").primaryKey().$defaultFn(id),
833
- userId: text("user_id").notNull(),
834
- flightId: text("flight_id").notNull(),
835
- seats: integer("seats").notNull(),
836
- status: text("status").notNull().default("pending"),
959
+ id: text("id").primaryKey().$defaultFn(id),
960
+ userId: text("user_id").notNull(),
961
+ flightId: text("flight_id").notNull(),
962
+ seats: integer("seats").notNull(),
963
+ status: text("status").notNull().default("pending"),
837
964
  createdAt: integer("created_at").notNull(),
838
965
  });
839
966
 
840
967
  export const flights = sqliteTable("flights", {
841
- id: text("id").primaryKey(),
968
+ id: text("id").primaryKey(),
842
969
  seatsAvailable: integer("seats_available").notNull(),
843
970
  });
844
971
 
845
972
  export const tickets = sqliteTable("tickets", {
846
- id: text("id").primaryKey(), subject: text("subject").notNull(),
847
- body: text("body").notNull(), urgency: text("urgency"), team: text("team"),
973
+ id: text("id").primaryKey(),
974
+ subject: text("subject").notNull(),
975
+ body: text("body").notNull(),
976
+ urgency: text("urgency"),
977
+ team: text("team"),
848
978
  summary: text("summary"),
849
979
  });
850
980
  ```
@@ -854,10 +984,10 @@ export const tickets = sqliteTable("tickets", {
854
984
  ```typescript
855
985
  import { z } from "zod";
856
986
 
857
- export const NewBooking = z.object({ flightId: z.string(), seats: z.number().min(1).max(9) });
858
- export const BookingId = z.object({ id: z.string() });
859
- export const BookingRow = z.object({ id: z.string(), status: z.string(), seats: z.number() });
860
- export const FlightFull = z.object({ seatsLeft: z.number() });
987
+ export const NewBooking = z.object({ flightId: z.string(), seats: z.number().min(1).max(9) });
988
+ export const BookingId = z.object({ id: z.string() });
989
+ export const BookingRow = z.object({ id: z.string(), status: z.string(), seats: z.number() });
990
+ export const FlightFull = z.object({ seatsLeft: z.number() });
861
991
  ```
862
992
 
863
993
  ### `src/flows/bookings/signals.ts`
@@ -867,10 +997,14 @@ import { signal } from "okengine";
867
997
  import { z } from "zod";
868
998
 
869
999
  export const orderPlaced = signal("order-placed", {
870
- schema: z.object({ orderId: z.string() }), delivery: "once", retries: 5, deadLetter: true,
1000
+ schema: z.object({ orderId: z.string() }),
1001
+ delivery: "once",
1002
+ retries: 5,
1003
+ deadLetter: true,
871
1004
  });
872
1005
  export const seatFeed = signal("seat-feed", {
873
- schema: z.object({ flightId: z.string(), left: z.number() }), delivery: "live",
1006
+ schema: z.object({ flightId: z.string(), left: z.number() }),
1007
+ delivery: "live",
874
1008
  });
875
1009
  ```
876
1010
 
@@ -885,32 +1019,54 @@ import { orderPlaced, seatFeed } from "./signals";
885
1019
  import { NewBooking, BookingId, BookingRow, FlightFull } from "./shapes";
886
1020
  import { bookings, flights } from "../../schema";
887
1021
 
888
- export const canBook = gate.policy("booking:create", ({ auth }) => auth.scopes.has("booking:create"));
889
-
890
- export const create = on(http.post("/bookings").gate(member, canBook, fair), flow({
891
- slo: { availability: "99.9%", latency: { p99: "200ms" } },
892
- in: NewBooking, out: BookingId, errors: { FlightFull },
893
- do: async ({ flightId, seats }, fx) => {
894
- const [flight] = await fx.store(db).select().from(flights).where(eq(flights.id, flightId)).limit(1);
895
- if (!flight || flight.seatsAvailable < seats)
896
- return fx.fail("FlightFull", { seatsLeft: flight?.seatsAvailable ?? 0 });
897
-
898
- const id = fx.id();
899
- await fx.store(db).insert(bookings).values(
900
- { id, userId: fx.auth.userId, flightId, seats, status: "pending", createdAt: Date.now() });
901
- await fx.emit(orderPlaced, { orderId: id });
902
- await fx.emit(seatFeed, { flightId, left: flight.seatsAvailable - seats });
903
- return { id };
904
- },
905
- }));
1022
+ export const canBook = gate.policy("booking:create", ({ auth }) =>
1023
+ auth.scopes.has("booking:create"),
1024
+ );
1025
+
1026
+ export const create = on(
1027
+ http.post("/bookings").gate(member, canBook, fair),
1028
+ flow({
1029
+ slo: { availability: "99.9%", latency: { p99: "200ms" } },
1030
+ in: NewBooking,
1031
+ out: BookingId,
1032
+ errors: { FlightFull },
1033
+ do: async ({ flightId, seats }, fx) => {
1034
+ const [flight] = await fx
1035
+ .store(db)
1036
+ .select()
1037
+ .from(flights)
1038
+ .where(eq(flights.id, flightId))
1039
+ .limit(1);
1040
+ if (!flight || flight.seatsAvailable < seats)
1041
+ return fx.fail("FlightFull", { seatsLeft: flight?.seatsAvailable ?? 0 });
1042
+
1043
+ const id = fx.id();
1044
+ await fx.store(db).insert(bookings).values({
1045
+ id,
1046
+ userId: fx.auth.userId,
1047
+ flightId,
1048
+ seats,
1049
+ status: "pending",
1050
+ createdAt: Date.now(),
1051
+ });
1052
+ await fx.emit(orderPlaced, { orderId: id });
1053
+ await fx.emit(seatFeed, { flightId, left: flight.seatsAvailable - seats });
1054
+ return { id };
1055
+ },
1056
+ }),
1057
+ );
906
1058
 
907
- export const mine = on(http.get("/bookings").gate(member).live(), flow({
908
- out: BookingRow.array(),
909
- do: (_, fx) => fx.store(db).select().from(bookings).where(eq(bookings.userId, fx.auth.userId)),
910
- }));
1059
+ export const mine = on(
1060
+ http.get("/bookings").gate(member).live(),
1061
+ flow({
1062
+ out: BookingRow.array(),
1063
+ do: (_, fx) => fx.store(db).select().from(bookings).where(eq(bookings.userId, fx.auth.userId)),
1064
+ }),
1065
+ );
911
1066
 
912
1067
  export const getBooking = flow({
913
- in: BookingId, out: BookingRow,
1068
+ in: BookingId,
1069
+ out: BookingRow,
914
1070
  do: async ({ id }, fx) => {
915
1071
  const [b] = await fx.store(db).select().from(bookings).where(eq(bookings.id, id)).limit(1);
916
1072
  return b;
@@ -920,7 +1076,8 @@ export const getBooking = flow({
920
1076
  // The agent's second tool — refunding is a distinct, gated capability, never the same
921
1077
  // permission as reading a booking, since the agent's tool list is exactly its authority.
922
1078
  export const refundBooking = flow({
923
- in: BookingId, out: BookingRow,
1079
+ in: BookingId,
1080
+ out: BookingRow,
924
1081
  do: async ({ id }, fx) => {
925
1082
  await fx.store(db).update(bookings).set({ status: "refunded" }).where(eq(bookings.id, id));
926
1083
  const [b] = await fx.store(db).select().from(bookings).where(eq(bookings.id, id)).limit(1);
@@ -937,15 +1094,15 @@ import { z } from "zod";
937
1094
  import { getBooking, refundBooking } from "./flows/bookings";
938
1095
 
939
1096
  export const smart = ai.model("smart", { provider: "anthropic", tier: "opus" });
940
- export const fast = ai.model("fast", { provider: "anthropic", tier: "haiku" });
1097
+ export const fast = ai.model("fast", { provider: "anthropic", tier: "haiku" });
941
1098
 
942
1099
  // A prompt is a VERSIONED ARTIFACT with a validated output shape — not a string in a handler
943
1100
  export const triage = smart.prompt("ticket-triage", {
944
- in: z.object({ subject: z.string(), body: z.string() }),
1101
+ in: z.object({ subject: z.string(), body: z.string() }),
945
1102
  out: z.object({ urgency: z.enum(["low", "high"]), team: z.string(), summary: z.string() }),
946
1103
  version: 3,
947
- evals: "./evals/triage.jsonl", // regression-gated in CI via `oke eval`
948
- budget: { maxCostPerCall: 0.02 }, // cost is a first-class dimension
1104
+ evals: "./evals/triage.jsonl", // regression-gated in CI via `oke eval`
1105
+ budget: { maxCostPerCall: 0.02 }, // cost is a first-class dimension
949
1106
  });
950
1107
 
951
1108
  export const embed = ai.embed("docs", { model: fast, into: store.index("kb") });
@@ -970,36 +1127,48 @@ import { db } from "../../core";
970
1127
  import { tickets } from "../../schema";
971
1128
 
972
1129
  // ① A prompt call with a provider fallback chain and a validated result
973
- export const createTicket = on(http.post("/tickets").gate(member), flow({
974
- in: z.object({ subject: z.string(), body: z.string() }),
975
- out: z.object({ id: z.string(), urgency: z.string() }),
976
- do: async (input, fx) => {
977
- const t = await fx.ask(triage, input, { via: [smart, fast] });
978
- const id = fx.id();
979
- await fx.store(db).insert(tickets).values({ id, ...input, ...t });
980
- return { id, urgency: t.urgency };
981
- },
982
- }));
1130
+ export const createTicket = on(
1131
+ http.post("/tickets").gate(member),
1132
+ flow({
1133
+ in: z.object({ subject: z.string(), body: z.string() }),
1134
+ out: z.object({ id: z.string(), urgency: z.string() }),
1135
+ do: async (input, fx) => {
1136
+ const t = await fx.ask(triage, input, { via: [smart, fast] });
1137
+ const id = fx.id();
1138
+ await fx
1139
+ .store(db)
1140
+ .insert(tickets)
1141
+ .values({ id, ...input, ...t });
1142
+ return { id, urgency: t.urgency };
1143
+ },
1144
+ }),
1145
+ );
983
1146
  // effects → writes[sql:tickets] asks[ticket-triage v3] cost[~$0.01] nondeterministic
984
1147
 
985
1148
  // ② RAG — retrieve, then answer with streaming tokens
986
- export const askDocs = on(http.post("/ask").gate(member).live(), flow({
987
- in: z.object({ question: z.string() }),
988
- do: async ({ question }, fx) => {
989
- const context = await fx.search(embed, question, { topK: 5 });
990
- return fx.stream(smart, { prompt: "answer-with-context", data: { question, context } });
991
- // streaming reaches the client through the Signal element — no separate socket layer
992
- },
993
- }));
1149
+ export const askDocs = on(
1150
+ http.post("/ask").gate(member).live(),
1151
+ flow({
1152
+ in: z.object({ question: z.string() }),
1153
+ do: async ({ question }, fx) => {
1154
+ const context = await fx.search(embed, question, { topK: 5 });
1155
+ return fx.stream(smart, { prompt: "answer-with-context", data: { question, context } });
1156
+ // streaming reaches the client through the Signal element — no separate socket layer
1157
+ },
1158
+ }),
1159
+ );
994
1160
 
995
1161
  // ③ A durable, bounded agent
996
- export const supportAgent = on(http.post("/support").gate(member), flow({
997
- durable: true, // nondeterministic calls are ALWAYS journaled
998
- in: z.object({ message: z.string() }),
999
- do: ({ message }, fx) => fx.run(support, { message }),
1000
- // the agent can only call getBooking and refundBooking, and only within THIS user's
1001
- // gates and tenant scope it cannot exceed what the code declares
1002
- }));
1162
+ export const supportAgent = on(
1163
+ http.post("/support").gate(member),
1164
+ flow({
1165
+ durable: true, // nondeterministic calls are ALWAYS journaled
1166
+ in: z.object({ message: z.string() }),
1167
+ do: ({ message }, fx) => fx.run(support, { message }),
1168
+ // the agent can only call getBooking and refundBooking, and only within THIS user's
1169
+ // gates and tenant scope — it cannot exceed what the code declares
1170
+ }),
1171
+ );
1003
1172
  ```
1004
1173
 
1005
1174
  **What the compiler enforces here, for free:**
@@ -1007,7 +1176,7 @@ export const supportAgent = on(http.post("/support").gate(member), flow({
1007
1176
  - A field tagged `pii` in the schema **cannot reach a third-party model** — the build fails unless the flow masks it or declares `allowPii` explicitly.
1008
1177
  - `nondeterministic` forces journaling: on replay, a model is never re-called; the recorded answer is reused.
1009
1178
  - Automatic caching is disabled for AI flows unless a semantic cache is explicitly enabled.
1010
- - Cost accumulates per flow, per tenant and per release — visible in the Console and in Manifest Diff *before* deploy.
1179
+ - Cost accumulates per flow, per tenant and per release — visible in the Console and in Manifest Diff _before_ deploy.
1011
1180
 
1012
1181
  ### Declaring what "working" means
1013
1182
 
@@ -1026,7 +1195,7 @@ journey("book-a-flight", {
1026
1195
  });
1027
1196
  ```
1028
1197
 
1029
- Forty services at 99.9% in sequence yield 96.1% for the user. Because the causal chain is known, **the compiler rejects the impossible**: *"this path composes to 99.4% but declares 99.5%."* And because the objective lives in the Manifest, lowering a target is a code change that passes through Manifest Diff and team review — not a silent dashboard edit.
1198
+ Forty services at 99.9% in sequence yield 96.1% for the user. Because the causal chain is known, **the compiler rejects the impossible**: _"this path composes to 99.4% but declares 99.5%."_ And because the objective lives in the Manifest, lowering a target is a code change that passes through Manifest Diff and team review — not a silent dashboard edit.
1030
1199
 
1031
1200
  ### Multi-tenancy as a dimension of `fx`
1032
1201
 
@@ -1034,11 +1203,11 @@ Forty services at 99.9% in sequence yield 96.1% for the user. Because the causal
1034
1203
 
1035
1204
  ### The three scaling axes, never conflated
1036
1205
 
1037
- | Axis | Question | Mechanism |
1038
- |---|---|---|
1039
- | **Split** (`topology`) | one deployable, or one per unit? | `monolith` = in-process calls · `services` = a container per unit, `fx.call` becomes network — code unchanged |
1040
- | **Clone** (horizontal) | how many copies of the app? | run N instances: `once` signals deliver to exactly one, crons leader-elect, live queries fan out. `oke docker --prod` emits `deploy.replicas` |
1041
- | **Data replicas** | how many copies of the data? | `replicas:` on the driver; read-only flows auto-route, derived from effects |
1206
+ | Axis | Question | Mechanism |
1207
+ | ---------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
1208
+ | **Split** (`topology`) | one deployable, or one per unit? | `monolith` = in-process calls · `services` = a container per unit, `fx.call` becomes network — code unchanged |
1209
+ | **Clone** (horizontal) | how many copies of the app? | run N instances: `once` signals deliver to exactly one, crons leader-elect, live queries fan out. `oke docker --prod` emits `deploy.replicas` |
1210
+ | **Data replicas** | how many copies of the data? | `replicas:` on the driver; read-only flows auto-route, derived from effects |
1042
1211
 
1043
1212
  ### `src/app.ts`
1044
1213
 
@@ -1073,7 +1242,7 @@ const u = await t.auth.loginAs({});
1073
1242
  const { data } = await t.api.support.createTicket({ subject: "…", body: "…" }, { as: u });
1074
1243
 
1075
1244
  expect(data!.urgency).toBe("high");
1076
- expect(t.ai.cost()).toBeLessThan(0.02); // budgets are assertable
1245
+ expect(t.ai.cost()).toBeLessThan(0.02); // budgets are assertable
1077
1246
  ```
1078
1247
 
1079
1248
  ### What you have
@@ -1081,6 +1250,7 @@ expect(t.ai.cost()).toBeLessThan(0.02); // budgets are assertable
1081
1250
  All eight elements, all ten exports, and one law.
1082
1251
 
1083
1252
  ---
1253
+
1084
1254
  ---
1085
1255
 
1086
1256
  # REFERENCE
@@ -1095,19 +1265,24 @@ All eight elements, all ten exports, and one law.
1095
1265
  "bookings.create": {
1096
1266
  "trigger": { "http": { "method": "POST", "path": "/bookings" } },
1097
1267
  "gates": ["member", "booking:create", "rate:sliding-window-counter:300/1m"],
1098
- "in": "…", "out": "…", "errors": ["FlightFull"],
1268
+ "in": "…",
1269
+ "out": "…",
1270
+ "errors": ["FlightFull"],
1099
1271
  "effects": {
1100
- "reads": ["sql:bookings"],
1272
+ "reads": ["sql:bookings"],
1101
1273
  "writes": ["sql:bookings"],
1102
- "emits": ["order-placed", "seat-feed"],
1274
+ "emits": ["order-placed", "seat-feed"],
1103
1275
  "secrets": []
1104
1276
  },
1105
1277
  "slo": { "availability": "99.9%", "latency": { "p99": "200ms" } },
1106
1278
  "source": "src/flows/bookings/index.ts:18"
1107
1279
  },
1108
1280
  "bookings.mine": { "live": true, "cacheKeys": "computed:sql:bookings/userId" },
1109
- "payments.chargeBooking": { "durable": true, "steps": ["create-intent", "confirm"],
1110
- "effects": { "secrets": ["STRIPE_KEY"] } },
1281
+ "payments.chargeBooking": {
1282
+ "durable": true,
1283
+ "steps": ["create-intent", "confirm"],
1284
+ "effects": { "secrets": ["STRIPE_KEY"] }
1285
+ },
1111
1286
  "support.createTicket": {
1112
1287
  "effects": { "writes": ["sql:tickets"], "asks": ["ticket-triage@3"] },
1113
1288
  "nondeterministic": true,
@@ -1117,14 +1292,15 @@ All eight elements, all ten exports, and one law.
1117
1292
  },
1118
1293
  "signals": {
1119
1294
  "order-placed": { "delivery": "once", "retries": 5, "deadLetter": true },
1120
- "seat-feed": { "delivery": "live" }
1295
+ "seat-feed": { "delivery": "live" }
1121
1296
  },
1122
1297
  "channels": { "booking-confirmed": { "medium": "email", "locales": ["en", "ar"] } },
1123
1298
  "ai": {
1124
- "models": { "smart": { "provider": "anthropic", "tier": "opus" } },
1299
+ "models": { "smart": { "provider": "anthropic", "tier": "opus" } },
1125
1300
  "prompts": { "ticket-triage": { "version": 3, "evals": "./evals/triage.jsonl" } },
1126
- "agents": { "support": { "tools": ["bookings.getBooking", "bookings.refundBooking"],
1127
- "maxSteps": 6 } }
1301
+ "agents": {
1302
+ "support": { "tools": ["bookings.getBooking", "bookings.refundBooking"], "maxSteps": 6 }
1303
+ }
1128
1304
  },
1129
1305
  "journeys": { "book-a-flight": { "slo": { "availability": "99.5%" }, "composes": "99.6%" } },
1130
1306
  "drivers": { "prod": ["postgres", "redis", "s3", "smtp", "sops", "anthropic", "pgvector"] },
@@ -1141,8 +1317,10 @@ bun add okengine # ONE package
1141
1317
 
1142
1318
  oke dev # watch · hot reload · Console :6533 · app :6530 · MCP :6535
1143
1319
  # → also auto-syncs client types on every save
1144
- oke dev --stack # -s infra compose under docker/ (no app container; host Bun)
1145
- oke dev -s store.sql,signal # partial: only these roles get real backends
1320
+ oke dev --local # -l session-only local (in-memory; never writes .oke/mode)
1321
+ oke dev --docker # -d infra compose under docker/ (no app container; host Bun)
1322
+ oke dev -d store.sql,signal # partial: only these roles get real backends
1323
+ oke mode local|docker # get/set default oke dev mode (saved in .oke/mode)
1146
1324
 
1147
1325
  oke start # runs exactly what production runs (this is the Docker CMD)
1148
1326
  oke doctor # verify secrets, ports, drivers, tenancy, schema drift
@@ -1173,23 +1351,23 @@ Runtime actions execute directly; structural changes arrive as reviewable diffs
1173
1351
 
1174
1352
  **Why Drizzle is a required peer dependency, not an abstraction.** The framework commits to one query builder rather than supporting several, because the effect inferencer performs real static analysis on Drizzle's own shapes — a table object, `.select().from(t)`, `.insert(t).values()` — to derive `reads`/`writes`/PII classification with no annotation from you. Supporting N ORMs would mean either analysing N different query builders (and getting it wrong for the ones nobody tests) or falling back to hints, which is exactly the annotation burden the effect system exists to remove. One committed ORM is what makes automatic cache invalidation and least-privilege capability tokens possible at all.
1175
1353
 
1176
- **When you still need `fx.id()` despite `$defaultFn(id)`.** The schema default fills the `id` column at insert time — fine when nothing in the flow needs the value beforehand, as in Notes' `create` (the id is only read back from `.returning()`). Generate it explicitly with `fx.id()`, and pass it into `.values({ id, … })` yourself, whenever the flow needs the same id *before or alongside* the insert — to reference it in an emitted signal payload, to use it as a foreign key in a second insert in the same flow, or to return it without a round-trip. Linkly's `shorten` is the pattern: `const id = fx.id()` because the row and any signal about it need to agree on the same identifier within one flow body.
1354
+ **When you still need `fx.id()` despite `$defaultFn(id)`.** The schema default fills the `id` column at insert time — fine when nothing in the flow needs the value beforehand, as in Notes' `create` (the id is only read back from `.returning()`). Generate it explicitly with `fx.id()`, and pass it into `.values({ id, … })` yourself, whenever the flow needs the same id _before or alongside_ the insert — to reference it in an emitted signal payload, to use it as a foreign key in a second insert in the same flow, or to return it without a round-trip. Linkly's `shorten` is the pattern: `const id = fx.id()` because the row and any signal about it need to agree on the same identifier within one flow body.
1177
1355
 
1178
1356
  ## Element checklist across the four applications
1179
1357
 
1180
- | Element | First appears | The unification it proves |
1181
- |---|---|---|
1182
- | **Flow** | Basic | endpoint = consumer = cron = CDC = workflow — one species |
1183
- | **Store** | Basic | sql · kv · files · index; cache and replica routing derived from effects |
1184
- | **Signal** | Intermediate | queue = pub/sub = stream; delivery is a property, not three ecosystems |
1185
- | **Clock** | Intermediate | cron = delay = durable time |
1186
- | **Gate** | Intermediate | auth = ABAC = rate limit, composable at the trigger |
1187
- | **Vault** | Advanced | typed contracts, boot-time validation, per-flow read capability |
1188
- | **Channel** | Advanced | email = sms = whatsapp = push; consent, locale and fallback built in |
1189
- | **AI** | Complex | prompts versioned · agents bounded by your own flows · cost and PII enforced by the compiler |
1190
- | **`fx` door** | Basic | journaling · tests · least privilege · transactions · tenancy · i18n · cost |
1191
- | **Manifest** | Basic | client · docs · console · security · bundle · infrastructure |
1192
- | **Plugin** | Advanced | extends through the same law; built-ins have no private API |
1358
+ | Element | First appears | The unification it proves |
1359
+ | ------------- | ------------- | -------------------------------------------------------------------------------------------- |
1360
+ | **Flow** | Basic | endpoint = consumer = cron = CDC = workflow — one species |
1361
+ | **Store** | Basic | sql · kv · files · index; cache and replica routing derived from effects |
1362
+ | **Signal** | Intermediate | queue = pub/sub = stream; delivery is a property, not three ecosystems |
1363
+ | **Clock** | Intermediate | cron = delay = durable time |
1364
+ | **Gate** | Intermediate | auth = ABAC = rate limit, composable at the trigger |
1365
+ | **Vault** | Advanced | typed contracts, boot-time validation, per-flow read capability |
1366
+ | **Channel** | Advanced | email = sms = whatsapp = push; consent, locale and fallback built in |
1367
+ | **AI** | Complex | prompts versioned · agents bounded by your own flows · cost and PII enforced by the compiler |
1368
+ | **`fx` door** | Basic | journaling · tests · least privilege · transactions · tenancy · i18n · cost |
1369
+ | **Manifest** | Basic | client · docs · console · security · bundle · infrastructure |
1370
+ | **Plugin** | Advanced | extends through the same law; built-ins have no private API |
1193
1371
 
1194
1372
  **Ten exports — `on, flow, signal, store, clock, gate, vault, channel, ai, plugin` — one law.**
1195
1373