okengine 0.19.9 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/AGENTS.md +1 -1
  2. package/package.json +1 -1
  3. package/site/content/docs/client/auth.mdx +1 -2
  4. package/site/content/docs/client/calling.mdx +8 -8
  5. package/site/content/docs/client/index.mdx +4 -4
  6. package/site/content/docs/client/react.mdx +5 -0
  7. package/site/content/docs/elements/channel/email.mdx +25 -36
  8. package/site/content/docs/elements/channel/index.mdx +88 -46
  9. package/site/content/docs/elements/channel/push.mdx +7 -9
  10. package/site/content/docs/elements/channel/sms.mdx +6 -4
  11. package/site/content/docs/elements/channel/whatsapp.mdx +11 -9
  12. package/site/content/docs/elements/clock/index.mdx +14 -25
  13. package/site/content/docs/elements/flow/http.mdx +24 -8
  14. package/site/content/docs/elements/flow/index.mdx +15 -12
  15. package/site/content/docs/elements/flow/routing.mdx +166 -122
  16. package/site/content/docs/elements/gate/rls.mdx +2 -2
  17. package/site/content/docs/elements/store/index.mdx +11 -3
  18. package/site/content/docs/elements/store/search.mdx +5 -5
  19. package/site/content/docs/elements/store/sql.mdx +91 -33
  20. package/site/content/docs/elements/vault/index.mdx +3 -5
  21. package/site/content/docs/plugins/magic-link.mdx +4 -3
  22. package/site/content/docs/plugins/otp.mdx +4 -3
  23. package/site/content/docs/plugins/two-factor.mdx +4 -0
  24. package/site/content/docs/providers/index.mdx +1 -1
  25. package/site/content/docs/recipes/index.mdx +1 -1
  26. package/site/content/docs/reference/cli.mdx +7 -4
  27. package/site/content/docs/reference/configuration.mdx +7 -7
  28. package/site/content/docs/reference/errors.mdx +30 -30
  29. package/site/content/docs/reference/fx.mdx +16 -8
  30. package/site/content/docs/reference/i18n.mdx +4 -4
  31. package/site/content/docs/reference/plugins.mdx +4 -4
  32. package/site/content/docs/understand/try-it.mdx +2 -0
  33. package/src/cli/ai-setup/ai-setup.test.ts +40 -0
  34. package/src/cli/ai-setup/apply.ts +28 -46
  35. package/src/cli/build.test.ts +3 -3
  36. package/src/cli/build.ts +5 -5
  37. package/src/cli/db-auto-push.test.ts +11 -0
  38. package/src/cli/db-auto-push.ts +6 -2
  39. package/src/cli/db.test.ts +1 -1
  40. package/src/cli/db.ts +6 -6
  41. package/src/cli/dev-db-push.test.ts +6 -2
  42. package/src/cli/dev-schema-sync.ts +1 -1
  43. package/src/cli/dev.test.ts +10 -7
  44. package/src/cli/dev.ts +13 -11
  45. package/src/cli/ensure-drizzle-config.ts +4 -3
  46. package/src/client-react/browser.test.ts +23 -0
  47. package/src/client-react/use-live-query.ts +1 -1
  48. package/src/compiler/flow-path.test.ts +1 -0
  49. package/src/compiler/flow-path.ts +1 -1
  50. package/src/compiler/generate-adopt.test.ts +55 -1
  51. package/src/compiler/generate-adopt.ts +111 -21
  52. package/src/config/index.ts +6 -4
  53. package/src/console/ui-next/dist/assets/{access-page-BoC83Ubl.js → access-page-DFLu0wTA.js} +1 -1
  54. package/src/console/ui-next/dist/assets/{agent-disclosure-CKjAEOqA.js → agent-disclosure-DGscxaF5.js} +1 -1
  55. package/src/console/ui-next/dist/assets/{cache-glyph-Ceaq9pYh.js → cache-glyph-BGmRZk7d.js} +1 -1
  56. package/src/console/ui-next/dist/assets/{call-pii-button-C4lmY7ck.js → call-pii-button--feUYxvG.js} +1 -1
  57. package/src/console/ui-next/dist/assets/{collapsible-82y257sL.js → collapsible-JWvpaiGY.js} +1 -1
  58. package/src/console/ui-next/dist/assets/{duration-tone-W63jaMZ8.js → duration-tone-D9yCJG4n.js} +1 -1
  59. package/src/console/ui-next/dist/assets/{flows-page-KFTFj2rK.js → flows-page-Bs6MD9GB.js} +1 -1
  60. package/src/console/ui-next/dist/assets/{highlighted-json-yE9zNTqC.js → highlighted-json-xH8MrEnv.js} +1 -1
  61. package/src/console/ui-next/dist/assets/{http-method-CwFeFroN.js → http-method-C4vB6ZIw.js} +1 -1
  62. package/src/console/ui-next/dist/assets/{index-r7xXt_VV.js → index-yTCY4AcS.js} +3 -3
  63. package/src/console/ui-next/dist/assets/{observability-page-BDiliMiC.js → observability-page-BxJ3R6dU.js} +1 -1
  64. package/src/console/ui-next/dist/assets/{replica-lag-DYDzWUFT.js → replica-lag-QRKB_IE8.js} +1 -1
  65. package/src/console/ui-next/dist/assets/{request-meta-CzrOfgiz.js → request-meta-DqZ-fMu5.js} +1 -1
  66. package/src/console/ui-next/dist/assets/{store-page-CZC2cwaw.js → store-page-Dixb6L7a.js} +1 -1
  67. package/src/console/ui-next/dist/assets/{trace-detail-sheet-DgeeejW7.js → trace-detail-sheet-CazhjtiU.js} +1 -1
  68. package/src/console/ui-next/dist/assets/{tree-expand-toggle-CzGIyOPY.js → tree-expand-toggle-DlnqYKfr.js} +1 -1
  69. package/src/console/ui-next/dist/assets/{units-page-DBiDCLIB.js → units-page-BXTLjU2-.js} +1 -1
  70. package/src/console/ui-next/dist/assets/{vault-page-CcHsthPe.js → vault-page-39KR__bc.js} +1 -1
  71. package/src/console/ui-next/dist/index.html +1 -1
  72. package/src/drivers/clock-postgres.test.ts +10 -2
  73. package/src/drivers/clock-postgres.ts +18 -2
  74. package/src/drivers/vault-driver-removal.test.ts +2 -2
  75. package/src/elements/channel/declare.ts +66 -3
  76. package/src/elements/channel/runtime.ts +9 -11
  77. package/src/elements/channel.test.ts +42 -0
  78. package/src/elements/channel.ts +4 -2
  79. package/src/elements/clock/reconcile.ts +45 -24
  80. package/src/elements/clock.test.ts +33 -0
  81. package/src/elements/store/emit-drizzle.ts +285 -65
  82. package/src/elements/store/load-plugin-tables.ts +1 -1
  83. package/src/elements/store/prepare-row.test.ts +57 -4
  84. package/src/elements/store/schema-decl.test.ts +178 -0
  85. package/src/elements/store/sql-session.ts +44 -2
  86. package/src/elements/store/table.ts +8 -6
  87. package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
  88. package/src/kernel/app.ts +26 -32
  89. package/src/kernel/auto-registry.test.ts +26 -1
  90. package/src/kernel/boot.ts +2 -2
  91. package/src/kernel/boundary-contract.ts +6 -1
  92. package/src/kernel/errors.ts +3 -3
  93. package/src/kernel/flow-units.ts +3 -3
  94. package/src/kernel/fx.ts +12 -2
  95. package/src/kernel/mutation-id.ts +8 -0
  96. package/src/kernel/plugin.ts +4 -3
  97. package/src/kernel/project-out.test.ts +176 -0
  98. package/src/kernel/project-out.ts +91 -0
  99. package/src/kernel/realtime-bind.ts +2 -3
  100. package/src/kernel/router/linear.ts +12 -6
  101. package/src/kernel/router.test.ts +13 -0
  102. package/src/plugins/magic-link.ts +25 -24
  103. package/src/plugins/otp.ts +35 -24
  104. package/src/plugins/two-factor.ts +15 -0
  105. package/src/runs/duckdb.test.ts +2 -2
@@ -6,8 +6,8 @@ source: "docs/spec/unified-theory.md"
6
6
  ---
7
7
 
8
8
  HTTP routes are inferred from where a Flow file lives. Put `http.get()` in
9
- `src/flows/notes/[id]/get.ts` and the compiler stamps `GET /notes/:id`, names the
10
- Flow `notes.get`, and the client calls `api.notes.get({ id })`.
9
+ `src/flows/links/[code]/get.ts` and the compiler stamps `GET /links/:code`, names the
10
+ Flow `links.get`, and the client calls `api.links.get({ code })`.
11
11
 
12
12
  <Callout title="The one rule">
13
13
  On a tree file, omit path and name: `on(http.get(), flow({ do }))`. The file
@@ -22,8 +22,9 @@ Flow `notes.get`, and the client calls `api.notes.get({ id })`.
22
22
  | **HTTP path** | Tree file under `src/flows/<unit>/` — `http.get()`, `http.post()`, … | Barrel `index.ts`; a public URL that must not match the folder; `http.resource(path, ops)`; custom live SSE path |
23
23
  | **Flow name** | Same tree file — `flow({ do })` stamps `unit.export` | Barrel (optional — export still stamps); stable name across moves; call-only Flow you `fx.call` by name |
24
24
 
25
- **Consequence:** most app code looks like the create-oke template —
26
- `http.get().public()` + `flow({ … })` — with no string path and no string name.
25
+ **Consequence:** create-oke `shorter` is this tree — `http.get()` + `flow({ … })` with no
26
+ string path and no string name, except where the public URL must not follow the folder
27
+ (`GET /:code`).
27
28
 
28
29
  ## Smallest Example
29
30
 
@@ -32,14 +33,15 @@ Flow `notes.get`, and the client calls `api.notes.get({ id })`.
32
33
  <Step>
33
34
  ### Place the file in the tree
34
35
 
35
- `oke dev` / `oke build` regenerate `src/flows/generated.ts`. Import it before
36
+ `oke dev` / `oke build` regenerate `src/flows/index.ts`. Import it before
36
37
  `oke()` so pathless triggers receive their stamp:
37
38
 
38
39
  ```typescript title="src/app.ts"
39
- import "@/flows/generated";
40
- import { oke } from "okengine";
40
+ import "@/core";
41
+ import "@/flows";
42
+ import { oke } from "okengine/http";
41
43
 
42
- export const app = oke({ name: "notes" });
44
+ export const app = oke({ name: "app" });
43
45
  ```
44
46
 
45
47
  </Step>
@@ -47,20 +49,20 @@ export const app = oke({ name: "notes" });
47
49
  <Step>
48
50
  ### Use pathless `http.get()`
49
51
 
50
- The tree stamps `GET /notes/:id`. `:id` merges into `in`. The export name is the
51
- client method (`api.notes.get`):
52
+ The tree stamps `GET /links/:code`. `:code` merges into `in`. The export name is the
53
+ client method (`api.links.get`):
52
54
 
53
- ```typescript title="src/flows/notes/[id]/get.ts"
55
+ ```typescript title="src/flows/links/[code]/get.ts"
54
56
  import { on, flow, http } from "okengine";
55
57
  import { z } from "zod";
56
58
 
57
59
  export const get = on(
58
60
  http.get({
59
- in: z.object({ id: z.string() }),
60
- out: z.object({ id: z.string() }),
61
+ in: z.object({ code: z.string() }),
62
+ out: z.object({ code: z.string(), url: z.string() }),
61
63
  }),
62
64
  flow({
63
- do: async ({ id }) => ({ id }),
65
+ do: async ({ code }) => ({ code, url: "https://example.com" }),
64
66
  }),
65
67
  );
66
68
  ```
@@ -71,14 +73,14 @@ export const get = on(
71
73
  ### Call the endpoint
72
74
 
73
75
  ```bash
74
- curl -X GET http://localhost:6530/notes/n_1 -H "accept: application/json"
76
+ curl -X GET http://localhost:6530/links/ok -H "accept: application/json"
75
77
  ```
76
78
 
77
79
  Response:
78
80
 
79
81
  ```json
80
82
  {
81
- "data": { "id": "n_1" },
83
+ "data": { "code": "ok", "url": "https://example.com" },
82
84
  "error": null
83
85
  }
84
86
  ```
@@ -95,16 +97,17 @@ Response:
95
97
 
96
98
  ## Progressive Patterns
97
99
 
98
- From a pathless tree file to an explicit barrel, an action leaf, and a catch-all:
100
+ From a pathless tree file to an explicit barrel, an action leaf, and a public URL that must
101
+ not follow the folder:
99
102
 
100
- <Tabs items={["Tree", "Barrel", "Action", "Catch-all"]}>
103
+ <Tabs items={["Tree", "Barrel", "Action", "Override"]}>
101
104
 
102
105
  <Tab value="Tree">
103
106
 
104
107
  One file per route. Skip the path argument. The generated barrel calls
105
108
  `stampHttpPath` / `stampFlowName` after import:
106
109
 
107
- ```typescript title="src/flows/notes/list.ts"
110
+ ```typescript title="src/flows/links/list.ts"
108
111
  import { on, flow, http } from "okengine";
109
112
 
110
113
  export const list = on(
@@ -115,7 +118,7 @@ export const list = on(
115
118
  );
116
119
  ```
117
120
 
118
- Stamped to `GET /notes`, Flow `notes.list`, client `api.notes.list()`.
121
+ Stamped to `GET /links`, Flow `links.list`, client `api.links.list()`.
119
122
 
120
123
  </Tab>
121
124
 
@@ -124,21 +127,21 @@ Stamped to `GET /notes`, Flow `notes.list`, client `api.notes.list()`.
124
127
  A unit that is **only** `index.ts` (plus skip-list files) is a barrel. The
125
128
  generated file re-exports it **without** stamping — pass explicit paths:
126
129
 
127
- ```typescript title="src/flows/notes/index.ts"
130
+ ```typescript title="src/flows/links/index.ts"
128
131
  import { on, flow, http } from "okengine";
129
132
  import { z } from "zod";
130
133
 
131
134
  export const list = on(
132
- http.get("/notes").public(),
133
- flow("notes.list", {
135
+ http.get("/links").public(),
136
+ flow("links.list", {
134
137
  do: () => [],
135
138
  }),
136
139
  );
137
140
 
138
141
  export const create = on(
139
- http.post("/notes", { in: z.object({ title: z.string().min(1) }) }),
140
- flow("notes.create", {
141
- do: async ({ title }, fx) => ({ id: fx.id(), title }),
142
+ http.post("/links", { in: z.object({ url: z.string().url() }) }),
143
+ flow("links.create", {
144
+ do: async ({ url }, fx) => ({ id: fx.id(), url }),
142
145
  }),
143
146
  );
144
147
  ```
@@ -151,25 +154,48 @@ Pathless `http.get()` inside a barrel stays unresolved and fails boot
151
154
  <Tab value="Action">
152
155
 
153
156
  A leaf that is not reserved **adds** a segment. `archive.ts` is
154
- `POST /notes/:id/archive`, not `POST /notes/:id`:
157
+ `POST /links/:code/archive`, not `POST /links/:code`:
155
158
 
156
- ```typescript title="src/flows/notes/[id]/archive.ts"
159
+ ```typescript title="src/flows/links/[code]/archive.ts"
157
160
  import { on, flow, http } from "okengine";
158
161
  import { z } from "zod";
159
162
 
160
163
  export const archive = on(
161
- http.post({ in: z.object({ id: z.string() }) }),
164
+ http.post({ in: z.object({ code: z.string() }) }),
162
165
  flow({
163
- do: async ({ id }) => ({ id, archived: true }),
166
+ do: async ({ code }) => ({ code, archived: true }),
164
167
  }),
165
168
  );
166
169
  ```
167
170
 
168
171
  </Tab>
169
172
 
170
- <Tab value="Catch-all">
173
+ <Tab value="Override">
171
174
 
172
- `[...slug]` becomes `*` on the URL. The request param is always `"*"`, never
175
+ Pass the path when the folder would stamp the wrong URL. `redirect.ts` would be
176
+ `GET /links/redirect`. Short URLs must be root-level:
177
+
178
+ ```typescript title="src/flows/links/redirect.ts"
179
+ import { on, flow, http } from "okengine";
180
+ import { z } from "zod";
181
+
182
+ export const redirect = on(
183
+ http.get("/:code", { in: z.object({ code: z.string() }) }).public(),
184
+ flow({
185
+ do: async ({ code }) =>
186
+ new Response(null, { status: 302, headers: { Location: `https://example.com/${code}` } }),
187
+ }),
188
+ );
189
+ ```
190
+
191
+ The tree never overwrites an explicit path. Static `/health` · `/links` · `/auth`
192
+ still win over `/:code`.
193
+
194
+ </Tab>
195
+
196
+ </Tabs>
197
+
198
+ Catch-all `[...slug]` becomes `*` on the URL. The request param is always `"*"`, never
173
199
  `slug`:
174
200
 
175
201
  ```typescript title="src/flows/docs/[...slug]/get.ts"
@@ -192,56 +218,59 @@ curl -X GET http://localhost:6530/docs/getting-started/install \
192
218
  `do` receives `{ "*": "getting-started/install" }`. Call
193
219
  `api.docs.get({ "*": "a/b/c" })` — not `{ slug }`.
194
220
 
195
- </Tab>
196
-
197
- </Tabs>
198
-
199
221
  ## Convention Reference
200
222
 
201
- | Convention | File | Stamped route | Client |
202
- | ------------- | --------------------------------------- | ------------------------- | ------------------------------ |
203
- | Dynamic param | `notes/[id]/get.ts` + `http.get()` | `GET /notes/:id` | `api.notes.get({ id })` |
204
- | Reserved leaf | `notes/list.ts` + `http.get()` | `GET /notes` | `api.notes.list()` |
205
- | Action leaf | `notes/[id]/archive.ts` + `http.post()` | `POST /notes/:id/archive` | `api.notes.archive({ id })` |
206
- | Catch-all | `docs/[...slug]/get.ts` + `http.get()` | `GET /docs/*` | `api.docs.get({ "*": "a/b" })` |
207
- | Route group | `notes/(ops)/archive.ts` | `/notes/archive` | `api.notes.archive` |
208
- | Root unit | `main/health.ts` + `http.get()` | `GET /health` | `api.main.health()` |
209
- | Folder root | `main/route.ts` + `http.get()` | `GET /` | `api.main.root()` |
223
+ | Convention | File | Stamped route | Client |
224
+ | ------------- | ------------------------------------------ | --------------------------- | ------------------------------ |
225
+ | Dynamic param | `links/[code]/get.ts` + `http.get()` | `GET /links/:code` | `api.links.get({ code })` |
226
+ | Reserved leaf | `links/list.ts` + `http.get()` | `GET /links` | `api.links.list()` |
227
+ | Action leaf | `links/[code]/archive.ts` + `http.post()` | `POST /links/:code/archive` | `api.links.archive({ code })` |
228
+ | Explicit path | `links/redirect.ts` + `http.get("/:code")` | `GET /:code` | `api.links.redirect({ code })` |
229
+ | Catch-all | `docs/[...slug]/get.ts` + `http.get()` | `GET /docs/*` | `api.docs.get({ "*": "a/b" })` |
230
+ | Route group | `links/(ops)/archive.ts` | `/links/archive` | `api.links.archive` |
231
+ | Root unit | `main/health.ts` + `http.get()` | `GET /health` | `api.main.health()` |
232
+ | Folder root | `main/route.ts` + `http.get()` | `GET /` | `api.main.root()` |
210
233
 
211
234
  ## Units
212
235
 
213
236
  The first folder under `src/flows/` is the **client unit**. Nested folders and
214
237
  the leaf file build the URL. The `export const` name is the method on that unit.
215
238
 
239
+ create-oke `-t shorter` ships this tree (`-t blank` is `main/` only):
240
+
216
241
  ```text
217
242
  src/flows/
218
- ├── notes/ # Unit: notes → api.notes.*
219
- │ ├── list.ts # GET /notes (reserved leaf)
220
- │ ├── create.ts # POST /notes (reserved leaf)
221
- │ ├── shapes.ts # skipped (not a route)
222
- │ └── [id]/
223
- │ ├── get.ts # GET /notes/:id
224
- │ └── archive.ts # POST /notes/:id/archive
225
- ├── billing/
226
- │ └── (checkout)/ # omitted from the URL
227
- │ └── charge.ts # POST /billing/charge (if http.post())
243
+ ├── links/ # Unit: links → api.links.*
244
+ │ ├── list.ts # GET /links (reserved leaf)
245
+ │ ├── create.ts # POST /links (reserved leaf)
246
+ │ ├── redirect.ts # explicit GET /:code
247
+ │ ├── expire.ts # Clock — not HTTP
248
+ │ ├── reach.ts # Clock — not HTTP
249
+ │ ├── shapes.ts # skipped (contracts)
250
+ │ ├── signals.ts # skipped as a route; on() still joins the unit
251
+ │ ├── _shared.ts # skipped (_ prefix)
252
+ │ └── [code]/
253
+ │ ├── get.ts # GET /links/:code
254
+ │ ├── archive.ts # POST /links/:code/archive
255
+ │ └── report.ts # GET /links/:code/report
228
256
  └── main/ # Unit: main — prefix omitted from the URL
229
257
  ├── health.ts # GET /health
230
- └── route.ts # GET /
258
+ ├── route.ts # GET /
259
+ └── shapes.ts
231
260
  ```
232
261
 
233
262
  A file sitting directly in `src/flows/` (no unit folder) is not a route.
234
263
 
235
- Unit folder names must be valid JS identifiers (`notes`, `main`, `_` allowed
236
- inside; `my-notes` is skipped). Folders starting with `_`, `[`, or `(` are not
264
+ Unit folder names must be valid JS identifiers (`links`, `main`, `_` allowed
265
+ inside; `my-links` is skipped). Folders starting with `_`, `[`, or `(` are not
237
266
  units.
238
267
 
239
- **Consequence:** `export const getNote` from `get.ts` is `api.notes.getNote`, not
240
- `api.notes.get`. Match the export to the name you want on the client.
268
+ **Consequence:** `export const getLink` from `get.ts` is `api.links.getLink`, not
269
+ `api.links.get`. Match the export to the name you want on the client.
241
270
 
242
271
  ## Path Conventions
243
272
 
244
- **Default — pathless.** Omit the path so `generated.ts` stamps the URL from disk:
273
+ **Default — pathless.** Omit the path so `src/flows/index.ts` stamps the URL from disk:
245
274
 
246
275
  ```typescript
247
276
  http.get(); // pending until stampHttpPath runs
@@ -252,7 +281,7 @@ own the route (barrel, public API shape, resource mount). The tree never
252
281
  overwrites an explicit path:
253
282
 
254
283
  ```typescript
255
- http.get("/organizations/:orgId/members/:memberId");
284
+ http.get("/:code");
256
285
  ```
257
286
 
258
287
  Path params, query string, and JSON body still merge into one object checked by
@@ -260,7 +289,7 @@ Path params, query string, and JSON body still merge into one object checked by
260
289
 
261
290
  ### Dynamic parameters
262
291
 
263
- `[id]` → `:id`. The folder name is the param key. Declare the same key on `in`:
292
+ `[code]` → `:code`. The folder name is the param key. Declare the same key on `in`:
264
293
 
265
294
  ```typescript title="src/flows/orgs/[orgId]/members/[memberId]/get.ts"
266
295
  import { on, flow, http } from "okengine";
@@ -290,10 +319,10 @@ A folder `(ops)` is omitted from the URL. Use it to group files without adding a
290
319
  segment:
291
320
 
292
321
  ```text
293
- src/flows/notes/(ops)/archive.ts → /notes/archive
322
+ src/flows/links/(ops)/archive.ts → /links/archive
294
323
  ```
295
324
 
296
- `(ops)` never enters the Flow name either (`notes.archive`).
325
+ `(ops)` never enters the Flow name either (`links.archive`).
297
326
 
298
327
  ### The `main` unit
299
328
 
@@ -307,35 +336,39 @@ src/flows/notes/(ops)/archive.ts → /notes/archive
307
336
 
308
337
  ### Skip list
309
338
 
310
- These files are never routes (walk skips them; path inference returns nothing):
339
+ These files are never HTTP routes (path inference returns nothing). Walk skips
340
+ them except `signals.ts`, which still contributes `on()` consumers:
311
341
 
312
- | Pattern | Why |
313
- | --------------------------- | ------------------------------------------------ |
314
- | `generated.ts` | Adopt barrel (`oke dev` / `oke build` writes it) |
315
- | `shapes.ts` | Shared Zod / contracts |
316
- | `signals.ts` | Signal declarations |
317
- | `*.test.ts` / `*.test.tsx` | Tests |
318
- | `_` prefix (file or folder) | Private helpers (`notes/_lib/util.ts`) |
342
+ | Pattern | Why |
343
+ | --------------------------- | --------------------------------------------------------- |
344
+ | `src/flows/index.ts` | Adopt barrel (`oke dev` / `oke build` writes it) |
345
+ | `shapes.ts` | Shared Zod / contracts |
346
+ | `signals.ts` | Declarations. `on()` in the same file still join the unit |
347
+ | `*.test.ts` / `*.test.tsx` | Tests |
348
+ | `_` prefix (file or folder) | Private helpers (`links/_shared.ts`) |
319
349
 
320
350
  Skip-list files may sit next to tree routes. They do **not** turn a tree into a
321
- barrel.
351
+ barrel. Clock files (`expire.ts`, `reach.ts`) still join the unit — they are not
352
+ HTTP unless they bind `http.*`.
322
353
 
323
354
  ## Reserved Leaves
324
355
 
325
- To avoid `/notes/get` and `/orders/list`, these filenames add **no** URL
356
+ To avoid `/links/get` and `/orders/list`, these filenames add **no** URL
326
357
  segment — the same five CRUD names as `http.resource`, plus folder roots:
327
358
 
328
359
  | Leaf | Typical trigger | Example file | Stamped path |
329
360
  | -------- | --------------- | ----------------------------------- | --------------- |
330
- | `list` | `http.get()` | `notes/list.ts` | `/notes` |
331
- | `create` | `http.post()` | `notes/create.ts` | `/notes` |
332
- | `get` | `http.get()` | `notes/[id]/get.ts` | `/notes/:id` |
333
- | `update` | `http.patch()` | `notes/[id]/update.ts` | `/notes/:id` |
334
- | `remove` | `http.delete()` | `notes/[id]/remove.ts` | `/notes/:id` |
335
- | `index` | _(barrel only)_ | `notes/index.ts` | `/notes` |
336
- | `route` | any | `notes/route.ts` or `main/route.ts` | `/notes` or `/` |
337
-
338
- Any other leaf **is** a segment: `query.ts` → `/notes/query`.
361
+ | `list` | `http.get()` | `links/list.ts` | `/links` |
362
+ | `create` | `http.post()` | `links/create.ts` | `/links` |
363
+ | `get` | `http.get()` | `links/[code]/get.ts` | `/links/:code` |
364
+ | `update` | `http.patch()` | `links/[code]/update.ts` | `/links/:code` |
365
+ | `remove` | `http.delete()` | `links/[code]/remove.ts` | `/links/:code` |
366
+ | `index` | _(barrel only)_ | `links/index.ts` | `/links` |
367
+ | `route` | any | `links/route.ts` or `main/route.ts` | `/links` or `/` |
368
+
369
+ Any other leaf **is** a segment: `query.ts` → `/links/query`. `redirect.ts` with
370
+ pathless `http.get()` would stamp `/links/redirect` — pass `http.get("/:code")`
371
+ instead.
339
372
 
340
373
  In a tree unit, do **not** add `index.ts` beside other route files — that is a
341
374
  generate error. Use `route.ts` (or `list.ts` / `create.ts`) for the collection
@@ -344,7 +377,7 @@ root.
344
377
  ## Barrel vs Tree
345
378
 
346
379
  `oke dev` / `oke build` scans each `src/flows/<unit>/` folder and writes
347
- `generated.ts`. Two shapes, never mixed:
380
+ `src/flows/index.ts`. Two shapes, never mixed:
348
381
 
349
382
  <Tabs items={["Tree", "Barrel", "App entry"]}>
350
383
 
@@ -354,15 +387,15 @@ root.
354
387
  each file and stamps path + name:
355
388
 
356
389
  ```typescript
357
- const notes = {
358
- get: stampHttpPath(stampFlowName(notes_$id$_get.get, "notes.get"), "/notes/:id"),
359
- list: stampHttpPath(stampFlowName(notes_list.list, "notes.list"), "/notes"),
390
+ const links = {
391
+ get: stampHttpPath(stampFlowName(links_$code$_get.get, "links.get"), "/links/:code"),
392
+ list: stampHttpPath(stampFlowName(links_list.list, "links.list"), "/links"),
360
393
  };
361
- export { notes };
362
- registerFlowUnits({ notes });
394
+ export { links };
395
+ registerFlowUnits({ links });
363
396
  ```
364
397
 
365
- `oke()` drains `registerFlowUnits` into `$routes`. `.adopt({ notes })` is
398
+ `oke()` drains `registerFlowUnits` into `$routes`. `.adopt({ links })` is
366
399
  optional and additive.
367
400
 
368
401
  </Tab>
@@ -372,12 +405,12 @@ optional and additive.
372
405
  Only `index.ts` (+ skip-list). Re-export, no stamp:
373
406
 
374
407
  ```typescript
375
- import * as notes from "./notes/index.ts";
376
- export { notes };
377
- registerFlowUnits({ notes });
408
+ import * as links from "./links/index.ts";
409
+ export { links };
410
+ registerFlowUnits({ links });
378
411
  ```
379
412
 
380
- Declare `http.get("/notes")` and `flow("notes.list", {…})` (or rely on adopt to
413
+ Declare `http.get("/links")` and `flow("links.list", {…})` (or rely on adopt to
381
414
  stamp the name from the export). Pathless HTTP fails **OKE1040**.
382
415
 
383
416
  </Tab>
@@ -385,14 +418,15 @@ stamp the name from the export). Pathless HTTP fails **OKE1040**.
385
418
  <Tab value="App entry">
386
419
 
387
420
  ```typescript title="src/app.ts"
388
- import "@/flows/generated";
389
- import { oke } from "okengine";
421
+ import "@/core";
422
+ import "@/flows";
423
+ import { oke } from "okengine/http";
390
424
 
391
- export const app = oke({ name: "notes" });
425
+ export const app = oke({ name: "app" });
392
426
  export type App = typeof app;
393
427
  ```
394
428
 
395
- Do not edit `generated.ts` by hand. Adding a unit folder without regenerating
429
+ Do not edit `src/flows/index.ts` by hand. Adding a unit folder without regenerating
396
430
  leaves a stale barrel — **OKE1030** in prod / `oke dev` with Compose.
397
431
 
398
432
  </Tab>
@@ -402,10 +436,10 @@ leaves a stale barrel — **OKE1030** in prod / `oke dev` with Compose.
402
436
  <Accordions>
403
437
 
404
438
  <Accordion title="Mixed barrel + tree">
405
- `index.ts` plus `[id]/get.ts` (or any other route file) throws at generate:
439
+ `index.ts` plus `[code]/get.ts` (or any other route file) throws at generate:
406
440
 
407
441
  ```text
408
- Unit "notes" mixes a barrel index.ts with tree route files. Use only index.ts (barrel), or move the collection path to route.ts and keep [id]/ beside it.
442
+ Unit "links" mixes a barrel index.ts with tree route files. Use only index.ts (barrel), or move the collection path to route.ts and keep [id]/ beside it.
409
443
  ```
410
444
 
411
445
  Fix: delete `index.ts` and use `list.ts` / `route.ts`, or fold every route into
@@ -417,7 +451,7 @@ Fix: delete `index.ts` and use `list.ts` / `route.ts`, or fold every route into
417
451
  Two files in the same unit cannot share an `export const` name:
418
452
 
419
453
  ```text
420
- Unit "notes" exports "get" from both list.ts and route.ts.
454
+ Unit "links" exports "get" from both list.ts and route.ts.
421
455
  ```
422
456
 
423
457
  Rename one export. The client method is the export name, not the filename.
@@ -425,13 +459,13 @@ Rename one export. The client method is the export name, not the filename.
425
459
  </Accordion>
426
460
 
427
461
  <Accordion title="Unit-prefix drift">
428
- `flow("tasks.get", {…})` living under `src/flows/notes/` throws:
462
+ `flow("tasks.get", {…})` living under `src/flows/links/` throws:
429
463
 
430
464
  ```text
431
- flow("tasks.…") in notes/get.ts does not match the folder "notes".
465
+ flow("tasks.…") in links/get.ts does not match the folder "links".
432
466
  ```
433
467
 
434
- Use `flow("notes.get", {…})`, a nameless `flow({ do })` (stamped `notes.get`
468
+ Use `flow("links.get", {…})`, a nameless `flow({ do })` (stamped `links.get`
435
469
  from the export), or move the file.
436
470
 
437
471
  </Accordion>
@@ -442,18 +476,18 @@ from the export), or move the file.
442
476
 
443
477
  Three names, one file:
444
478
 
445
- | Surface | Source | Example |
446
- | --------- | ------------------------------------------------------------------- | ----------------------- |
447
- | HTTP path | File tree (default) or explicit `http.get("/x")` | `/notes/:id` |
448
- | Flow name | `unit.export` from `flow({ do })` (default), or `flow("notes.get")` | `notes.get` |
449
- | Client | Unit folder + `export const` | `api.notes.get({ id })` |
479
+ | Surface | Source | Example |
480
+ | --------- | ------------------------------------------------------------------- | ------------------------- |
481
+ | HTTP path | File tree (default) or explicit `http.get("/x")` | `/links/:code` |
482
+ | Flow name | `unit.export` from `flow({ do })` (default), or `flow("links.get")` | `links.get` |
483
+ | Client | Unit folder + `export const` | `api.links.get({ code })` |
450
484
 
451
485
  Nameless `flow({ do })` is the tree default — same rule as pathless HTTP. Pass
452
- `flow("notes.get")` only for control (stable name, barrel, or matching unit
486
+ `flow("links.get")` only for control (stable name, barrel, or matching unit
453
487
  prefix). Wrong-unit prefixes fail generate.
454
488
 
455
- Non-HTTP files still join the unit. A signal consumer in `notes/on-created.ts`
456
- is `api.notes.onCreated` over RPC (`POST /_oke/notes/onCreated`), not HTTP.
489
+ Non-HTTP files still join the unit. A signal consumer in `links/signals.ts` is
490
+ `api.links.onCreated` over RPC (`POST /_oke/links/onCreated`), not HTTP.
457
491
 
458
492
  Signal / Clock consumers pass an explicit `flow("…")` name (**OKE1072** if nameless
459
493
  outside `src/flows/<unit>/`; **OKE1070** on collision). Clock may write `clock.every(…)`
@@ -469,11 +503,15 @@ All adopted HTTP bindings go into one matcher. Wrong method on a known path is
469
503
  | `"default"` | yes | Compiled RegExp (O(1) static map + per-bucket regex for `:id`). Falls back to Trie when a path includes `*` |
470
504
  | `"edge"` | | Linear scan, then Trie. No RegExp compile — cold-start / isolates |
471
505
 
506
+ Static paths win over `:param` on the same method (`GET /health` beats
507
+ `GET /:code`), including the edge linear scan. `okengine/http` defaults to
508
+ `"edge"`.
509
+
472
510
  p99 match stays under **1 ms** on the compiled matcher. You do not pick buckets
473
511
  by hand — a catch-all in the table selects Trie for the whole app.
474
512
 
475
513
  Duplicate `METHOD + path` fails boot (**OKE1041**), including a resource mount
476
- plus a handwritten `http.get("/notes")`.
514
+ plus a handwritten `http.get("/links")`.
477
515
 
478
516
  ## Troubleshooting
479
517
 
@@ -481,8 +519,8 @@ plus a handwritten `http.get("/notes")`.
481
519
 
482
520
  <Accordion title="404 Not Found — route missing">
483
521
  No Flow is bound to that method + path. Check the explicit path, or for pathless routes the
484
- file-tree stamp (`notes/[id]/get.ts` → `GET /notes/:id`). A bare `404` with body `Not Found` means
485
- the router found no match.
522
+ file-tree stamp (`links/[code]/get.ts` → `GET /links/:code`). A bare `404` with body `Not Found`
523
+ means the router found no match.
486
524
  </Accordion>
487
525
 
488
526
  <Accordion title="405 Method Not Allowed on valid route">
@@ -492,8 +530,8 @@ plus a handwritten `http.get("/notes")`.
492
530
 
493
531
  <Accordion title="OKE1040 — pathless trigger never stamped">
494
532
  Cause: `Flow "{flow}" bound {method} with no path — the file-tree stamp never ran.` Import
495
- `@/flows/generated`, run `oke dev` / `oke build`, or pass `http.get("/…")`. Barrels do not stamp —
496
- they need the explicit path.
533
+ `@/flows`, run `oke dev` / `oke build`, or pass `http.get("/…")`. Barrels do not stamp — they need
534
+ the explicit path.
497
535
  </Accordion>
498
536
 
499
537
  <Accordion title="OKE1030 — adopt barrel stale">
@@ -525,8 +563,8 @@ plus a handwritten `http.get("/notes")`.
525
563
  </Accordion>
526
564
 
527
565
  <Accordion title="422 — path param missing from in">
528
- `[id]` stamps `:id`. `in` must declare `id` (same key). A schema that expects `userId` while the
529
- path is `:id` fails validation before `do`.
566
+ `[code]` stamps `:code`. `in` must declare `code` (same key). A schema that expects `id` while the
567
+ path is `:code` fails validation before `do`.
530
568
  </Accordion>
531
569
 
532
570
  <Accordion title="Catch-all input is empty / wrong key">
@@ -537,7 +575,13 @@ plus a handwritten `http.get("/notes")`.
537
575
 
538
576
  <Accordion title="Unit mixes index.ts with tree files">
539
577
  Generate: `Unit "…" mixes a barrel index.ts with tree route files.` Use only `index.ts` (explicit
540
- paths), or move the collection path to `route.ts` and keep `[id]/` beside it.
578
+ paths), or move the collection path to `route.ts` and keep `[code]/` beside it.
579
+ </Accordion>
580
+
581
+ <Accordion title="GET /:code swallowed /health">
582
+ Static paths win. If `/health` 404s, a catch-all `/:code` registered first on the edge matcher
583
+ used to win — upgrade. Shorter binds `GET /:code` as an explicit override; `GET /health` still
584
+ matches `main.health`.
541
585
  </Accordion>
542
586
 
543
587
  </Accordions>
@@ -545,7 +589,7 @@ plus a handwritten `http.get("/notes")`.
545
589
  ## Learn more
546
590
 
547
591
  - [HTTP](/docs/elements/flow/http) — verbs, envelopes, `http.resource`, live SSE
548
- - [Client](/docs/client/calling) — `api.notes.get`, REST vs RPC, `$routes`
592
+ - [Client](/docs/client/calling) — `api.links.get`, REST vs RPC, `$routes`
549
593
  - [Errors](/docs/reference/errors) — OKE1040 · OKE1030 · OKE1041 · OKE1045 · OKE1070 · OKE1072
550
594
  - [The Architecture](/docs/understand/the-architecture) — derived routes, no hand-written table
551
595
  - [Gate](/docs/elements/gate) — `.gate(...)` / `.public()` on the same trigger
@@ -34,7 +34,7 @@ import { gate } from "okengine";
34
34
  export const member = gate.policy("member", ({ auth }) => !!auth.verified);
35
35
  ```
36
36
 
37
- ```typescript title="src/db/schema.decl.ts"
37
+ ```typescript title="src/db/schema.ts"
38
38
  import { store, field } from "okengine";
39
39
 
40
40
  export const tasks = store.schema.table(
@@ -193,7 +193,7 @@ Extras are the **third argument** of `store.schema.table(name, cols, extras)`:
193
193
  `for` accepts `select` · `insert` · `update` · `delete` · `all`. Optional `as`:
194
194
  `"permissive"` (default) or `"restrictive"`. Optional `to` limits Postgres roles.
195
195
 
196
- ```typescript title="src/db/schema.decl.ts"
196
+ ```typescript title="src/db/schema.ts"
197
197
  import { store, field } from "okengine";
198
198
  import { member, bookingCreate } from "@/core/gate";
199
199
 
@@ -24,7 +24,7 @@ For developers persisting domain data on okengine — one handle shape, drivers
24
24
  <Step>
25
25
  ### Declare a SQL store
26
26
 
27
- ```typescript title="src/db/schema.decl.ts"
27
+ ```typescript title="src/db/schema.ts"
28
28
  import { store, field } from "okengine";
29
29
 
30
30
  export const notes = store.schema.table("notes", {
@@ -49,17 +49,20 @@ import { db, notes } from "@/schema";
49
49
  export const create = on(
50
50
  http.post({
51
51
  in: z.object({ title: z.string().min(1) }),
52
+ out: z.object({ id: z.string(), title: z.string() }),
52
53
  }),
53
54
  flow({
54
55
  do: async ({ title }, fx) => {
55
56
  const id = fx.id();
56
- await fx.store(db).insert(notes).values({ id, title });
57
- return fx.json.create({ id, title });
57
+ const [row] = await fx.store(db).insert(notes).values({ id, title }).returning();
58
+ return fx.json.create(row);
58
59
  },
59
60
  }),
60
61
  );
61
62
  ```
62
63
 
64
+ `out` projects the insert row — `createdAt` strips; Date timestamps become ISO-8601.
65
+
63
66
  </Step>
64
67
 
65
68
  <Step>
@@ -84,6 +87,11 @@ Response:
84
87
 
85
88
  </Steps>
86
89
 
90
+ <Callout title="Two declare layouts">
91
+ One file (`schema.ts`) or a folder (`schema/`, one table per file). Both import as `@/db/schema`.
92
+ Pick one — see [SQL](/docs/elements/store/sql#declaring-stores).
93
+ </Callout>
94
+
87
95
  <Callout title="Effects are inferred">
88
96
  Every `fx.store` touch is recorded on the Flow as `reads` / `writes` (`sql:app`, `kv:sessions`,
89
97
  `files:uploads`, …). That powers the Manifest, Console, cache invalidation, and least privilege —