okengine 0.13.0 → 0.15.2

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 (277) hide show
  1. package/manifest.v1.schema.json +7 -1
  2. package/package.json +4 -5
  3. package/site/content/docs/ai/mcp.mdx +27 -2
  4. package/site/content/docs/elements/ai.mdx +59 -11
  5. package/site/content/docs/elements/channel.mdx +1 -0
  6. package/site/content/docs/elements/clock.mdx +30 -12
  7. package/site/content/docs/elements/flow.mdx +9 -3
  8. package/site/content/docs/elements/gate.mdx +25 -22
  9. package/site/content/docs/elements/signal.mdx +71 -25
  10. package/site/content/docs/elements/store.mdx +87 -28
  11. package/site/content/docs/elements/vault.mdx +1 -0
  12. package/site/content/docs/get-started/basic-usage.mdx +22 -13
  13. package/site/content/docs/get-started/index.mdx +5 -0
  14. package/site/content/docs/get-started/installation.mdx +2 -2
  15. package/site/content/docs/get-started/introduction.mdx +11 -3
  16. package/site/content/docs/get-started/meta.json +1 -1
  17. package/site/content/docs/get-started/project-structure.mdx +932 -0
  18. package/site/content/docs/reference/cli.md +13 -10
  19. package/site/content/docs/reference/client.mdx +25 -21
  20. package/site/content/docs/reference/fx.mdx +27 -22
  21. package/site/content/docs/reference/security.md +1 -1
  22. package/src/auth/bindings.ts +14 -5
  23. package/src/auth/cross-plane.ts +10 -3
  24. package/src/cli/ai-setup/ai-setup.test.ts +62 -0
  25. package/src/cli/ai-setup/apply.ts +32 -1
  26. package/src/cli/ai-setup/recommend.test.ts +25 -0
  27. package/src/cli/ai-setup/recommend.ts +8 -3
  28. package/src/cli/attach-host-console.test.ts +3 -2
  29. package/src/cli/attach-host-console.ts +3 -0
  30. package/src/cli/build.test.ts +4 -2
  31. package/src/cli/db-seed.ts +48 -15
  32. package/src/cli/db.test.ts +104 -0
  33. package/src/cli/db.ts +122 -10
  34. package/src/cli/dev-controls.test.ts +20 -0
  35. package/src/cli/dev-controls.ts +13 -0
  36. package/src/cli/dev.test.ts +15 -4
  37. package/src/cli/dev.ts +102 -29
  38. package/src/cli/ensure-drizzle-config.ts +2 -1
  39. package/src/cli/tui/DevLive.tsx +12 -2
  40. package/src/cli/tui/keys.test.ts +2 -1
  41. package/src/cli/tui/keys.ts +3 -2
  42. package/src/compiler/aot.test.ts +1 -1
  43. package/src/compiler/effects-infer.ts +58 -3
  44. package/src/compiler/extract.test.ts +458 -29
  45. package/src/compiler/extract.ts +387 -69
  46. package/src/compiler/flow-path.test.ts +96 -0
  47. package/src/compiler/flow-path.ts +173 -0
  48. package/src/compiler/generate-adopt.test.ts +112 -3
  49. package/src/compiler/generate-adopt.ts +280 -26
  50. package/src/compiler/response.ts +45 -1
  51. package/src/compiler/schema-from-ast.ts +609 -0
  52. package/src/console/server/ai.ts +5 -2
  53. package/src/console/server/bind.ts +2 -2
  54. package/src/console/server/console.test.ts +3 -0
  55. package/src/console/server/flows.ts +121 -40
  56. package/src/console/server/invoke-user-flow.test.ts +3 -1
  57. package/src/console/server/invoke-user-flow.ts +17 -2
  58. package/src/console/server/iso-at.test.ts +9 -0
  59. package/src/console/server/iso-at.ts +11 -0
  60. package/src/console/server/security-headers.ts +6 -3
  61. package/src/console/server/serve.ts +17 -2
  62. package/src/console/server/spa-proxy.test.ts +1 -0
  63. package/src/console/server/sql-catalog.ts +147 -7
  64. package/src/console/server/sql-rls.test.ts +16 -0
  65. package/src/console/server/state.ts +8 -0
  66. package/src/console/server/store.test.ts +7 -4
  67. package/src/console/server/store.ts +142 -58
  68. package/src/console/ui-next/dist/assets/agent-disclosure-C0X1fbWF.js +1 -0
  69. package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +1 -0
  70. package/src/console/ui-next/dist/assets/{call-pii-button-bqkxMrJH.js → call-pii-button-DEDCl_j3.js} +1 -1
  71. package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +1 -0
  72. package/src/console/ui-next/dist/assets/{confirm-sheet-DDCRmG62.js → confirm-sheet-3ptDzXbz.js} +1 -1
  73. package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
  74. package/src/console/ui-next/dist/assets/{explorer-empty-HjTnVQoR.js → explorer-empty-BvYrygyO.js} +1 -1
  75. package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +1 -0
  76. package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-CS_O8L-r.js} +1 -1
  77. package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +1 -0
  78. package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +66 -0
  79. package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +2 -0
  80. package/src/console/ui-next/dist/assets/{link-DF7SZ9Ek.js → link-COeyggt-.js} +1 -1
  81. package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +4 -0
  82. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
  83. package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +18 -0
  84. package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +1 -0
  85. package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +1 -0
  86. package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +1 -0
  87. package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +41 -0
  88. package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +2 -0
  89. package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +54 -0
  90. package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +1 -0
  91. package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-BLbZhzyF.js} +1 -1
  92. package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +2 -0
  93. package/src/console/ui-next/dist/index.html +6 -6
  94. package/src/console/ui-next/seed-invoke-host.ts +2 -0
  95. package/src/console/ui-next/src/client.ts +35 -7
  96. package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
  97. package/src/console/ui-next/src/components/motion/table/types.ts +4 -0
  98. package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
  99. package/src/console/ui-next/src/features/flows/graph/build-flow-graph.test.ts +18 -0
  100. package/src/console/ui-next/src/features/flows/graph/build-flow-graph.ts +14 -1
  101. package/src/console/ui-next/src/features/flows/graph/neighborhood.test.ts +17 -0
  102. package/src/console/ui-next/src/features/flows/graph/neighborhood.ts +16 -3
  103. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +3 -1
  104. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  105. package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
  106. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +9 -3
  107. package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +29 -2
  108. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
  109. package/src/console/ui-next/src/features/observability/lib/ask-count.test.ts +25 -0
  110. package/src/console/ui-next/src/features/observability/lib/ask-count.ts +4 -1
  111. package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
  112. package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
  113. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
  114. package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
  115. package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
  116. package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
  117. package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
  118. package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
  119. package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
  120. package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
  121. package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
  122. package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
  123. package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
  124. package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
  125. package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
  126. package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
  127. package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
  128. package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
  129. package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
  130. package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
  131. package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
  132. package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
  133. package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
  134. package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
  135. package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
  136. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
  137. package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +12 -4
  138. package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
  139. package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
  140. package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
  141. package/src/docker/compose-health.test.ts +27 -0
  142. package/src/docker/compose-health.ts +19 -3
  143. package/src/docker/docker.test.ts +4 -1
  144. package/src/docker/dockerfile.ts +1 -1
  145. package/src/docker/recipes/pgdog.ts +2 -0
  146. package/src/drivers/ai-anthropic.ts +5 -0
  147. package/src/drivers/ai-ollama.ts +49 -30
  148. package/src/drivers/ai-openai-compatible.ts +57 -46
  149. package/src/drivers/ai-providers.test.ts +3 -0
  150. package/src/drivers/bun-native-completeness.test.ts +7 -9
  151. package/src/drivers/clock-postgres.ts +13 -9
  152. package/src/drivers/instances-postgres.ts +4 -6
  153. package/src/drivers/journal-postgres.ts +13 -9
  154. package/src/drivers/pg-rls.test.ts +87 -0
  155. package/src/drivers/pg-rls.ts +233 -7
  156. package/src/drivers/pglite.ts +26 -3
  157. package/src/drivers/postgres.test.ts +23 -0
  158. package/src/drivers/postgres.ts +133 -9
  159. package/src/drivers/redis.ts +11 -4
  160. package/src/drivers/signal-redis.ts +24 -14
  161. package/src/drivers/signal-types.ts +2 -1
  162. package/src/drivers/types.ts +10 -0
  163. package/src/elements/ai/declare.ts +109 -0
  164. package/src/elements/ai/errors.test.ts +5 -1
  165. package/src/elements/ai/errors.ts +30 -2
  166. package/src/elements/ai/eval.ts +4 -6
  167. package/src/elements/ai/mcp-client.test.ts +206 -0
  168. package/src/elements/ai/mcp-client.ts +362 -0
  169. package/src/elements/ai/mcp-http.ts +159 -0
  170. package/src/elements/ai/mcp-mock.ts +134 -0
  171. package/src/elements/ai/mcp-protocol.ts +234 -0
  172. package/src/elements/ai/mcp-stdio.test.ts +50 -0
  173. package/src/elements/ai/mcp-stdio.ts +212 -0
  174. package/src/elements/ai/mcp-transport.ts +70 -0
  175. package/src/elements/ai/runtime.ts +159 -29
  176. package/src/elements/ai.test.ts +139 -0
  177. package/src/elements/ai.ts +16 -0
  178. package/src/elements/clock/declare.ts +26 -1
  179. package/src/elements/clock/health.test.ts +43 -0
  180. package/src/elements/clock/runtime.ts +55 -2
  181. package/src/elements/clock/schedule.ts +71 -142
  182. package/src/elements/clock.test.ts +9 -40
  183. package/src/elements/clock.ts +6 -1
  184. package/src/elements/gate/boot.ts +5 -5
  185. package/src/elements/gate/declare.ts +36 -4
  186. package/src/elements/gate/flatten.ts +1 -1
  187. package/src/elements/gate/runtime.ts +1 -1
  188. package/src/elements/index.ts +2 -0
  189. package/src/elements/signal/declare.ts +2 -1
  190. package/src/elements/signal/runtime.ts +16 -1
  191. package/src/elements/signal.ts +1 -0
  192. package/src/elements/store/cache.test.ts +2 -0
  193. package/src/elements/store/cache.ts +3 -3
  194. package/src/elements/store/declare.ts +9 -6
  195. package/src/elements/store/emit-drizzle.ts +44 -8
  196. package/src/elements/store/resource.test.ts +1 -1
  197. package/src/elements/store/rls-identity.test.ts +95 -0
  198. package/src/elements/store/rls-identity.ts +116 -0
  199. package/src/elements/store/runtime.ts +4 -0
  200. package/src/elements/store/schema-decl.test.ts +37 -0
  201. package/src/elements/store/schema-decl.ts +161 -0
  202. package/src/elements/store/seed.ts +1 -1
  203. package/src/elements/store/sql-condition.test.ts +12 -0
  204. package/src/elements/store/sql-condition.ts +26 -2
  205. package/src/elements/store/sql-rls-isolation.test.ts +165 -0
  206. package/src/elements/store/sql-rls-stamp.test.ts +117 -0
  207. package/src/elements/store/sql-session.ts +81 -3
  208. package/src/elements/store/upsert-app.test.ts +1 -1
  209. package/src/elements/store.ts +14 -0
  210. package/src/elements/vault/builtin-adapter.ts +17 -0
  211. package/src/full.ts +2 -0
  212. package/src/http.ts +20 -3
  213. package/src/index.ts +14 -1
  214. package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
  215. package/src/kernel/adopt-routes.ts +30 -2
  216. package/src/kernel/app.ts +227 -77
  217. package/src/kernel/auto-cache.test.ts +8 -8
  218. package/src/kernel/auto-registry.test.ts +54 -9
  219. package/src/kernel/boot-bind/ai.ts +24 -0
  220. package/src/kernel/boot-bind/clock.ts +2 -0
  221. package/src/kernel/boot.test.ts +4 -4
  222. package/src/kernel/boot.ts +5 -3
  223. package/src/kernel/effects-stamping.test.ts +3 -3
  224. package/src/kernel/element-registries.ts +15 -5
  225. package/src/kernel/errors.ts +24 -0
  226. package/src/kernel/flow-units.ts +64 -0
  227. package/src/kernel/flow.ts +29 -17
  228. package/src/kernel/fx-dead-letters.test.ts +77 -0
  229. package/src/kernel/fx.test.ts +22 -0
  230. package/src/kernel/fx.ts +135 -6
  231. package/src/kernel/horizontal-child.ts +4 -4
  232. package/src/kernel/http-path-pending.ts +20 -0
  233. package/src/kernel/http-resource.ts +4 -0
  234. package/src/kernel/http-stream.test.ts +174 -0
  235. package/src/kernel/index.ts +2 -0
  236. package/src/kernel/pipeline.test.ts +2 -2
  237. package/src/kernel/plugin-elements.test.ts +1 -1
  238. package/src/kernel/plugin-needs.test.ts +1 -1
  239. package/src/kernel/ready.test.ts +2 -2
  240. package/src/kernel/resource-mount.test.ts +14 -0
  241. package/src/kernel/run-duration.test.ts +2 -2
  242. package/src/kernel/stamp-http.test.ts +79 -0
  243. package/src/kernel/stamp-http.ts +75 -0
  244. package/src/kernel/triggers.ts +52 -27
  245. package/src/manifest/mcp-ref.ts +88 -0
  246. package/src/manifest/types.ts +41 -2
  247. package/src/manifest/validate.test.ts +20 -0
  248. package/src/mcp/docs-server.ts +1 -1
  249. package/src/mcp/server.ts +1 -1
  250. package/src/plugins/compression.test.ts +21 -0
  251. package/src/plugins/compression.ts +1 -0
  252. package/src/release/measure.ts +2 -2
  253. package/src/runtime/bun.ts +41 -4
  254. package/src/runtime/json-code-block.test.ts +2 -2
  255. package/src/runtime/serve.test.ts +4 -4
  256. package/src/term.test.ts +52 -4
  257. package/src/term.ts +125 -17
  258. package/src/test/reset-element-registries.ts +10 -3
  259. package/src/cli/ask-seed.test.ts +0 -96
  260. package/src/cli/ask-seed.ts +0 -82
  261. package/src/console/ui-next/dist/assets/cache-glyph-CLPBqZeb.js +0 -1
  262. package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
  263. package/src/console/ui-next/dist/assets/flows-page-Dg8CTE29.js +0 -1
  264. package/src/console/ui-next/dist/assets/http-method-BJ92Z_ke.js +0 -1
  265. package/src/console/ui-next/dist/assets/index-Bp-R7jtM.js +0 -66
  266. package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
  267. package/src/console/ui-next/dist/assets/observability-page-DAnpEaq1.js +0 -4
  268. package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
  269. package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
  270. package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
  271. package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
  272. package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
  273. package/src/console/ui-next/dist/assets/skeleton-CL_X0GCj.js +0 -1
  274. package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
  275. package/src/console/ui-next/dist/assets/trace-detail-sheet-D16lWQMt.js +0 -2
  276. package/src/console/ui-next/dist/assets/units-page-BtQ0bqMe.js +0 -1
  277. package/src/console/ui-next/dist/assets/vault-page-Ca-MvcmJ.js +0 -2
@@ -71,7 +71,8 @@ export const sendConfirmation = on(
71
71
 
72
72
  </Steps>
73
73
 
74
- That's the loop: declare → `fx.emit` → `on(signal, flow)`. The runtime handles routing, retries, and dead-lettering.
74
+ That's the loop for `once` and `broadcast`: declare → `fx.emit` → `on(signal, flow)`.
75
+ `live` keeps the same emit; the listener is `bus.live()`, not a Flow.
75
76
 
76
77
  ## The three delivery physics
77
78
 
@@ -96,7 +97,7 @@ One placed order can need all three physics at once — pick a signal per job, n
96
97
 
97
98
  | Signal | Mode | Job |
98
99
  | --------------- | ----------- | ----------------------------------------------------------- |
99
- | `order-placed` | `once` | Fulfillment job — competing workers, retries, DLQ |
100
+ | `order-placed` | `once` | One job — competing workers, retries, DLQ |
100
101
  | `order-changed` | `broadcast` | Fan-out — cache invalidation **and** customer notification |
101
102
  | `order-status` | `live` | Status feed — retained history a late subscriber can replay |
102
103
 
@@ -107,7 +108,7 @@ import { z } from "zod";
107
108
  export const orderPlaced = signal("order-placed", {
108
109
  schema: z.object({ orderId: z.string(), total: z.number() }),
109
110
  delivery: "once",
110
- retries: 2,
111
+ retries: 3,
111
112
  deadLetter: true,
112
113
  });
113
114
 
@@ -126,25 +127,43 @@ export const orderStatus = signal("order-status", {
126
127
  });
127
128
  ```
128
129
 
129
- `once` two fulfillment workers compete; **exactly one** claims each message (at-least-once; make the handler idempotent):
130
+ A Flow **writes** all three tapes. `once` / `broadcast` **listen** with `on(signal, flow)`. `live` does not.
130
131
 
131
- ```typescript title="src/flows/fulfillment/on-order.ts"
132
- on(
132
+ ```typescript title="src/flows/orders/place.ts"
133
+ export const placeOrder = on(
134
+ http.post("/orders").public(),
135
+ flow("orders.place", {
136
+ in: z.object({ id: z.string(), total: z.number() }),
137
+ do: async (input, fx) => {
138
+ await fx.emit(orderPlaced, { orderId: input.id, total: input.total });
139
+ await fx.emit(orderChanged, { orderId: input.id, kind: "placed" });
140
+ await fx.emit(orderStatus, { orderId: input.id, status: "placed" });
141
+ await fx.emit(orderStatus, { orderId: input.id, status: "fulfilling" });
142
+ await fx.emit(orderStatus, { orderId: input.id, status: "shipped" });
143
+ },
144
+ }),
145
+ );
146
+ ```
147
+
148
+ Stacked status emits are so replay is visible in one request. A real app emits as the order moves.
149
+
150
+ `once` — the listener is `on(orderPlaced, flow)`. Two workers compete; **exactly one** claims each message (at-least-once; make the handler idempotent):
151
+
152
+ ```typescript title="src/flows/orders/send-confirmation.ts"
153
+ export const sendConfirmation = on(
133
154
  orderPlaced,
134
- flow("fulfillment.onOrder", {
135
- do: async ({ orderId }, fx) => {
136
- await fx.emit(orderChanged, { orderId, kind: "placed" });
137
- await fx.emit(orderStatus, { orderId, status: "fulfilling" });
138
- await fx.emit(orderStatus, { orderId, status: "shipped" });
155
+ flow("orders.sendConfirmation", {
156
+ do: async (input, fx) => {
157
+ await fx.send(orderConfirmed, { to: "user-1", data: input });
139
158
  },
140
159
  }),
141
160
  );
142
161
  ```
143
162
 
144
- `broadcast` — every subscriber gets its own copy of the same domain event:
163
+ `broadcast` — every `on(orderChanged, flow)` gets its own copy. Neither Flow knows the other exists:
145
164
 
146
165
  ```typescript title="src/flows/orders/side-effects.ts"
147
- on(
166
+ export const invalidateCache = on(
148
167
  orderChanged,
149
168
  flow("cache.onOrderChanged", {
150
169
  do: async ({ orderId }, fx) => {
@@ -153,7 +172,7 @@ on(
153
172
  }),
154
173
  );
155
174
 
156
- on(
175
+ export const notifyCustomer = on(
157
176
  orderChanged,
158
177
  flow("notify.onOrderChanged", {
159
178
  do: async ({ orderId }, fx) => {
@@ -163,27 +182,38 @@ on(
163
182
  );
164
183
  ```
165
184
 
166
- `live` — retain status updates and replay the **full history** to a late `bus.live()` subscriber (tracking UI, ops feed).
185
+ `live` — retain the tape. A late subscriber replays **full history** (`placed fulfilling → shipped`), then keeps going.
167
186
 
168
- Today that subscription is a **server-side** bus API. `createClient` does not yet expose SSE / WebSocket / `client.live` — see [Client](/docs/reference/client#signal-and-live-queries).
187
+ The listener is **`bus.live()`**, not `on(orderStatus, flow)`. `on(signal, flow)` does not replay retained live payloads. Today that API is server-side; `createClient` has no SSE / WebSocket / `client.live` yet — see [Client](/docs/reference/client#signal-and-live-queries).
169
188
 
170
189
  ```typescript
171
- await fx.emit(orderStatus, { orderId, status: "placed" });
190
+ const bus = app.bootResult?.signal?.bus;
191
+ if (!bus) throw new Error("signal runtime not booted");
172
192
 
173
- // Late subscriber (server / test harness) receives placed → fulfilling → shipped.
174
193
  const unsub = await bus.live("order-status", (payload) => {
175
- /* push to UI */
194
+ /* placed fulfilling → shipped */
176
195
  });
177
196
  ```
178
197
 
179
- **Why separate signals:** delivery is fixed per declaration. Competing work stays on `once`; fan-out stays on `broadcast`; the client-visible timeline stays on `live`. Switching a word later is cheap; mixing physics on one name is not.
198
+ **Why separate signals:** delivery is fixed per declaration. Competing work stays on `once`; fan-out stays on `broadcast`; the timeline stays on `live`. Switching a word later is cheap; mixing physics on one name is not.
180
199
 
181
200
  ## When delivery fails
182
201
 
183
202
  `once` signals retry automatically. Every attempt keeps a **typed failure reason**, and the full attempt history survives into the DLQ — so when a message lands there you see _why_ each attempt failed, not just that it did.
184
203
 
204
+ Query that queue from a Flow with `fx.deadLetters(signal)` — same handle as `fx.emit`. The compiler stamps `reads: ["signal:<name>"]`; another signal throws **OKE1001**.
205
+
185
206
  The **Console** (`:6533` → `/overview`) shows emit and consume on the graph, and delivery attempts in Traces — no separate broker UI to run.
186
207
 
208
+ ```typescript
209
+ on(
210
+ http.get("/notifications/failed"),
211
+ flow("notifications.failed", {
212
+ do: async (input, fx) => fx.json.withQuery(await fx.deadLetters(orderPlaced), input),
213
+ }),
214
+ );
215
+ ```
216
+
187
217
  <Callout title="Orphan emits and schema fail loudly">
188
218
  Zero subscribers throws **OKE1042** unless `optional: true`. A payload that fails the signal's
189
219
  Standard Schema throws **OKE1043** before the message is staged — same `validate()` path as Flow
@@ -225,9 +255,9 @@ Committed `once` messages survive process death. A claim sets `lockedBy` and a v
225
255
  `live` retains every delivered message and replays the **full history** to a late `bus.live()` subscriber. There is no TTL or max-count window on that retention today (the Console payload monitor shows only the newest 50 for display).
226
256
 
227
257
  <Callout title="Client subscription is not shipped yet">
228
- `delivery: "live"` means the **driver** retains and replays. `createClient` has no SSE / WebSocket
229
- / `client.live` yet — poll an HTTP Flow or use `bus.live()` server-side. See [Client · Signal and
230
- live queries](/docs/reference/client#signal-and-live-queries).
258
+ `delivery: "live"` means the **driver** retains and replays. Listen with `bus.live()` on the
259
+ server — not `on(signal, flow)`. `createClient` has no SSE yet — poll HTTP. See
260
+ [Client](/docs/reference/client#signal-and-live-queries).
231
261
  </Callout>
232
262
 
233
263
  ## Orphaned signal config
@@ -258,7 +288,10 @@ Pairing an arbitrary Store insert with emit inside **one** shared SQL transactio
258
288
  <Accordions>
259
289
  <Accordion title="Emit fails with OKE1042 (no subscriber)">
260
290
 
261
- You emitted a signal that nobody currently subscribes to. Wire a consumer with `on(signal, flow)` before emit, or set `optional: true` when zero-subscriber emits are intentional.
291
+ You emitted a signal that nobody currently subscribes to. For `once` and `broadcast`, wire
292
+ `on(signal, flow)` before emit.
293
+
294
+ `live` listeners are `bus.live()`, not a Flow — set `optional: true` when they may connect later.
262
295
 
263
296
  </Accordion>
264
297
  <Accordion title="Emit fails with OKE1043 (schema)">
@@ -268,7 +301,18 @@ The payload failed the signal's Standard Schema at emit time — nothing was sta
268
301
  </Accordion>
269
302
  <Accordion title="A message keeps retrying and then disappears">
270
303
 
271
- When `attempts > retries` the message moves to the DLQ (if `deadLetter: true`) — it is not lost. Open Console → Flows, inspect the typed failure on the trace, fix the consumer, then replay.
304
+ When `attempts > retries` the message moves to the DLQ (if `deadLetter: true`) — it is not lost. List it from a Flow with `fx.deadLetters(signal)`, or open Console → Flows, inspect the typed failure, fix the consumer, then replay.
305
+
306
+ </Accordion>
307
+ <Accordion title="fx.deadLetters throws OKE1001">
308
+
309
+ The flow read a signal it did not declare. The compiler infers `reads: ["signal:<name>"]` from `fx.deadLetters(signal)`. A different handle needs its own read.
310
+
311
+ </Accordion>
312
+ <Accordion title="on(liveSignal, flow) never replays history">
313
+
314
+ `on(signal, flow)` listens for `once` and `broadcast`. `live` replay is `bus.live()` on the Signal
315
+ bus — a Flow trigger does not replay retained payloads.
272
316
 
273
317
  </Accordion>
274
318
  <Accordion title="once vs broadcast vs live — how do I choose?">
@@ -290,7 +334,9 @@ At-least-once: crash-after-claim reclaims when the lease expires, and a handler
290
334
 
291
335
  ## Learn more
292
336
 
337
+ - [Project structure](/docs/get-started/project-structure) — signal files get a name, never a URL; `http.get("/…")` still wins
293
338
  - [Flow](/docs/elements/flow) — `on(trigger, flow)` and `fx.emit`
339
+ - [fx](/docs/reference/fx) — `fx.emit` and `fx.deadLetters`
294
340
  - [Clock](/docs/elements/clock) — scheduled and delayed work
295
341
 
296
342
  ## Next
@@ -19,6 +19,9 @@ only the driver changes.
19
19
  Every facet accepts optional `description` — a human title in the Console (falls back to the store
20
20
  name).
21
21
 
22
+ `store.sql` / `kv` / `files` / `index` auto-register when the declaring module is imported before
23
+ `oke()`. `oke({ stores })` is still valid and additive.
24
+
22
25
  ## SQL
23
26
 
24
27
  <StoreFacetMark facet="sql" />
@@ -39,9 +42,9 @@ Manifest tables, columns, and keywords.
39
42
  Classified PII stays masked unless the toolbar **PII** control is on
40
43
  (`revealPii: true`, audited).
41
44
 
42
- **Gate** views the result as Operator (default, bypasses RLS), public, a
43
- selected user (via their matching policy), or a Manifest policy gate.
44
- `asGate` sets `oke.gate` on postgres / pglite for `current_setting('oke.gate', true)`.
45
+ **Gate** views as Operator (bypass), public, a user, or a policy. SQL,
46
+ browse, and grid edit send `{ asGate, asUserId }`. postgres / pglite stamp
47
+ `oke.gate()` / `oke.user()` / `oke.has_scope()`; memory is catalog-only.
45
48
 
46
49
  The SQL band **Schema** control opens a visualizer of Manifest tables and
47
50
  relations — declared `.references()` plus inferred `*_id` columns — not the
@@ -89,16 +92,25 @@ When a newer packaged version exists, the name chip shows **Upgrade**
89
92
  (`ALTER EXTENSION … UPDATE`). Names link to the project or Postgres docs.
90
93
 
91
94
  Each SQL table row shows whether Postgres RLS is on (emerald shield)
92
- or off (muted shield). KV, files, and index have no badge. Enabling
93
- RLS on create flips that shield.
94
-
95
- **RLS Policies** lists `pg_policies`. Create policy pins the SQL preview;
96
- **Templates** slides a flush library beside it (every `FOR` command, owner,
97
- `oke.gate`). Or edit `roles` / `USING` / `WITH CHECK` in the grid (`ALTER POLICY`).
95
+ or off (muted shield). The resource header repeats that as an **RLS**
96
+ chip before **PII** — **N RLS** when enabled and policies exist. KV,
97
+ files, and index have no badge. Enabling RLS on create flips that
98
+ shield.
99
+
100
+ **RLS Policies** lists `pg_policies` plus Manifest policies (`declared` /
101
+ `live`). Two-finger click a row to open its sheet (Edit / Save on editable
102
+ columns); two-finger click empty grid to create. Create policy pins
103
+ SQL | Code (`store.schema.policy` + `pgPolicy`); drag the handle to
104
+ resize the preview.
105
+ **Templates** use `oke.user()` / `oke.gate()` / `oke.has_scope()`.
106
+ Owner cards open a **Column** select of the table fields (guessed
107
+ `creator_email` / `owner_email` when present) with the same PK / FK
108
+ marks as schema cards. Create stays blocked until a real column is
109
+ picked.
98
110
 
99
111
  Create policy picks a Gate posture (Read / Write / Both), or **Policy &
100
- scope** for `gate.public`, `gate.policy`, and `gate.scope`. `TO public`
101
- stays on the SQL — those picks are not Postgres roles.
112
+ scope**. Those picks fill `USING` / `WITH CHECK`. `TO` stays `public`
113
+ Gate names are not Postgres roles.
102
114
 
103
115
  ### Quick start
104
116
 
@@ -192,7 +204,7 @@ const notesR = store.resource(db, notes, {
192
204
  },
193
205
  });
194
206
 
195
- const mounted = on(http.resource("/notes", notesR.all()).gate.public.live());
207
+ const mounted = on(http.resource("/notes", notesR.all()).public().live());
196
208
  ```
197
209
 
198
210
  `.gate(...)` / `.live()` chain like `http.get` — gates on every verb, live on list and get.
@@ -240,7 +252,7 @@ list: { mode: "offset", filter: "none", limit: 20 },
240
252
 
241
253
  ```typescript
242
254
  export const list = on(
243
- http.get("/notes").gate.public,
255
+ http.get("/notes").public(),
244
256
  flow("notes.list", {
245
257
  out: z.array(Note),
246
258
  do: async (input, fx) => {
@@ -251,6 +263,8 @@ export const list = on(
251
263
  );
252
264
  ```
253
265
 
266
+ `list.ts` + `http.get()` fills `GET /notes` named `notes.list` — explicit strings still win. See [Project structure](/docs/get-started/project-structure).
267
+
254
268
  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.
255
269
 
256
270
  <Callout title="Preferred, not required">
@@ -292,15 +306,44 @@ await fx.store(db).delete(notes).where(lt(notes.createdAt, cutoff));
292
306
 
293
307
  The recommended path: declare tables ORM-agnostically, then let `oke db` emit real Drizzle (`pgTable` for Postgres / PGLite) into `src/db/schema.drizzle.ts`.
294
308
 
295
- | Field API | Meaning |
296
- | -------------------------------------------- | ----------------------------------------------- |
297
- | `field.text()` / `field.integer()` | v1 column primitives |
298
- | `.primaryKey()` · `.notNull()` · `.unique()` | constraints |
299
- | `.default(v)` · `.defaultFn(id \| now)` | defaults |
300
- | `.pii()` · `.sensitive()` · `.retain("30d")` | privacy classification |
301
- | `.as("sql_name")` | override the automatic `camelCase → snake_case` |
302
- | `.describe("…")` | human title in the Console (falls back to key) |
303
- | `.references(() => col, { onDelete })` | foreign key |
309
+ | Field API | Meaning |
310
+ | -------------------------------------------- | ---------------------------------------------------------------- |
311
+ | `field.text()` / `field.integer()` | v1 column primitives |
312
+ | `.primaryKey()` · `.notNull()` · `.unique()` | constraints |
313
+ | `.default(v)` · `.defaultFn(id \| now)` | defaults |
314
+ | `.pii()` · `.sensitive()` · `.retain("30d")` | privacy classification |
315
+ | `.as("sql_name")` | override the automatic `camelCase → snake_case` |
316
+ | `.describe("…")` | human title in the Console (falls back to key) |
317
+ | `.references(() => col, { onDelete })` | foreign key |
318
+ | `store.schema.rls()` | `pgTable.withRLS` when there are no policies |
319
+ | `store.schema.policy.gate/owner/scope` | happy-path RLS (`oke.gate()` / `oke.user()` / `oke.has_scope()`) |
320
+
321
+ Third argument on `store.schema.table` is Drizzle-shaped extras:
322
+
323
+ ```typescript
324
+ export const bookings = store.schema.table(
325
+ "bookings",
326
+ {
327
+ id: field.text().primaryKey(),
328
+ owner: field.text().notNull(),
329
+ },
330
+ [
331
+ store.schema.policy.gate("member", { for: "select" }),
332
+ store.schema.policy.owner("owner", { for: "all" }),
333
+ store.schema.policy.scope("booking:create", { for: "insert" }),
334
+ ],
335
+ );
336
+ ```
337
+
338
+ Helpers emit stable names (`gate_member_select`). Raw
339
+ `store.schema.policy("name", { as, to, for, using, withCheck })` is the
340
+ escape hatch — predicates use `oke.*`, never `current_setting`.
341
+
342
+ <Callout title="User-plane fx.store applies RLS">
343
+ HTTP / resource flows stamp Gate identity onto every postgres / pglite statement (`SET LOCAL ROLE
344
+ oke_app` + `set_config` in one pinned transaction). Table-owner bypass ends there; operator / cron
345
+ / CDC / signal / catalog stay unstamped.
346
+ </Callout>
304
347
 
305
348
  #### Foreign keys and relations
306
349
 
@@ -356,9 +399,9 @@ Staging/prod accumulate versioned SQL under `drizzle/` (`oke db generate`).
356
399
  | Staging / prod | `oke db generate` → review files → `oke db migrate` on that DB |
357
400
 
358
401
  <Callout title="Connection pooling is infrastructure, not app code" type="info">
359
- Bun.SQL defaults to **10** connections per process — fine per instance. Scale out and `N × pool`
360
- can exceed Postgres `max_connections`. `dev`/`prod` put **PgDog** in front; `DATABASE_URL` → port
361
- `6432`. No app code changes.
402
+ One shared Bun.SQL pool (**8** connections) per URL. `dev`/`prod` put **PgDog** in front
403
+ (`DATABASE_URL` port `6432`, `pool_size` 20). RLS stamps `reserve` one slot, then `SET LOCAL` in
404
+ that transaction a leaked `BEGIN` exhausts checkout.
362
405
  </Callout>
363
406
 
364
407
  **Why PgDog (not PgBouncer as the default).** Transaction pooling fixes the
@@ -371,8 +414,9 @@ Not wired this round — readiness only.
371
414
 
372
415
  #### Seeding
373
416
 
374
- `oke db seed` loads `defineSeed` from `src/db/seed/index.ts` — **never at boot**.
375
- `name` is this app's seed (template or example). The prompt and `.oke/state.json` key off it.
417
+ `oke db seed` loads `defineSeed` from `src/db/seed/index.ts` — a separate
418
+ command, never at boot. Not every project has a seed. In `oke dev`, `s` runs
419
+ the same command. `name` is this app's seed (template or example).
376
420
 
377
421
  <StoreSeeding />
378
422
 
@@ -642,8 +686,12 @@ Default `store.kv("sessions")` is cache-shaped — a Redis recreate drops keys.
642
686
  ```typescript
643
687
  export const sessions = store.kv("sessions", { description: "Session cache" });
644
688
  export const ledger = store.kv("ledger", { durable: true, description: "Idempotency keys" });
689
+ export const drafts = store.kv("drafts", { durable: true, description: "Compose drafts" });
645
690
  ```
646
691
 
692
+ **Consequence:** `oke db seed` into cache Redis looks fine until compose recreates the
693
+ container — Console Store then shows **No rows.** Seeded namespaces need `{ durable: true }`.
694
+
647
695
  Gate rates and Signal stay on `REDIS_URL`. Missing `DATABASE_URL` with the postgres driver
648
696
  fails boot: `oke boot: durable store.kv needs DATABASE_URL`.
649
697
 
@@ -777,7 +825,7 @@ images: {
777
825
 
778
826
  ### Images — `image` / `putImage`
779
827
 
780
- For photos, use Bun's built-in `Bun.Image` pipeline on the same handle (requires Bun `>=1.3.14`).
828
+ For photos, use Bun's built-in `Bun.Image` pipeline on the same handle (requires Bun `>=1.4.0`).
781
829
  `putImage` is one write that fans into several keys:
782
830
 
783
831
  <StoreFilesVariants />
@@ -1014,6 +1062,11 @@ Inserts and selects now resolve it from the declaration. Keep the `name` column.
1014
1062
 
1015
1063
  Schema DDL never runs automatically in `prod`. Under `oke dev`, `db.autoPush` (default `true`) runs `oke db push`. For production use `oke db generate` + `oke db migrate`.
1016
1064
 
1065
+ </Accordion>
1066
+ <Accordion title="oke db push: missing_hints">
1067
+
1068
+ New tables are created automatically. Push only manages `public` — it will not drop schema `oke` (RLS helpers) or `oke_console`. Remaining `missing_hints` are destructive (drop a non-empty public table, type change). Review with `oke db generate`.
1069
+
1017
1070
  </Accordion>
1018
1071
  <Accordion title="I need a join — with: is not supported">
1019
1072
 
@@ -1064,11 +1117,17 @@ Unlike KV/Files, unset `drivers.store.index` does **not** promote to `pgvector`
1064
1117
 
1065
1118
  `.oke/state.json` stores `seededAt` plus this app's `defineSeed({ name })` — notes vs keel are different ids. A new name re-asks. Clear those keys to prompt again.
1066
1119
 
1120
+ </Accordion>
1121
+ <Accordion title="Console KV shows No rows after oke db seed">
1122
+
1123
+ 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.
1124
+
1067
1125
  </Accordion>
1068
1126
  </Accordions>
1069
1127
 
1070
1128
  ## Learn more
1071
1129
 
1130
+ - [Project structure](/docs/get-started/project-structure) — `list.ts` + `http.get()` is `GET /notes` named `notes.list`
1072
1131
  - [Flow](/docs/elements/flow) — the `fx.store` session inside `do`
1073
1132
  - [AI](/docs/elements/ai) — `ai.embed` into a vector `store.index`, searched via `fx.search`
1074
1133
  - [Gate](/docs/elements/gate) — `pii:reveal` and other permissions on flows
@@ -363,6 +363,7 @@ Schedule the live command with cron when you want automatic cleanup.
363
363
 
364
364
  ## Learn more
365
365
 
366
+ - [Project structure](/docs/get-started/project-structure) — `flow({…})` names as `unit.export`; `flow("billing.charge")` still wins
366
367
  - [Flow](/docs/elements/flow) — how `fx.vault.get` reads secrets inside `do`
367
368
  - [CLI Reference](/docs/reference/cli) — `oke vault set` · `list` · `import`
368
369
 
@@ -45,19 +45,21 @@ lists HTTP routes — static GET paths are links. `curl` still gets `{ data, err
45
45
 
46
46
  The standard starter already exports a named health Flow:
47
47
 
48
- ```typescript title="flows/main"
49
- import { on, flow, http, gate } from "okengine/http";
48
+ ```typescript title="flows/main/health.ts"
49
+ import { on, flow, http } from "okengine";
50
50
  import { z } from "zod";
51
51
 
52
52
  export const health = on(
53
- http.get("/health").gate.public,
54
- flow("main.health", {
53
+ http.get().public(),
54
+ flow({
55
55
  out: z.object({ ok: z.literal(true) }),
56
56
  do: () => ({ ok: true as const }),
57
57
  }),
58
58
  );
59
59
  ```
60
60
 
61
+ `main` omits the URL prefix, so this file is `GET /health` named `main.health`. See [Project structure](/docs/get-started/project-structure).
62
+
61
63
  Named flows show up as `main.health` (not `flow_1`) in `oke dev` logs, with the run id after the timestamp.
62
64
  Change the route or output and save — App and Console update together from the
63
65
  same Manifest.
@@ -67,20 +69,19 @@ same Manifest.
67
69
  <Step>
68
70
  ### Call it from a typed client
69
71
 
70
- Adopted modules become namespaces on the client:
72
+ Generated units become namespaces on the client:
71
73
 
72
74
  ```typescript title="app"
73
75
  import "@/core";
74
-
76
+ import "@/flows/generated";
75
77
  import { oke } from "okengine/http";
76
- import * as routes from "@/flows/generated";
77
78
 
78
- export const app = oke({ name: "notes" }).adopt(routes);
79
+ export const app = oke({ name: "notes" });
79
80
  export type App = typeof app;
80
81
  ```
81
82
 
82
- `import "@/core"` loads stores, secrets, channels, and message catalogs
83
- (via `locales/index.ts`) so they auto-register before `oke()`.
83
+ `import "@/core"` loads stores, secrets, gates, clocks, channels, and
84
+ message catalogs (via `locales/index.ts`) so they auto-register before `oke()`.
84
85
 
85
86
  ```typescript title="client"
86
87
  import { createClient } from "okengine/client";
@@ -107,7 +108,7 @@ Every backend behavior has one shape:
107
108
 
108
109
  ## From Flow to client
109
110
 
110
- Export → adopt → call or test. Same App type — no separate codegen project.
111
+ Export → generated barrel → call or test. Same App type — no separate codegen project.
111
112
 
112
113
  <ClientLoop />
113
114
 
@@ -137,8 +138,10 @@ bun test
137
138
  <Accordions>
138
139
 
139
140
  <Accordion title="api.main.health is not a function / type error">
140
- Confirm the Flow is `export`ed from the module you `.adopt({main})`, and that `createClient` is
141
- typed with your `App` type. Restart `oke dev` after renaming exports so the Manifest refreshes.
141
+ Confirm the Flow is `export`ed from a generated unit (`import "@/flows/generated"` then `oke({ name })`), or from a module you still `.adopt({ main })`.
142
+
143
+ Type `createClient` with your `App` type. Restart `oke dev` after renaming exports so the Manifest refreshes.
144
+
142
145
  </Accordion>
143
146
 
144
147
  <Accordion title="createTestApp fails to boot">
@@ -151,6 +154,7 @@ bun test
151
154
 
152
155
  ## Learn more
153
156
 
157
+ - [Project structure](/docs/get-started/project-structure) — folders are the URL
154
158
  - [Flow](/docs/elements/flow) — triggers, contracts, effects, and composition
155
159
  - [Store](/docs/elements/store) — SQL, KV, files, index, and the preferred list envelope
156
160
  - [fx](/docs/reference/fx) — the full effect surface
@@ -158,6 +162,11 @@ bun test
158
162
  ## Next
159
163
 
160
164
  <Cards>
165
+ <Card
166
+ title="Project structure"
167
+ description="Folders are the URL; unit plus export is the name."
168
+ href="/docs/get-started/project-structure"
169
+ />
161
170
  <Card
162
171
  title="Flow"
163
172
  description="Triggers, contracts, effects, and composition."
@@ -30,6 +30,11 @@ Learn the shape once, install on Bun, then write your first Flow from the standa
30
30
  description="Health Flow, typed client, Console proof."
31
31
  href="/docs/get-started/basic-usage"
32
32
  />
33
+ <Card
34
+ title="Project structure"
35
+ description="Folder and file conventions — URL, name, and the generated barrel."
36
+ href="/docs/get-started/project-structure"
37
+ />
33
38
  <Card
34
39
  title="Deployment"
35
40
  description="Compose, Swarm, Kubernetes, and reverse proxy."
@@ -6,7 +6,7 @@ icon: Download
6
6
  ---
7
7
 
8
8
  This page gets you from zero to a running app on Bun. The engine targets
9
- **Bun ≥ 1.3.14** — prefer it for install, scaffold, and `oke dev`.
9
+ **Bun ≥ 1.4.0** — prefer it for install, scaffold, and `oke dev`.
10
10
 
11
11
  <Callout title="The one rule">
12
12
  The npm package is `okengine`; the CLI binary is `oke`. Use `bun add` in a project or `bun install
@@ -20,7 +20,7 @@ This page gets you from zero to a running app on Bun. The engine targets
20
20
  <Step>
21
21
  ### Prerequisites
22
22
 
23
- - [Bun](https://bun.sh) ≥ 1.3.14 (`bun --version`)
23
+ - [Bun](https://bun.sh) ≥ 1.4.0 (`bun --version`)
24
24
  - [Docker](https://docs.docker.com/get-docker/) with a running daemon (`docker info`)
25
25
  - A terminal and a code editor
26
26
 
@@ -23,19 +23,21 @@ shape. Learn the shape once; only the trigger changes.
23
23
 
24
24
  Four contracts plus a `do`. This is the standard starter's health check:
25
25
 
26
- ```typescript title="health"
26
+ ```typescript title="flows/main/health.ts"
27
27
  import { on, flow, http } from "okengine";
28
28
  import { z } from "zod";
29
29
 
30
30
  export const health = on(
31
- http.get("/health"),
32
- flow("health.check", {
31
+ http.get().public(),
32
+ flow({
33
33
  out: z.object({ ok: z.literal(true) }),
34
34
  do: () => ({ ok: true as const }),
35
35
  }),
36
36
  );
37
37
  ```
38
38
 
39
+ `main` omits the URL prefix, so this file is `GET /health` named `main.health`. See [Project structure](/docs/get-started/project-structure). Explicit `http.get("/health")` and `flow("main.health")` still win.
40
+
39
41
  </Step>
40
42
 
41
43
  <Step>
@@ -162,6 +164,7 @@ and `okengine/full`.
162
164
  | Topic | Page |
163
165
  | ------- | ------------------------------------------------------------------------------- |
164
166
  | Why OKE | [Why OKE](/docs/get-started/why) |
167
+ | Folders | [Project structure](/docs/get-started/project-structure) |
165
168
  | Flow | [Flow](/docs/elements/flow) |
166
169
  | `fx` | [fx](/docs/reference/fx) |
167
170
  | Agents | [MCP](/docs/ai/mcp) · [Skills](/docs/ai/skills) · [llms.txt](/docs/ai/llms-txt) |
@@ -184,4 +187,9 @@ and `okengine/full`.
184
187
  description="Health Flow, typed client, and bun:test."
185
188
  href="/docs/get-started/basic-usage"
186
189
  />
190
+ <Card
191
+ title="Project structure"
192
+ description="Folders are the URL; unit plus export is the name."
193
+ href="/docs/get-started/project-structure"
194
+ />
187
195
  </Cards>
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "title": "Get Started",
3
3
  "icon": "Rocket",
4
- "pages": ["index", "introduction", "why", "installation", "basic-usage"]
4
+ "pages": ["index", "introduction", "why", "installation", "basic-usage", "project-structure"]
5
5
  }