okengine 0.14.1 → 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 (205) hide show
  1. package/package.json +3 -4
  2. package/site/content/docs/elements/ai.mdx +2 -1
  3. package/site/content/docs/elements/channel.mdx +1 -0
  4. package/site/content/docs/elements/clock.mdx +3 -0
  5. package/site/content/docs/elements/flow.mdx +3 -0
  6. package/site/content/docs/elements/gate.mdx +25 -22
  7. package/site/content/docs/elements/signal.mdx +4 -4
  8. package/site/content/docs/elements/store.mdx +86 -27
  9. package/site/content/docs/elements/vault.mdx +1 -0
  10. package/site/content/docs/get-started/basic-usage.mdx +22 -13
  11. package/site/content/docs/get-started/index.mdx +5 -0
  12. package/site/content/docs/get-started/introduction.mdx +11 -3
  13. package/site/content/docs/get-started/meta.json +1 -1
  14. package/site/content/docs/get-started/project-structure.mdx +932 -0
  15. package/site/content/docs/reference/cli.md +13 -10
  16. package/site/content/docs/reference/client.mdx +25 -21
  17. package/site/content/docs/reference/security.md +1 -1
  18. package/src/auth/bindings.ts +14 -5
  19. package/src/auth/cross-plane.ts +10 -3
  20. package/src/cli/ai-setup/ai-setup.test.ts +62 -0
  21. package/src/cli/ai-setup/apply.ts +32 -1
  22. package/src/cli/attach-host-console.test.ts +3 -2
  23. package/src/cli/attach-host-console.ts +3 -0
  24. package/src/cli/build.test.ts +4 -2
  25. package/src/cli/db-seed.ts +48 -15
  26. package/src/cli/db.test.ts +104 -0
  27. package/src/cli/db.ts +122 -10
  28. package/src/cli/dev-controls.test.ts +20 -0
  29. package/src/cli/dev-controls.ts +13 -0
  30. package/src/cli/dev.test.ts +15 -4
  31. package/src/cli/dev.ts +102 -29
  32. package/src/cli/ensure-drizzle-config.ts +2 -1
  33. package/src/cli/tui/DevLive.tsx +12 -2
  34. package/src/cli/tui/keys.test.ts +2 -1
  35. package/src/cli/tui/keys.ts +3 -2
  36. package/src/compiler/aot.test.ts +1 -1
  37. package/src/compiler/extract.test.ts +409 -33
  38. package/src/compiler/extract.ts +318 -67
  39. package/src/compiler/flow-path.test.ts +96 -0
  40. package/src/compiler/flow-path.ts +173 -0
  41. package/src/compiler/generate-adopt.test.ts +112 -3
  42. package/src/compiler/generate-adopt.ts +280 -26
  43. package/src/compiler/schema-from-ast.ts +609 -0
  44. package/src/console/server/bind.ts +2 -2
  45. package/src/console/server/console.test.ts +3 -0
  46. package/src/console/server/flows.ts +120 -40
  47. package/src/console/server/invoke-user-flow.test.ts +3 -1
  48. package/src/console/server/invoke-user-flow.ts +17 -2
  49. package/src/console/server/iso-at.test.ts +9 -0
  50. package/src/console/server/iso-at.ts +11 -0
  51. package/src/console/server/security-headers.ts +6 -3
  52. package/src/console/server/spa-proxy.test.ts +1 -0
  53. package/src/console/server/sql-catalog.ts +147 -7
  54. package/src/console/server/sql-rls.test.ts +16 -0
  55. package/src/console/server/state.ts +8 -0
  56. package/src/console/server/store.test.ts +7 -4
  57. package/src/console/server/store.ts +142 -58
  58. package/src/console/ui-next/dist/assets/agent-disclosure-C0X1fbWF.js +1 -0
  59. package/src/console/ui-next/dist/assets/{cache-glyph-BanhLsEY.js → cache-glyph-F1FI122b.js} +1 -1
  60. package/src/console/ui-next/dist/assets/{call-pii-button-bqkxMrJH.js → call-pii-button-DEDCl_j3.js} +1 -1
  61. package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +1 -0
  62. package/src/console/ui-next/dist/assets/{confirm-sheet-DDCRmG62.js → confirm-sheet-3ptDzXbz.js} +1 -1
  63. package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
  64. package/src/console/ui-next/dist/assets/{explorer-empty-HjTnVQoR.js → explorer-empty-BvYrygyO.js} +1 -1
  65. package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +1 -0
  66. package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-CS_O8L-r.js} +1 -1
  67. package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +1 -0
  68. package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +66 -0
  69. package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +2 -0
  70. package/src/console/ui-next/dist/assets/{link-DF7SZ9Ek.js → link-COeyggt-.js} +1 -1
  71. package/src/console/ui-next/dist/assets/{observability-page-BEZDzyYh.js → observability-page-HK9-BO8z.js} +2 -2
  72. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
  73. package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +18 -0
  74. package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +1 -0
  75. package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +1 -0
  76. package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +1 -0
  77. package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +41 -0
  78. package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +2 -0
  79. package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +54 -0
  80. package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +1 -0
  81. package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-BLbZhzyF.js} +1 -1
  82. package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +2 -0
  83. package/src/console/ui-next/dist/index.html +6 -6
  84. package/src/console/ui-next/src/client.ts +35 -7
  85. package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
  86. package/src/console/ui-next/src/components/motion/table/types.ts +4 -0
  87. package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
  88. package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
  89. package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +19 -1
  90. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
  91. package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
  92. package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
  93. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
  94. package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
  95. package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
  96. package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
  97. package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
  98. package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
  99. package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
  100. package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
  101. package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
  102. package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
  103. package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
  104. package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
  105. package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
  106. package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
  107. package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
  108. package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
  109. package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
  110. package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
  111. package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
  112. package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
  113. package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
  114. package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
  115. package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
  116. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
  117. package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
  118. package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
  119. package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
  120. package/src/docker/compose-health.test.ts +27 -0
  121. package/src/docker/compose-health.ts +19 -3
  122. package/src/docker/docker.test.ts +3 -0
  123. package/src/docker/recipes/pgdog.ts +2 -0
  124. package/src/drivers/clock-postgres.ts +13 -9
  125. package/src/drivers/instances-postgres.ts +4 -6
  126. package/src/drivers/journal-postgres.ts +13 -9
  127. package/src/drivers/pg-rls.test.ts +87 -0
  128. package/src/drivers/pg-rls.ts +233 -7
  129. package/src/drivers/pglite.ts +26 -3
  130. package/src/drivers/postgres.test.ts +23 -0
  131. package/src/drivers/postgres.ts +133 -9
  132. package/src/drivers/types.ts +10 -0
  133. package/src/elements/clock/declare.ts +26 -1
  134. package/src/elements/gate/boot.ts +5 -5
  135. package/src/elements/gate/declare.ts +36 -4
  136. package/src/elements/gate/flatten.ts +1 -1
  137. package/src/elements/gate/runtime.ts +1 -1
  138. package/src/elements/store/declare.ts +9 -6
  139. package/src/elements/store/emit-drizzle.ts +44 -8
  140. package/src/elements/store/resource.test.ts +1 -1
  141. package/src/elements/store/rls-identity.test.ts +95 -0
  142. package/src/elements/store/rls-identity.ts +116 -0
  143. package/src/elements/store/runtime.ts +4 -0
  144. package/src/elements/store/schema-decl.test.ts +37 -0
  145. package/src/elements/store/schema-decl.ts +161 -0
  146. package/src/elements/store/seed.ts +1 -1
  147. package/src/elements/store/sql-condition.test.ts +12 -0
  148. package/src/elements/store/sql-condition.ts +26 -2
  149. package/src/elements/store/sql-rls-isolation.test.ts +165 -0
  150. package/src/elements/store/sql-rls-stamp.test.ts +117 -0
  151. package/src/elements/store/sql-session.ts +81 -3
  152. package/src/elements/store/upsert-app.test.ts +1 -1
  153. package/src/elements/store.ts +14 -0
  154. package/src/http.ts +20 -3
  155. package/src/index.ts +10 -1
  156. package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
  157. package/src/kernel/adopt-routes.ts +30 -2
  158. package/src/kernel/app.ts +131 -14
  159. package/src/kernel/auto-cache.test.ts +8 -8
  160. package/src/kernel/auto-registry.test.ts +49 -9
  161. package/src/kernel/boot.test.ts +4 -4
  162. package/src/kernel/boot.ts +2 -2
  163. package/src/kernel/effects-stamping.test.ts +3 -3
  164. package/src/kernel/element-registries.ts +12 -5
  165. package/src/kernel/errors.ts +24 -0
  166. package/src/kernel/flow-units.ts +64 -0
  167. package/src/kernel/flow.ts +29 -17
  168. package/src/kernel/fx.ts +23 -0
  169. package/src/kernel/horizontal-child.ts +4 -4
  170. package/src/kernel/http-path-pending.ts +20 -0
  171. package/src/kernel/http-resource.ts +4 -0
  172. package/src/kernel/http-stream.test.ts +4 -4
  173. package/src/kernel/pipeline.test.ts +2 -2
  174. package/src/kernel/plugin-elements.test.ts +1 -1
  175. package/src/kernel/plugin-needs.test.ts +1 -1
  176. package/src/kernel/ready.test.ts +2 -2
  177. package/src/kernel/resource-mount.test.ts +14 -0
  178. package/src/kernel/run-duration.test.ts +2 -2
  179. package/src/kernel/stamp-http.test.ts +79 -0
  180. package/src/kernel/stamp-http.ts +75 -0
  181. package/src/kernel/triggers.ts +52 -27
  182. package/src/manifest/types.ts +13 -0
  183. package/src/release/measure.ts +2 -2
  184. package/src/runtime/json-code-block.test.ts +2 -2
  185. package/src/runtime/serve.test.ts +4 -4
  186. package/src/term.test.ts +52 -4
  187. package/src/term.ts +125 -17
  188. package/src/test/reset-element-registries.ts +8 -3
  189. package/src/cli/ask-seed.test.ts +0 -96
  190. package/src/cli/ask-seed.ts +0 -82
  191. package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
  192. package/src/console/ui-next/dist/assets/flows-page-DxDsOd4f.js +0 -1
  193. package/src/console/ui-next/dist/assets/http-method-CJCBYL2j.js +0 -1
  194. package/src/console/ui-next/dist/assets/index-Ce6WKWKM.js +0 -66
  195. package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
  196. package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
  197. package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
  198. package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
  199. package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
  200. package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
  201. package/src/console/ui-next/dist/assets/skeleton-CL_X0GCj.js +0 -1
  202. package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
  203. package/src/console/ui-next/dist/assets/trace-detail-sheet-DFLFfUUX.js +0 -2
  204. package/src/console/ui-next/dist/assets/units-page-C0gW6Kdo.js +0 -1
  205. package/src/console/ui-next/dist/assets/vault-page-DuKzqwzW.js +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okengine",
3
- "version": "0.14.1",
3
+ "version": "0.15.2",
4
4
  "description": "One law. Eight elements. Ten exports. One package. One manifest. Every backend need is derived, never added.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -174,10 +174,9 @@
174
174
  "d3-array": "^3.2.4",
175
175
  "d3-shape": "^3.2.0",
176
176
  "date-fns": "^4.1.0",
177
- "drizzle-kit": "1.0.0-rc.4",
178
- "drizzle-orm": "1.0.0-rc.4",
177
+ "drizzle-kit": "1.0.0-rc.5-ab785fc",
178
+ "drizzle-orm": "1.0.0-rc.5-169397b",
179
179
  "drizzle-seed": "^0.3.1",
180
- "drizzle-zod": "^0.8.3",
181
180
  "gflows": "^1.2.1",
182
181
  "happy-dom": "^20.11.1",
183
182
  "hast-util-to-jsx-runtime": "^2.3.6",
@@ -182,7 +182,7 @@ Return those chunks on HTTP with `fx.json.stream(...)`. The kernel answers `text
182
182
 
183
183
  ```typescript
184
184
  on(
185
- http.post("/complete").gate.public,
185
+ http.post("/complete").public(),
186
186
  flow("chat.complete", {
187
187
  do: (input, fx) => fx.json.stream(fx.stream(smart, { prompt: input.prompt })),
188
188
  }),
@@ -364,6 +364,7 @@ A hang-up is `AbortError` — it does not retry or advance `via`.
364
364
 
365
365
  ## Learn more
366
366
 
367
+ - [Project structure](/docs/get-started/project-structure) — agent `tools` are Flow names (`notes.get`), not URLs
367
368
  - [Flow](/docs/elements/flow) — `fx.ask` and `fx.search` inside `do`
368
369
  - [Store](/docs/elements/store) — `store.index`, the home of embeddings
369
370
  - [MCP](/docs/ai/mcp) — serve the app to agents, or consume external servers as tools
@@ -327,6 +327,7 @@ Confirm a visible OS notification titled `okengine webpush interop`.
327
327
 
328
328
  ## Learn more
329
329
 
330
+ - [Project structure](/docs/get-started/project-structure) — `flow({…})` names as `unit.export`; explicit `flow("unit.action")` still wins
330
331
  - [Flow](/docs/elements/flow) — `fx.send` inside `do`
331
332
  - [Environment Variables](/docs/reference/environment-variables) — Channel boot credentials
332
333
  - [Configuration](/docs/reference/configuration) — `drivers.channel.*` maps
@@ -52,6 +52,8 @@ export const dailyReport = clock("daily-report", {
52
52
  });
53
53
  ```
54
54
 
55
+ Import the module before `oke()` — named clocks auto-register. No `oke({ clocks })` list.
56
+
55
57
  ```typescript title="src/flows/reports/daily.ts"
56
58
  export const sendDaily = on(
57
59
  dailyReport,
@@ -218,6 +220,7 @@ The registry is off in `test` and when there is no shared SQL URL. `dev`/`prod`
218
220
 
219
221
  ## Learn more
220
222
 
223
+ - [Project structure](/docs/get-started/project-structure) — clock flows in the tree get a name, never a URL
221
224
  - [Flow](/docs/elements/flow) — `on(trigger, flow)` and the `fx` surface
222
225
  - [fx · Runs](/docs/reference/fx#runs-observability-read) — native SLO checkers via `on(every(…))` + `fx.runs`
223
226
  - [Signal](/docs/elements/signal) — reacting to events instead of time
@@ -112,6 +112,8 @@ Built-in codes attach a localized `message` ([Errors](/docs/reference/errors));
112
112
 
113
113
  `http.get` · `http.post` · `http.put` · `http.patch` · `http.delete` · `http.head` · `http.options` · `http.query`. QUERY (RFC 10008) is a safe, idempotent read with a JSON body.
114
114
 
115
+ `http.get()` with no path is filled from the file tree (`flows/notes/[id]/get.ts` → `/notes/:id`). `flow({ do })` is named `unit.export`. Explicit `http.get("/me/tasks")` and `flow("notes.get")` still win. Folder conventions live on [Project structure](/docs/get-started/project-structure).
116
+
115
117
  `Content-Type: application/json` is required — missing → **400** `InvalidQuery`, anything else → **415** `UnsupportedMediaType` with `Accept-Query: "application/json"`. Invalid JSON is **400** (no sniffing). Responses advertise `Accept-Query`. okengine does not assign `Location` / `Content-Location` result URIs.
116
118
 
117
119
  A browser `GET` (`Accept` prefers `text/html`) paints the `{ data, error }` envelope in traces chrome — status, latency, and cache, Routes rail (static GET links), copy, line numbers, Pretty / Raw, Console on **6533**. `curl`, `Accept: application/json`, and `?format=json` stay JSON.
@@ -369,6 +371,7 @@ a **hit**. A matching write turns the next read into a miss.
369
371
 
370
372
  ## Learn more
371
373
 
374
+ - [Project structure](/docs/get-started/project-structure) — folders are the URL; `unit.export` is the name
372
375
  - [fx](/docs/reference/fx) — full `fx` surface, including ICU `fx.t` / `fx.locale`
373
376
  - [Signal](/docs/elements/signal) — delivery physics (`once` · `broadcast` · `live`)
374
377
  - [Clock](/docs/elements/clock) — schedules and durable sleep
@@ -10,7 +10,7 @@ burned its minute quota? Attach gates to the HTTP trigger — the pipeline check
10
10
  store write, emit, or channel send runs.
11
11
 
12
12
  <Callout title="The one rule">
13
- Every HTTP trigger declares posture: attach a real gate, or attach `.gate.public`. Omitting both
13
+ Every HTTP trigger declares posture: attach a real gate, or attach `.public()`. Omitting both
14
14
  fails boot. Denial is a typed error value — never a thrown exception mid-`do`.
15
15
  </Callout>
16
16
 
@@ -19,10 +19,10 @@ store write, emit, or channel send runs.
19
19
  <Steps>
20
20
 
21
21
  <Step>
22
- ### Declare policies and register the Gate bag
22
+ ### Declare policies
23
23
 
24
- Policies are named predicates; rate limits are declarative budgets. Put the ones you reuse in
25
- `oke({ gate: { policies } })`:
24
+ Policies are named predicates; rate limits are declarative budgets. Import the module before
25
+ `oke()` — they auto-register. `oke({ gate: { policies } })` is still valid and additive.
26
26
 
27
27
  ```typescript title="src/gates.ts"
28
28
  import { gate } from "okengine";
@@ -43,14 +43,10 @@ export const fair = gate.rate({
43
43
  the scope checked.
44
44
 
45
45
  ```typescript title="src/app.ts"
46
+ import "./gates";
46
47
  import { oke } from "okengine";
47
- import { member, canBook, fair } from "./gates";
48
48
 
49
- export const app = oke({
50
- name: "shop",
51
- env: "dev",
52
- gate: { policies: [member, canBook, fair] },
53
- });
49
+ export const app = oke({ name: "shop", env: "dev" });
54
50
  ```
55
51
 
56
52
  </Step>
@@ -58,14 +54,14 @@ export const app = oke({
58
54
  <Step>
59
55
  ### Attach them to triggers
60
56
 
61
- `.gate(...)` composes left to right — first denial wins. Public routes must say so explicitly:
57
+ `.gate(...)` composes left to right — first denial wins. Public routes must say so explicitly. Path and name can come from the file tree — see [Project structure](/docs/get-started/project-structure).
62
58
 
63
59
  ```typescript title="src/flows/links/shorten.ts"
64
60
  import { on, flow, http, gate } from "okengine";
65
61
  import { member, canBook, fair } from "../../gates";
66
62
 
67
63
  export const health = on(
68
- http.get("/health").gate.public,
64
+ http.get("/health").public(),
69
65
  flow("health.check", {
70
66
  out: HealthOut,
71
67
  do: () => ({ ok: true as const }),
@@ -88,8 +84,8 @@ export const shorten = on(
88
84
  ```
89
85
 
90
86
  A single gate stays `.gate(member)`. Reuse two or more with `gate.all` — `.gate(write)`
91
- flattens. `http.resource(path, ops).gate(...)` stamps every verb. Do **not** put
92
- `gate.public` in `gate.policies` — attach it on the trigger only.
87
+ flattens. `http.resource(path, ops).gate(...)` stamps every verb. Public routes use
88
+ `.public()` — do **not** put `gate.public` in `gate.policies`.
93
89
 
94
90
  </Step>
95
91
 
@@ -131,16 +127,16 @@ gates are skipped. `do` runs only when every gate passed.
131
127
  | ------------------------------------- | ------------------------------------------- | ---------------------------------- |
132
128
  | `gate.policy(name, check \| options)` | Is this principal allowed? (ABAC) | auth / operator / request metadata |
133
129
  | `gate.scope(name)` | Does `auth.scopes` contain this exact name? | `auth.scopes` |
134
- | `gate.public` / `.gate.public` | Intentionally unauthenticated surface | always allows |
130
+ | `gate.public` / `.public()` | Intentionally unauthenticated surface | always allows |
135
131
  | `gate.rate(options)` | Is there budget left for this subject? | an atomic counter on the kv driver |
136
132
  | `gate.all(...members)` | Reuse this chain on many triggers | flattens; each member is evaluated |
137
133
 
138
134
  Pass `{ check, description }` on `gate.policy` when you want a human title in the Console (falls
139
- back to the policy name). The name `"public"` is reserved — use `gate.public`, not
140
- `gate.policy("public", …)`.
135
+ back to the policy name). The name `"public"` is reserved — mark the trigger with
136
+ `.public()`, not `gate.policy("public", …)`.
141
137
 
142
138
  `gate.all()` with no members throws. Nested `all` handles flatten.
143
- `oke({ gate: { policies: [write] } })` registers the members.
139
+ Members auto-register when declared; `oke({ gate: { policies: [write] } })` is additive.
144
140
 
145
141
  ### Policy context
146
142
 
@@ -178,11 +174,11 @@ driver in tests. Boot opens a dedicated `oke:gates` namespace on **`drivers.stor
178
174
 
179
175
  ## Auth posture at boot
180
176
 
181
- Every HTTP trigger must carry a gate or `.gate.public`. Omitting both fails boot with
177
+ Every HTTP trigger must carry a gate or `.public()`. Omitting both fails boot with
182
178
  `GateBootError` — message starts with `gate boot failed —` and lists every gap.
183
179
 
184
180
  ```typescript
185
- on(http.get("/health").gate.public, flow("health.check", {/* … */}));
181
+ on(http.get("/health").public(), flow("health.check", {/* … */}));
186
182
  on(http.post("/bookings").gate(member, canBook), flow("bookings.create", {/* … */}));
187
183
  ```
188
184
 
@@ -191,7 +187,7 @@ on(http.post("/bookings").gate(member, canBook), flow("bookings.create", {/* …
191
187
  posture but does **not** authenticate.
192
188
 
193
189
  `gate: { unguardedHttp: "allow" }` skips the audit **only** when `env === "test"`.
194
- In `dev` / `prod` migrate with per-trigger `.gate.public`.
190
+ In `dev` / `prod` migrate with per-trigger `.public()`.
195
191
 
196
192
  ## Built-in auth (`gate.auth`)
197
193
 
@@ -270,12 +266,18 @@ Pass **and** deny land on the run's `gates` dimension. Console (`:6533` → `/fl
270
266
  chain on the flow contract. Rate `overridable` is a catalog flag only — Clock schedules are what the Console can
271
267
  edit live today.
272
268
 
269
+ <Callout title="RLS reads the stamped principal">
270
+ After HTTP Gate passes, user-plane `fx.store` stamps `oke.gate()`, `oke.user()`, and
271
+ `oke.has_scope()`. Rate, cron, CDC, and signal stay unstamped. Helpers:
272
+ [Store](/docs/elements/store).
273
+ </Callout>
274
+
273
275
  ## Troubleshooting
274
276
 
275
277
  <Accordions>
276
278
  <Accordion title="Boot: gate boot failed — N HTTP trigger(s) missing auth posture">
277
279
 
278
- Empty `.gate(...)` chain. Attach a policy/rate gate, or `.gate.public`. First `fetch`
280
+ Empty `.gate(...)` chain. Attach a policy/rate gate, or `.public()`. First `fetch`
279
281
  auto-boots — do not rely on `gate.unguardedHttp: "allow"` outside `env: "test"`.
280
282
 
281
283
  </Accordion>
@@ -313,6 +315,7 @@ Keying an authenticated endpoint by IP punishes shared NAT.
313
315
 
314
316
  ## Learn more
315
317
 
318
+ - [Project structure](/docs/get-started/project-structure) — `http.get()` and `flow({…})` fill path and name from the file tree
316
319
  - [Flow](/docs/elements/flow) — the trigger pipeline gates plug into
317
320
  - [fx](/docs/reference/fx) — `fx.auth`, `fx.operator`, `fx.principal`
318
321
  - [Client](/docs/reference/client) — Bearer `createClient` + `okengine/client/auth`
@@ -131,7 +131,7 @@ A Flow **writes** all three tapes. `once` / `broadcast` **listen** with `on(sign
131
131
 
132
132
  ```typescript title="src/flows/orders/place.ts"
133
133
  export const placeOrder = on(
134
- http.post("/orders").gate.public,
134
+ http.post("/orders").public(),
135
135
  flow("orders.place", {
136
136
  in: z.object({ id: z.string(), total: z.number() }),
137
137
  do: async (input, fx) => {
@@ -256,9 +256,8 @@ Committed `once` messages survive process death. A claim sets `lockedBy` and a v
256
256
 
257
257
  <Callout title="Client subscription is not shipped yet">
258
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 / WebSocket / `client.live` yet — poll
260
- an HTTP Flow. See [Client · Signal and live
261
- queries](/docs/reference/client#signal-and-live-queries).
259
+ server — not `on(signal, flow)`. `createClient` has no SSE yet poll HTTP. See
260
+ [Client](/docs/reference/client#signal-and-live-queries).
262
261
  </Callout>
263
262
 
264
263
  ## Orphaned signal config
@@ -335,6 +334,7 @@ At-least-once: crash-after-claim reclaims when the lease expires, and a handler
335
334
 
336
335
  ## Learn more
337
336
 
337
+ - [Project structure](/docs/get-started/project-structure) — signal files get a name, never a URL; `http.get("/…")` still wins
338
338
  - [Flow](/docs/elements/flow) — `on(trigger, flow)` and `fx.emit`
339
339
  - [fx](/docs/reference/fx) — `fx.emit` and `fx.deadLetters`
340
340
  - [Clock](/docs/elements/clock) — scheduled and delayed work
@@ -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
 
@@ -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."
@@ -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
  }