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
@@ -0,0 +1,932 @@
1
+ ---
2
+ title: "Project structure"
3
+ description: "Folder and file conventions for an OKE app — how the tree becomes URLs, Flow names, and the generated barrel."
4
+ icon: "FolderTree"
5
+ source: "docs/spec/unified-theory.md"
6
+ ---
7
+
8
+ The standard Notes starter is a folder tree. Put a file at `flows/notes/[id]/get.ts`, export `get`, and you get `GET /notes/:id` named `notes.get` — still `on(http.get(), flow({…}))`.
9
+
10
+ <Callout title="The one rule">
11
+ Folders are the URL. The first-level folder plus the export name is the Flow name. Import the
12
+ generated barrel, then `oke({name})`.
13
+ </Callout>
14
+
15
+ ## Quick start
16
+
17
+ <Steps>
18
+
19
+ <Step>
20
+ ### Write a tree file
21
+
22
+ ```typescript title="flows/notes/[id]/get.ts"
23
+ import { on, flow, http } from "okengine";
24
+
25
+ export const get = on(
26
+ http.get().public(),
27
+ flow({
28
+ in: NoteIdIn,
29
+ out: NoteOut,
30
+ do: async (input, fx) => fx.store(db).findById(notes, input.id),
31
+ }),
32
+ );
33
+ ```
34
+
35
+ </Step>
36
+
37
+ <Step>
38
+ ### Load the generated barrel
39
+
40
+ ```typescript title="app.ts"
41
+ import "@/core";
42
+ import "@/flows/generated";
43
+ import { oke } from "okengine/http";
44
+
45
+ export const app = oke({ name: "notes" });
46
+ export type App = typeof app;
47
+ ```
48
+
49
+ `.adopt({ notes })` is optional and additive.
50
+
51
+ </Step>
52
+
53
+ <Step>
54
+ ### Call it
55
+
56
+ `oke dev` regenerates the barrel. The client key is the export:
57
+
58
+ ```typescript
59
+ const { data } = await api.notes.get({ id });
60
+ ```
61
+
62
+ </Step>
63
+
64
+ </Steps>
65
+
66
+ ## Folder and file conventions
67
+
68
+ ### Top-level folders
69
+
70
+ The app source folder holds behavior. Config and the Vite SPA sit beside it.
71
+
72
+ | Folder | Role |
73
+ | -------------------- | --------------------------------------------------------------------------- |
74
+ | `app.ts` | `import "@/core"` + `import "@/flows/generated"` + `oke({ name })` |
75
+ | `core.ts` / `core/` | Store, gate, vault, channel, clock, AI decls — auto-register before `oke()` |
76
+ | `flows/<unit>/` | One client unit per first-level folder (`notes`, `main`) |
77
+ | `flows/generated.ts` | Regenerated barrel — do not edit |
78
+ | `db/` | `schema.decl.ts`, seed, and the Drizzle file `oke db` emits |
79
+ | `locales/` | Message catalogs (`import "@/locales"` from core) |
80
+ | `web/` | Vite SPA — `createClient` and proxies `/notes` · `/health` to **6530** |
81
+ | `tests/` | `oke test` / `bun:test` against the same `App` type |
82
+ | `docker/` | Compose from `oke docker` / `oke dev` (not in the clean template) |
83
+ | `.oke/` | Machine state — gitignored |
84
+
85
+ `core/` is a split of the same decls (`store.ts`, `gate.ts`, …) with `core.ts` as the barrel. Not a ninth element. Load order in the starter: locales → store → gate → vault → channel → AI.
86
+
87
+ <Tabs items={["core.ts", "schema.decl.ts", "locales", "web", "tests"]}>
88
+ <Tab value="core.ts">
89
+
90
+ ```typescript title="core.ts"
91
+ import "@/locales";
92
+ import { channel, gate, store, vault } from "okengine";
93
+ import { z } from "zod";
94
+ import * as schema from "@/db/schema.decl";
95
+
96
+ export const db = store.sql("app", { schema });
97
+ export const notesWrite = gate.policy("notes:write", () => true);
98
+ export const webhookSecret = vault.secret("APP_WEBHOOK_SECRET", {
99
+ dev: "dev-webhook-secret-change-me",
100
+ });
101
+ export const noteCreatedMail = channel
102
+ .email({ from: "Notes <notes@localhost>" })
103
+ .template("note-created", {
104
+ locales: ["en"],
105
+ schema: z.object({ id: z.string(), title: z.string() }),
106
+ });
107
+ ```
108
+
109
+ </Tab>
110
+ <Tab value="schema.decl.ts">
111
+
112
+ ```typescript title="db/schema.decl.ts"
113
+ import { store, field, id, now } from "okengine";
114
+
115
+ export const notes = store.schema.table("notes", {
116
+ id: field.text().primaryKey().defaultFn(id),
117
+ title: field.text().notNull(),
118
+ body: field.text().notNull(),
119
+ archivedAt: field.integer(),
120
+ createdAt: field.integer().notNull().defaultFn(now),
121
+ });
122
+ ```
123
+
124
+ </Tab>
125
+ <Tab value="locales">
126
+
127
+ ```typescript title="locales/en.ts"
128
+ import { defineMessages, defineLocale } from "okengine";
129
+
130
+ export const en = defineMessages({
131
+ notes: { created: "Note “{title}” was created." },
132
+ });
133
+ defineLocale("en", en);
134
+ ```
135
+
136
+ </Tab>
137
+ <Tab value="web">
138
+
139
+ ```typescript title="web/src/client.ts"
140
+ import { createClient } from "okengine/client";
141
+
142
+ export const api = createClient<App>(import.meta.env.VITE_API_URL ?? "");
143
+ const { data } = await api.notes.get({ id });
144
+ ```
145
+
146
+ </Tab>
147
+ <Tab value="tests">
148
+
149
+ ```typescript title="tests/standard.test.ts"
150
+ import { createTestApp } from "okengine/test";
151
+ import { app } from "@/app";
152
+
153
+ const t = await createTestApp(app);
154
+ const { data } = await t.api.main.health({});
155
+ ```
156
+
157
+ </Tab>
158
+ </Tabs>
159
+
160
+ ### Top-level files
161
+
162
+ | File | Role |
163
+ | -------------------------- | ---------------------------------------------------------- |
164
+ | `oke.config.ts` | Drivers, image pins, `db.declare` / `db.generated`, `i18n` |
165
+ | `package.json` | `oke dev` · `oke test` · `bun run web` |
166
+ | `drizzle.config.ts` | drizzle-kit — `oke db push` / `generate` / `migrate` |
167
+ | `tsconfig.json` | `@/*` → the app source folder |
168
+ | `.env.example` | Tracked sample keys |
169
+ | `.env` / `.env.local` | Local secrets — gitignored |
170
+ | `.gitignore` | Ignores `.oke/`, env files, `node_modules/` |
171
+ | `AGENTS.md` | Agent contract written by `create-oke` |
172
+ | `.github/workflows/ci.yml` | Template CI |
173
+
174
+ ```typescript title="oke.config.ts"
175
+ import { defineConfig } from "okengine/config";
176
+
177
+ export default defineConfig({
178
+ db: {
179
+ declare: "src/db/schema.decl.ts",
180
+ generated: "src/db/schema.drizzle.ts",
181
+ },
182
+ i18n: { locales: ["en"], default: "en" },
183
+ });
184
+ ```
185
+
186
+ See [Configuration](/docs/reference/configuration) and [Environment variables](/docs/reference/environment-variables).
187
+
188
+ ### Notes starter tree
189
+
190
+ Standard create-oke layout. Advanced adds `digest.ts`, `[id]/attach.ts`, and `[id]/summarize.ts`. `oke dev` writes the barrel — do not edit it.
191
+
192
+ <Tabs items={["Tree", "generated.ts"]}>
193
+ <Tab value="Tree">
194
+
195
+ ```text
196
+ app.ts
197
+ core.ts
198
+ flows/
199
+ generated.ts
200
+ main/
201
+ health.ts GET /health main.health
202
+ route.ts GET / main.root
203
+ shapes.ts
204
+ signals.ts
205
+ notes/
206
+ list.ts GET /notes notes.list
207
+ create.ts POST /notes notes.create
208
+ on-created.ts (signal) notes.onCreated
209
+ shapes.ts
210
+ signals.ts
211
+ [id]/
212
+ get.ts GET /notes/:id notes.get
213
+ archive.ts POST /notes/:id/archive notes.archive
214
+ db/
215
+ schema.decl.ts
216
+ seed/
217
+ locales/
218
+ web/
219
+ tests/
220
+ oke.config.ts
221
+ ```
222
+
223
+ </Tab>
224
+ <Tab value="generated.ts">
225
+
226
+ ```typescript title="flows/generated.ts"
227
+ import { registerFlowUnits, stampFlowName, stampHttpPath } from "okengine";
228
+ import * as notes_list from "./notes/list.ts";
229
+ import * as notes_$id$_get from "./notes/[id]/get.ts";
230
+
231
+ const notes = {
232
+ list: stampHttpPath(stampFlowName(notes_list.list, "notes.list"), "/notes"),
233
+ get: stampHttpPath(stampFlowName(notes_$id$_get.get, "notes.get"), "/notes/:id"),
234
+ };
235
+ export { notes };
236
+ registerFlowUnits({ notes });
237
+ ```
238
+
239
+ </Tab>
240
+ </Tabs>
241
+
242
+ ## Routing files
243
+
244
+ A folder is a URL segment. A route exists when a walked `.ts` file exports a Flow — `shapes.ts` alone does not publish a path.
245
+
246
+ ### Reserved leaves
247
+
248
+ These names do **not** add a segment. Same five ops as `http.resource`, plus `index` / `route`.
249
+
250
+ | Leaf | Typical method | URL if under `notes/` |
251
+ | -------- | --------------- | ---------------------------------------------- |
252
+ | `list` | `http.get()` | `/notes` |
253
+ | `create` | `http.post()` | `/notes` |
254
+ | `get` | `http.get()` | `/notes` or `/notes/:id` when nested |
255
+ | `update` | `http.patch()` | `/notes` or `/notes/:id` |
256
+ | `remove` | `http.delete()` | `/notes` or `/notes/:id` |
257
+ | `index` | any | `/notes` — reserved leaf, not a barrel |
258
+ | `route` | any | `/notes` — several exports may share this path |
259
+
260
+ Any other leaf **does** add a segment (`archive.ts` → `/archive`). Method always comes from `http.get` / `http.post` / … — never from the filename.
261
+
262
+ Several Flow exports in one `route.ts` share the inferred path; different methods are OK. The same method twice fails boot (**OKE1011**).
263
+
264
+ <Tabs items={["list", "create", "get", "archive", "route.ts"]}>
265
+ <Tab value="list">
266
+
267
+ ```typescript title="flows/notes/list.ts"
268
+ export const list = on(
269
+ http.get().public(),
270
+ flow({
271
+ out: NoteListOut,
272
+ do: async (input, fx) => fx.json.withQuery(await fx.store(db).select().from(notes), input),
273
+ }),
274
+ );
275
+ ```
276
+
277
+ </Tab>
278
+ <Tab value="create">
279
+
280
+ ```typescript title="flows/notes/create.ts"
281
+ export const create = on(
282
+ http.post().gate(notesMutate),
283
+ flow({
284
+ in: NoteCreateIn,
285
+ out: NoteOut,
286
+ do: async (input, fx) => {
287
+ const id = fx.id();
288
+ const createdAt = fx.clock.now();
289
+ await fx
290
+ .store(db)
291
+ .insert(notes)
292
+ .values({ id, ...input, archivedAt: null, createdAt });
293
+ await fx.emit(noteCreated, { id, title: input.title }, { key: id });
294
+ return { id, ...input, archivedAt: null, createdAt };
295
+ },
296
+ }),
297
+ );
298
+ ```
299
+
300
+ </Tab>
301
+ <Tab value="get">
302
+
303
+ ```typescript title="flows/notes/[id]/get.ts"
304
+ export const get = on(
305
+ http.get().public(),
306
+ flow({
307
+ in: NoteIdIn,
308
+ out: NoteOut,
309
+ errors: { NotFound },
310
+ do: async ({ id }, fx) => {
311
+ const row = await fx.store(db).findById(notes, id);
312
+ if (!row) return fail("NotFound", { id });
313
+ return {
314
+ id: String(row.id),
315
+ title: String(row.title),
316
+ body: String(row.body),
317
+ archivedAt: null,
318
+ createdAt: Number(row.createdAt),
319
+ };
320
+ },
321
+ }),
322
+ );
323
+ ```
324
+
325
+ </Tab>
326
+ <Tab value="archive">
327
+
328
+ ```typescript title="flows/notes/[id]/archive.ts"
329
+ export const archive = on(
330
+ http.post().gate(notesMutate),
331
+ flow({
332
+ in: NoteIdIn,
333
+ out: NoteOut,
334
+ do: async ({ id }, fx) => {
335
+ await fx.store(db).update(notes).set({ archivedAt: fx.clock.now() }).where(eq(notes.id, id));
336
+ return fx.store(db).findById(notes, id);
337
+ },
338
+ }),
339
+ );
340
+ ```
341
+
342
+ </Tab>
343
+ <Tab value="route.ts">
344
+
345
+ ```typescript title="flows/notes/route.ts"
346
+ export const list = on(http.get().public(), flow({ do: async () => [] }));
347
+ export const create = on(http.post().gate(notesMutate), flow({ do: async (input) => input }));
348
+ ```
349
+
350
+ </Tab>
351
+ </Tabs>
352
+
353
+ ### Skip list
354
+
355
+ Never routes. Safe to colocate next to walked files.
356
+
357
+ | Pattern | Why |
358
+ | -------------- | ----------------------------------------------- |
359
+ | `generated.ts` | The barrel itself |
360
+ | `shapes.ts` | Zod contracts |
361
+ | `signals.ts` | Signal decls |
362
+ | `*.test.ts` | Tests |
363
+ | `_` prefix | Private — file or folder, and every nested file |
364
+
365
+ <Tabs items={["shapes.ts", "signals.ts"]}>
366
+ <Tab value="shapes.ts">
367
+
368
+ ```typescript title="flows/notes/shapes.ts"
369
+ import { z } from "zod";
370
+
371
+ export const NoteIdIn = z.object({ id: z.string().min(1) });
372
+ export const NoteOut = z.object({ id: z.string(), title: z.string() });
373
+ ```
374
+
375
+ </Tab>
376
+ <Tab value="signals.ts">
377
+
378
+ ```typescript title="flows/notes/signals.ts"
379
+ import { signal } from "okengine";
380
+ import { z } from "zod";
381
+
382
+ export const noteCreated = signal("note-created", {
383
+ delivery: "once",
384
+ retries: 3,
385
+ deadLetter: true,
386
+ schema: z.object({ id: z.string(), title: z.string() }),
387
+ });
388
+ ```
389
+
390
+ </Tab>
391
+ </Tabs>
392
+
393
+ ### On disk → URL → name
394
+
395
+ | On disk | URL | Flow name |
396
+ | ------------------------------------------------ | ------------------------- | ----------------- |
397
+ | `notes/list.ts` + `export const list` | `GET /notes` | `notes.list` |
398
+ | `notes/create.ts` + `export const create` | `POST /notes` | `notes.create` |
399
+ | `notes/[id]/get.ts` + `export const get` | `GET /notes/:id` | `notes.get` |
400
+ | `notes/[id]/archive.ts` + `export const archive` | `POST /notes/:id/archive` | `notes.archive` |
401
+ | `notes/on-created.ts` | (signal — no URL) | `notes.onCreated` |
402
+ | `notes/digest.ts` + `on(every("1d"), …)` | (clock — no URL) | `notes.digest` |
403
+
404
+ `[id]` / `(group)` never enter the Flow name. Renaming an export renames the Flow (`fx.call`, agent tools, Console).
405
+
406
+ <Tabs items={["signal", "clock"]}>
407
+ <Tab value="signal">
408
+
409
+ ```typescript title="flows/notes/on-created.ts"
410
+ export const onCreated = on(
411
+ noteCreated,
412
+ flow({
413
+ do: async (payload, fx) => {
414
+ await fx.send(noteCreatedMail, { to: "you@localhost", data: payload });
415
+ },
416
+ }),
417
+ );
418
+ ```
419
+
420
+ </Tab>
421
+ <Tab value="clock">
422
+
423
+ ```typescript title="flows/notes/digest.ts"
424
+ export const digest = on(
425
+ every("1d"),
426
+ flow({
427
+ out: z.object({ active: z.number(), at: z.number() }),
428
+ do: async (_input, fx) => ({
429
+ active: (await fx.store(db).select().from(notes)).length,
430
+ at: fx.clock.now(),
431
+ }),
432
+ }),
433
+ );
434
+ ```
435
+
436
+ </Tab>
437
+ </Tabs>
438
+
439
+ ## Nested routes
440
+
441
+ Folders nest segments. A public HTTP path appears only when a walked file plus `http.*` sits at that depth.
442
+
443
+ | On disk | URL | Name |
444
+ | -------------------------------------------------- | --------------------- | ---------------- |
445
+ | `notes/list.ts` | `/notes` | `notes.list` |
446
+ | `notes/[id]/get.ts` | `/notes/:id` | `notes.get` |
447
+ | `notes/[id]/comments.ts` + `export const comments` | `/notes/:id/comments` | `notes.comments` |
448
+ | `comments/list.ts` | `/comments` | `comments.list` |
449
+
450
+ Two files in one unit cannot both export `list`. Rename the nested export, or start a `comments/` unit.
451
+
452
+ <Tabs items={["Action leaf", "New unit"]}>
453
+ <Tab value="Action leaf">
454
+
455
+ ```typescript title="flows/notes/[id]/comments.ts"
456
+ export const comments = on(
457
+ http.get().public(),
458
+ flow({
459
+ in: NoteIdIn,
460
+ do: async ({ id }, fx) =>
461
+ fx.store(db).select().from(noteComments).where(eq(noteComments.noteId, id)),
462
+ }),
463
+ );
464
+ ```
465
+
466
+ </Tab>
467
+ <Tab value="New unit">
468
+
469
+ ```typescript title="flows/comments/list.ts"
470
+ export const list = on(
471
+ http.get().public(),
472
+ flow({
473
+ out: z.array(CommentOut),
474
+ do: async (input, fx) =>
475
+ fx.json.withQuery(await fx.store(db).select().from(noteComments), input),
476
+ }),
477
+ );
478
+ ```
479
+
480
+ </Tab>
481
+ </Tabs>
482
+
483
+ ## Dynamic segments
484
+
485
+ | On disk | URL | Client fill |
486
+ | ----------------------- | ------------ | ------------------ |
487
+ | `notes/[id]/get.ts` | `/notes/:id` | `{ id }` |
488
+ | `docs/[...slug]/get.ts` | `/docs/*` | `{ "*": "a/b/c" }` |
489
+
490
+ `[id]` → `:id`. `[...slug]` → `*` (the router wildcard). `[[...slug]]` is not supported — generate fails.
491
+
492
+ `restRequest` only substitutes `:name`. Call `api.docs.get({ "*": "a/b/c" })`, not `{ slug }`. Named rest is a later router change.
493
+
494
+ Wildcards skip Bun.serve native `routes` (`*` is not a native path) and fall through to `app.fetch`.
495
+
496
+ <Tabs items={["[id]", "[...slug]"]}>
497
+ <Tab value="[id]">
498
+
499
+ ```typescript title="flows/notes/[id]/get.ts"
500
+ export const get = on(
501
+ http.get().public(),
502
+ flow({
503
+ in: z.object({ id: z.string() }),
504
+ out: NoteOut,
505
+ do: async ({ id }, fx) => fx.store(db).findById(notes, id),
506
+ }),
507
+ );
508
+ ```
509
+
510
+ ```typescript title="client"
511
+ await api.notes.get({ id: "n1" });
512
+ ```
513
+
514
+ </Tab>
515
+ <Tab value="[...slug]">
516
+
517
+ ```typescript title="flows/docs/[...slug]/get.ts"
518
+ export const get = on(
519
+ http.get().public(),
520
+ flow({
521
+ in: z.object({ "*": z.string() }),
522
+ out: z.object({ path: z.string() }),
523
+ do: async (input) => ({ path: input["*"] }),
524
+ }),
525
+ );
526
+ ```
527
+
528
+ ```typescript title="client"
529
+ const { data } = await api.docs.get({ "*": "getting-started/install" });
530
+ ```
531
+
532
+ </Tab>
533
+ </Tabs>
534
+
535
+ ## Route groups
536
+
537
+ Wrap a folder in parentheses to organize without a URL segment.
538
+
539
+ | On disk | URL | Notes |
540
+ | ------------------------- | ---------------- | ----------------------------- |
541
+ | `notes/(ops)/archive.ts` | `/notes/archive` | `(ops)` omitted |
542
+ | `main/(public)/health.ts` | `/health` | `main` still omits its prefix |
543
+
544
+ <Tabs items={["(ops)", "(public)"]}>
545
+ <Tab value="(ops)">
546
+
547
+ ```typescript title="flows/notes/(ops)/archive.ts"
548
+ export const archive = on(
549
+ http.post().gate(notesMutate),
550
+ flow({
551
+ in: NoteIdIn,
552
+ out: NoteOut,
553
+ do: async ({ id }, fx) => {
554
+ await fx.store(db).update(notes).set({ archivedAt: fx.clock.now() }).where(eq(notes.id, id));
555
+ },
556
+ }),
557
+ );
558
+ ```
559
+
560
+ </Tab>
561
+ <Tab value="(public)">
562
+
563
+ ```typescript title="flows/main/(public)/health.ts"
564
+ export const health = on(
565
+ http.get().public(),
566
+ flow({ out: z.object({ ok: z.literal(true) }), do: () => ({ ok: true as const }) }),
567
+ );
568
+ // still GET /health named main.health
569
+ ```
570
+
571
+ </Tab>
572
+ </Tabs>
573
+
574
+ ## Private files
575
+
576
+ Prefix a file or folder with `_` to opt it — and every nested file — out of routing.
577
+
578
+ | On disk | URL | Notes |
579
+ | -------------------------------- | --- | ------------------------------------------------------- |
580
+ | `notes/_lib.ts` | — | Ignored; siblings may `import { helper } from "./_lib"` |
581
+ | `notes/_lib/query.ts` | — | The whole folder is skipped |
582
+ | `_shared/` as a first-level unit | — | Not a client unit |
583
+
584
+ `_` means private implementation. It will not appear in `generated.ts`. If you want it registered, do not prefix `_`.
585
+
586
+ <Tabs items={["_lib.ts", "_lib/", "import"]}>
587
+ <Tab value="_lib.ts">
588
+
589
+ ```typescript title="flows/notes/_lib.ts"
590
+ export function titleOf(row: { title: string }): string {
591
+ return String(row.title);
592
+ }
593
+ ```
594
+
595
+ </Tab>
596
+ <Tab value="_lib/">
597
+
598
+ ```typescript title="flows/notes/_lib/title.ts"
599
+ export function titleOf(row: { title: string }): string {
600
+ return String(row.title);
601
+ }
602
+ ```
603
+
604
+ </Tab>
605
+ <Tab value="import">
606
+
607
+ ```typescript title="flows/notes/[id]/get.ts"
608
+ import { titleOf } from "../_lib";
609
+ import { titleOf as fromFolder } from "../_lib/title";
610
+
611
+ export const get = on(
612
+ http.get().public(),
613
+ flow({
614
+ in: NoteIdIn,
615
+ do: async ({ id }, fx) => {
616
+ const row = await fx.store(db).findById(notes, id);
617
+ return { id, title: titleOf(row!) ?? fromFolder(row!) };
618
+ },
619
+ }),
620
+ );
621
+ ```
622
+
623
+ </Tab>
624
+ </Tabs>
625
+
626
+ ## `main` is the only first-level exception
627
+
628
+ `main` is the starter's root unit (`main.health`, `main.root`). Every other first-level folder is both the client unit **and** the first URL segment (`notes` → `/notes`).
629
+
630
+ If `main` followed that law, health would be `GET /main/health` and the app root `GET /main`. The exception exists so `main/health.ts` is `GET /health` and `main/route.ts` is `GET /`, while the Flow name stays `main.health` / `main.root`.
631
+
632
+ Do not put `index.ts` next to `health.ts` — that mix is a generate error. Use `route.ts` for `/`. There is no `flows/index.ts` as the app root.
633
+
634
+ <Tabs items={["health.ts", "route.ts"]}>
635
+ <Tab value="health.ts">
636
+
637
+ ```typescript title="flows/main/health.ts"
638
+ export const health = on(
639
+ http.get().public(),
640
+ flow({
641
+ out: z.object({ ok: z.literal(true) }),
642
+ do: () => ({ ok: true as const }),
643
+ }),
644
+ );
645
+ // GET /health name main.health
646
+ ```
647
+
648
+ </Tab>
649
+ <Tab value="route.ts">
650
+
651
+ ```typescript title="flows/main/route.ts"
652
+ export const root = on(
653
+ http.get().public(),
654
+ flow({
655
+ out: z.object({ ok: z.literal(true), app: z.string() }),
656
+ do: () => ({ ok: true as const, app: "notes" }),
657
+ }),
658
+ );
659
+ // GET / name main.root
660
+ ```
661
+
662
+ </Tab>
663
+ </Tabs>
664
+
665
+ ## QUERY is a method, not a reserved leaf
666
+
667
+ Reserved leaves are **resource op names** (`list` `create` `get` `update` `remove`) — the same five as `http.resource` — plus `index` / `route`. They are not HTTP verbs.
668
+
669
+ `http.query()` is a verb (RFC 10008), like GET/POST. Put it on `list.ts` / `get.ts` / `route.ts`. A file named `query.ts` is an **action leaf** and adds `/query`. Not a router collision with GET `list` / `get`.
670
+
671
+ <Tabs items={["http.query()", "query.ts"]}>
672
+ <Tab value="http.query()">
673
+
674
+ ```typescript title="flows/notes/list.ts"
675
+ export const list = on(
676
+ http.query().public(),
677
+ flow({ out: NoteListOut, do: async (input, fx) => fx.json.withQuery(rows, input) }),
678
+ );
679
+ // QUERY /notes
680
+ ```
681
+
682
+ </Tab>
683
+ <Tab value="query.ts">
684
+
685
+ ```typescript title="flows/notes/query.ts"
686
+ export const query = on(http.get().public(), flow({ do: async () => ({}) }));
687
+ // GET /notes/query — action leaf, not a reserved op
688
+ ```
689
+
690
+ </Tab>
691
+ </Tabs>
692
+
693
+ ## Escape hatch
694
+
695
+ `http.get("/me/tasks")` and `flow("notes.get")` still win when the folder is not the URL. If `flow("tasks.get")` lives under `flows/notes/`, generate fails — drift, not a silent rename.
696
+
697
+ Plugin `.binding()` is never inferred. File tree is app flows only.
698
+
699
+ <Tabs items={["Explicit path", "Explicit name", "Drift"]}>
700
+ <Tab value="Explicit path">
701
+
702
+ ```typescript title="flows/my/index.ts"
703
+ export const tasks = on(
704
+ http.get("/me/tasks").gate(member).live(),
705
+ flow("my.tasks", {
706
+ do: async (_input, fx) => fx.store(db).select().from(tasks),
707
+ }),
708
+ );
709
+ ```
710
+
711
+ </Tab>
712
+ <Tab value="Explicit name">
713
+
714
+ ```typescript title="flows/notes/[id]/get.ts"
715
+ export const get = on(
716
+ http.get("/notes/:id").public(),
717
+ flow("notes.get", {
718
+ in: NoteIdIn,
719
+ do: async ({ id }, fx) => fx.store(db).findById(notes, id),
720
+ }),
721
+ );
722
+ ```
723
+
724
+ </Tab>
725
+ <Tab value="Drift">
726
+
727
+ ```typescript title="flows/notes/get.ts"
728
+ export const get = on(http.get().public(), flow("tasks.get", { do: async () => ({}) }));
729
+ // generate error — unit prefix tasks ≠ folder notes
730
+ ```
731
+
732
+ </Tab>
733
+ </Tabs>
734
+
735
+ ## Organizing the tree
736
+
737
+ The framework is opinionated about **where a unit lives**, not about how you split helpers inside it.
738
+
739
+ ### Barrel or tree — per unit
740
+
741
+ | Shape | When | Emit |
742
+ | ------ | ------------------------------------------------- | ------------------------------------------- |
743
+ | Barrel | The unit folder has only `index.ts` (+ skip-list) | `import * as notes from "./notes/index.ts"` |
744
+ | Tree | `[id]/`, `(group)/`, or extra route files | Synthesized namespace + path/name stamps |
745
+
746
+ Mixed barrel `index.ts` plus tree files is a generate error. `index` in a tree is also a reserved leaf (`/notes`), so barrel vs leaf would be ambiguous.
747
+
748
+ Keel-style paths that are not the folder (`/me/tasks`) stay on a barrel with an explicit `http.get("/me/tasks")`.
749
+
750
+ ```typescript title="flows/my/index.ts"
751
+ export const tasks = on(
752
+ http.get("/me/tasks").gate(member),
753
+ flow("my.tasks", { do: async () => [] }),
754
+ );
755
+ ```
756
+
757
+ ### Decls stay in core
758
+
759
+ Stores, gates, vault contracts, channel templates, clocks, and AI models belong in `core.ts` (or `core/*.ts`). Flows import them. That is what `import "@/core"` loads before `oke()`.
760
+
761
+ ```typescript title="flows/notes/create.ts"
762
+ import { db, notesMutate, webhookSecret } from "@/core";
763
+ ```
764
+
765
+ ### Colocate contracts next to the unit
766
+
767
+ `shapes.ts` / `signals.ts` / `_lib/` sit beside the walked files and are not routes. A signal or clock flow in the tree gets a name, never a URL.
768
+
769
+ ```typescript title="flows/notes/create.ts"
770
+ import { NoteCreateIn, NoteOut } from "./shapes";
771
+ import { noteCreated } from "./signals";
772
+ ```
773
+
774
+ ### `http.resource` in a tree
775
+
776
+ Five reserved leaves reproduce `GET|POST /notes` and `GET|PATCH|DELETE /notes/:id`, but not one shared `.gate()` / `.live()` and not `store.resource().all()` in a single declaration.
777
+
778
+ Put `on(http.resource("/notes", ops).public().live())` in `route.ts`. Inference is skipped — the five verbs already declare those paths. Extra actions (`[id]/archive.ts`) sit beside it. Pathless `http.resource()` is not a thing.
779
+
780
+ | Verb | Path |
781
+ | -------- | ------------------- |
782
+ | `list` | `GET /notes` |
783
+ | `create` | `POST /notes` |
784
+ | `get` | `GET /notes/:id` |
785
+ | `update` | `PATCH /notes/:id` |
786
+ | `remove` | `DELETE /notes/:id` |
787
+
788
+ <Tabs items={["route.ts", "archive.ts"]}>
789
+ <Tab value="route.ts">
790
+
791
+ ```typescript title="flows/notes/route.ts"
792
+ const notesR = store.resource(db, notes, { in: NewNote, out: Note });
793
+ export const mounted = on(http.resource("/notes", notesR.all()).public().live());
794
+ ```
795
+
796
+ </Tab>
797
+ <Tab value="archive.ts">
798
+
799
+ ```typescript title="flows/notes/[id]/archive.ts"
800
+ export const archive = on(
801
+ http.post().gate(notesMutate),
802
+ flow({ in: NoteIdIn, do: async ({ id }, fx) => /* POST /notes/:id/archive beside the mount */ }),
803
+ );
804
+ ```
805
+
806
+ </Tab>
807
+ </Tabs>
808
+
809
+ ### Match order
810
+
811
+ The default router tries a **static** segment, then `:param`, then `*`. File name order does not decide the winner: `archive.ts` (`/notes/archive`) is not eaten by `[slug]/get.ts` (`/notes/:slug`).
812
+
813
+ <Tabs items={["static", ":slug"]}>
814
+ <Tab value="static">
815
+
816
+ ```typescript title="flows/notes/archive.ts"
817
+ export const archive = on(http.post().gate(notesMutate), flow({ do: async () => ({}) }));
818
+ // POST /notes/archive — static wins
819
+ ```
820
+
821
+ </Tab>
822
+ <Tab value=":slug">
823
+
824
+ ```typescript title="flows/notes/[slug]/get.ts"
825
+ export const bySlug = on(
826
+ http.get().public(),
827
+ flow({ in: z.object({ slug: z.string() }), do: async ({ slug }) => ({ slug }) }),
828
+ );
829
+ // GET /notes/:slug — does not match /notes/archive
830
+ ```
831
+
832
+ </Tab>
833
+ </Tabs>
834
+
835
+ ## Troubleshooting
836
+
837
+ <Accordions>
838
+
839
+ <Accordion title="Mixed index.ts + [id]/">
840
+
841
+ Generate fails when a unit `index.ts` (the barrel) sits next to tree files. Use only the barrel, or move the collection path to `route.ts` and keep `[id]/` beside it.
842
+
843
+ <Tabs items={["route.ts", "[id]/get.ts"]}>
844
+ <Tab value="route.ts">
845
+
846
+ ```typescript title="flows/notes/route.ts"
847
+ export const list = on(http.get().public(), flow({ do: async () => [] }));
848
+ ```
849
+
850
+ </Tab>
851
+ <Tab value="[id]/get.ts">
852
+
853
+ ```typescript title="flows/notes/[id]/get.ts"
854
+ export const get = on(http.get().public(), flow({ in: NoteIdIn, do: async ({ id }) => ({ id }) }));
855
+ ```
856
+
857
+ </Tab>
858
+ </Tabs>
859
+
860
+ </Accordion>
861
+
862
+ <Accordion title="Export collision">
863
+
864
+ Two files in one unit exporting `get` is a generate error. Rename one export. Several Flow exports in one `route.ts` may share the inferred collection path; different methods are OK.
865
+
866
+ </Accordion>
867
+
868
+ <Accordion title="Unresolved http.get()">
869
+
870
+ `http.get()` with no path is a sentinel, not `/`. Import the generated barrel so the stamp runs, or pass an explicit path. Boot fails with **OKE1010** if the sentinel is still there.
871
+
872
+ </Accordion>
873
+
874
+ <Accordion title="Duplicate method + path">
875
+
876
+ Two bindings on the same method and path fail boot (**OKE1011**) — last-add-wins is refused. `list.ts` (`GET /notes`) plus a `GET` in `route.ts` is that collision. `list.ts` + `create.ts` share `/notes` on different methods and are fine.
877
+
878
+ </Accordion>
879
+
880
+ <Accordion title="Stale generated.ts">
881
+
882
+ A unit folder on disk with no adopted flows is **OKE1009**. Run `oke dev` or `oke build`. The watcher regenerates on `flows/**` changes except `generated.ts` itself.
883
+
884
+ </Accordion>
885
+
886
+ <Accordion title='[...slug] client key is "*", not slug'>
887
+
888
+ `restRequest` only substitutes `:name`. The router wildcard param is `*`. Call `api.docs.get({ "*": "a/b/c" })`, not `{ slug: "a/b/c" }`.
889
+
890
+ </Accordion>
891
+
892
+ <Accordion title="http.resource inside a tree unit">
893
+
894
+ Mixed-barrel fires only for a barrel `index.ts` plus tree files — not because the mount exists. Keep the mount in `route.ts`. Shared posture plus `store.resource().all()` is why the helper stays useful next to extra action files.
895
+
896
+ </Accordion>
897
+
898
+ <Accordion title="Unnamed HTTP flow">
899
+
900
+ `flow({ do })` outside a unit is `flow_1` until stamped. Adopted HTTP still unnamed is **OKE1012**. Export from `flows/<unit>/` or pass `flow("unit.export", {…})`.
901
+
902
+ </Accordion>
903
+
904
+ </Accordions>
905
+
906
+ ## Learn more
907
+
908
+ - [Flow](/docs/elements/flow) — one species, `on(trigger, flow)`
909
+ - [Basic Usage](/docs/get-started/basic-usage) — health + notes from the starter
910
+ - [Client](/docs/reference/client) — `createClient<App>` from `$routes`
911
+ - [Configuration](/docs/reference/configuration) — `oke.config.ts`
912
+ - [CLI Reference](/docs/reference/cli) — `oke dev` regenerates the barrel
913
+
914
+ ## Next
915
+
916
+ <Cards>
917
+ <Card
918
+ title="Flow"
919
+ description="The one species of backend behavior."
920
+ href="/docs/elements/flow"
921
+ />
922
+ <Card
923
+ title="Basic Usage"
924
+ description="Scaffold Notes and call health."
925
+ href="/docs/get-started/basic-usage"
926
+ />
927
+ <Card
928
+ title="Console"
929
+ description="Panels derived from the Manifest."
930
+ href="/docs/console/overview"
931
+ />
932
+ </Cards>