toiljs 0.0.116 → 0.0.118

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 (337) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +16 -7
  3. package/build/backend/.tsbuildinfo +1 -1
  4. package/build/cli/.tsbuildinfo +1 -1
  5. package/build/cli/index.js +2375 -258
  6. package/build/client/.tsbuildinfo +1 -1
  7. package/build/client/auth.d.ts +3 -0
  8. package/build/client/auth.js +19 -10
  9. package/build/client/dev/devtools.js +1 -1
  10. package/build/client/dev/error-overlay.js +1 -1
  11. package/build/client/index.d.ts +1 -1
  12. package/build/client/index.js +1 -1
  13. package/build/client/navigation/navigation.js +28 -3
  14. package/build/client/routing/Router.js +5 -2
  15. package/build/client/routing/mount.js +1 -1
  16. package/build/compiler/.tsbuildinfo +1 -1
  17. package/build/compiler/auth-emails.js +7 -3
  18. package/build/compiler/emails.js +1 -1
  19. package/build/compiler/index.d.ts +1 -0
  20. package/build/compiler/index.js +26 -5
  21. package/build/compiler/pages.js +2 -3
  22. package/build/compiler/prerender.d.ts +3 -8
  23. package/build/compiler/prerender.js +88 -127
  24. package/build/compiler/ssg.js +3 -4
  25. package/build/compiler/template-build.js +3 -4
  26. package/build/compiler/toil-docs.generated.js +8 -7
  27. package/build/compiler/vite.js +4 -6
  28. package/build/devserver/.tsbuildinfo +1 -1
  29. package/build/devserver/db/database.js +7 -3
  30. package/build/devserver/db/types.js +1 -1
  31. package/build/devserver/production.js +4 -3
  32. package/build/devserver/runtime/crypto.d.ts +1 -1
  33. package/build/io/.tsbuildinfo +1 -1
  34. package/build/logger/.tsbuildinfo +1 -1
  35. package/build/shared/.tsbuildinfo +1 -1
  36. package/examples/basic/client/routes/analytics.tsx +7 -5
  37. package/examples/basic/client/routes/pq.tsx +2 -1
  38. package/examples/basic/server/tsconfig.json +1 -3
  39. package/package.json +45 -44
  40. package/presets/no-uint8array-tostring.js +120 -153
  41. package/presets/oxlint.d.ts +5 -0
  42. package/presets/oxlint.js +224 -0
  43. package/server/auth/AuthController.ts +53 -14
  44. package/server/auth/FactorSettings.ts +41 -0
  45. package/server/auth/SessionEpochs.ts +58 -0
  46. package/server/auth/SessionRevocations.ts +13 -0
  47. package/server/globals/auth.ts +27 -2
  48. package/server/runtime/tsconfig.json +1 -3
  49. package/tsconfig.json +2 -11
  50. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
  51. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -90
  52. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  53. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  54. package/.github/PULL_REQUEST_TEMPLATE.md +0 -43
  55. package/.github/changelog-config.json +0 -45
  56. package/.github/dependabot.yml +0 -27
  57. package/.github/workflows/ci.yml +0 -222
  58. package/.vscode/settings.json +0 -10
  59. package/docs/README.md +0 -107
  60. package/docs/auth/README.md +0 -104
  61. package/docs/auth/configuration.md +0 -105
  62. package/docs/auth/emails.md +0 -92
  63. package/docs/auth/extending.md +0 -202
  64. package/docs/auth/how-it-works.md +0 -138
  65. package/docs/auth/usage.md +0 -188
  66. package/docs/backend/README.md +0 -143
  67. package/docs/backend/data.md +0 -351
  68. package/docs/backend/rest.md +0 -402
  69. package/docs/backend/rpc.md +0 -226
  70. package/docs/background/README.md +0 -114
  71. package/docs/background/daemons.md +0 -276
  72. package/docs/background/derive.md +0 -179
  73. package/docs/cli/README.md +0 -379
  74. package/docs/concepts/ai-guide.md +0 -132
  75. package/docs/concepts/config.md +0 -416
  76. package/docs/concepts/decorators.md +0 -127
  77. package/docs/concepts/security.md +0 -108
  78. package/docs/concepts/tiers.md +0 -166
  79. package/docs/concepts/types.md +0 -216
  80. package/docs/database/README.md +0 -143
  81. package/docs/database/capacity.md +0 -350
  82. package/docs/database/counters.md +0 -174
  83. package/docs/database/documents.md +0 -277
  84. package/docs/database/events.md +0 -307
  85. package/docs/database/membership.md +0 -246
  86. package/docs/database/setup.md +0 -155
  87. package/docs/database/unique.md +0 -216
  88. package/docs/database/views.md +0 -246
  89. package/docs/frontend/README.md +0 -104
  90. package/docs/frontend/components.md +0 -286
  91. package/docs/frontend/data-fetching.md +0 -260
  92. package/docs/frontend/images.md +0 -148
  93. package/docs/frontend/metadata.md +0 -344
  94. package/docs/frontend/navigation.md +0 -296
  95. package/docs/frontend/rendering.md +0 -236
  96. package/docs/frontend/routing.md +0 -346
  97. package/docs/frontend/scripts.md +0 -118
  98. package/docs/frontend/search.md +0 -191
  99. package/docs/frontend/styling.md +0 -147
  100. package/docs/frontend/toil-global.md +0 -216
  101. package/docs/getting-started/README.md +0 -81
  102. package/docs/getting-started/create-project.md +0 -131
  103. package/docs/getting-started/deploy.md +0 -101
  104. package/docs/getting-started/first-app.md +0 -215
  105. package/docs/getting-started/installation.md +0 -124
  106. package/docs/getting-started/migrating.md +0 -125
  107. package/docs/getting-started/project-structure.md +0 -163
  108. package/docs/introduction/README.md +0 -66
  109. package/docs/introduction/design-principles.md +0 -81
  110. package/docs/introduction/distributed.md +0 -91
  111. package/docs/introduction/how-it-works.md +0 -121
  112. package/docs/introduction/hyperscale.md +0 -70
  113. package/docs/introduction/modern-stack.md +0 -85
  114. package/docs/introduction/vs-other-frameworks.md +0 -64
  115. package/docs/introduction/why-toil.md +0 -99
  116. package/docs/llms.txt +0 -95
  117. package/docs/realtime/README.md +0 -102
  118. package/docs/realtime/channels.md +0 -211
  119. package/docs/realtime/streams.md +0 -369
  120. package/docs/services/README.md +0 -60
  121. package/docs/services/analytics.md +0 -279
  122. package/docs/services/caching.md +0 -175
  123. package/docs/services/cookies.md +0 -235
  124. package/docs/services/crypto.md +0 -209
  125. package/docs/services/email.md +0 -289
  126. package/docs/services/environment.md +0 -141
  127. package/docs/services/ratelimit.md +0 -174
  128. package/docs/services/time.md +0 -85
  129. package/eslint.config.js +0 -57
  130. package/examples/basic/client/routes/docs/[...slug].tsx +0 -12
  131. package/presets/eslint.js +0 -88
  132. package/src/backend/index.ts +0 -204
  133. package/src/cli/configure.ts +0 -363
  134. package/src/cli/create.ts +0 -924
  135. package/src/cli/db.ts +0 -158
  136. package/src/cli/diagnostics.ts +0 -665
  137. package/src/cli/doctor.ts +0 -982
  138. package/src/cli/features.ts +0 -170
  139. package/src/cli/index.ts +0 -326
  140. package/src/cli/notify.ts +0 -120
  141. package/src/cli/proc.ts +0 -56
  142. package/src/cli/ui.ts +0 -169
  143. package/src/cli/update.ts +0 -248
  144. package/src/cli/updates.ts +0 -92
  145. package/src/cli/validate.ts +0 -31
  146. package/src/cli/version-check.ts +0 -125
  147. package/src/client/auth.ts +0 -869
  148. package/src/client/channel/channel.ts +0 -146
  149. package/src/client/components/Form.tsx +0 -65
  150. package/src/client/components/Image.tsx +0 -160
  151. package/src/client/components/Script.tsx +0 -113
  152. package/src/client/components/Slot.tsx +0 -21
  153. package/src/client/dev/devtools.tsx +0 -1089
  154. package/src/client/dev/error-overlay.tsx +0 -225
  155. package/src/client/errors.ts +0 -11
  156. package/src/client/head/head.ts +0 -182
  157. package/src/client/head/metadata.ts +0 -110
  158. package/src/client/index.ts +0 -130
  159. package/src/client/navigation/Link.tsx +0 -108
  160. package/src/client/navigation/NavLink.tsx +0 -86
  161. package/src/client/navigation/navigation.ts +0 -235
  162. package/src/client/navigation/prefetch.ts +0 -169
  163. package/src/client/navigation/scroll.ts +0 -53
  164. package/src/client/routing/Router.tsx +0 -216
  165. package/src/client/routing/action.ts +0 -122
  166. package/src/client/routing/error-boundary.tsx +0 -43
  167. package/src/client/routing/hooks.ts +0 -140
  168. package/src/client/routing/lazy.ts +0 -77
  169. package/src/client/routing/loader.ts +0 -381
  170. package/src/client/routing/match.ts +0 -47
  171. package/src/client/routing/mount.tsx +0 -90
  172. package/src/client/routing/params-context.ts +0 -10
  173. package/src/client/routing/slot-context.ts +0 -7
  174. package/src/client/rpc.ts +0 -97
  175. package/src/client/search/search.ts +0 -189
  176. package/src/client/search/use-page-search.ts +0 -73
  177. package/src/client/ssr/markers.tsx +0 -172
  178. package/src/client/stream/client.ts +0 -237
  179. package/src/client/types.ts +0 -73
  180. package/src/compiler/auth-emails.ts +0 -312
  181. package/src/compiler/config.ts +0 -369
  182. package/src/compiler/docs.ts +0 -127
  183. package/src/compiler/email-preview.ts +0 -314
  184. package/src/compiler/emails.ts +0 -404
  185. package/src/compiler/fonts.ts +0 -89
  186. package/src/compiler/generate.ts +0 -471
  187. package/src/compiler/image-blur.ts +0 -42
  188. package/src/compiler/image-report.ts +0 -88
  189. package/src/compiler/index.ts +0 -1362
  190. package/src/compiler/pages.ts +0 -49
  191. package/src/compiler/plugin.ts +0 -391
  192. package/src/compiler/prerender.ts +0 -263
  193. package/src/compiler/routes.ts +0 -170
  194. package/src/compiler/seo.ts +0 -415
  195. package/src/compiler/sri.ts +0 -144
  196. package/src/compiler/ssg.ts +0 -177
  197. package/src/compiler/ssr-codegen.ts +0 -85
  198. package/src/compiler/template-build.ts +0 -676
  199. package/src/compiler/template.ts +0 -262
  200. package/src/compiler/toil-docs.generated.ts +0 -77
  201. package/src/compiler/vite.ts +0 -245
  202. package/src/devserver/analytics/index.ts +0 -317
  203. package/src/devserver/config/dotenv.ts +0 -102
  204. package/src/devserver/config/env.ts +0 -30
  205. package/src/devserver/config/ratelimit.ts +0 -123
  206. package/src/devserver/daemon/catalog.ts +0 -120
  207. package/src/devserver/daemon/cron.ts +0 -87
  208. package/src/devserver/daemon/host.ts +0 -134
  209. package/src/devserver/daemon/index.ts +0 -369
  210. package/src/devserver/daemon/runtime.ts +0 -48
  211. package/src/devserver/db/catalog.ts +0 -109
  212. package/src/devserver/db/database.ts +0 -1834
  213. package/src/devserver/db/derives.ts +0 -121
  214. package/src/devserver/db/index.ts +0 -20
  215. package/src/devserver/db/routeKinds.ts +0 -191
  216. package/src/devserver/db/types.ts +0 -168
  217. package/src/devserver/email/caps.ts +0 -0
  218. package/src/devserver/email/config.ts +0 -141
  219. package/src/devserver/email/console.ts +0 -348
  220. package/src/devserver/email/index.ts +0 -155
  221. package/src/devserver/email/providers.ts +0 -131
  222. package/src/devserver/email/status.ts +0 -23
  223. package/src/devserver/email/validate.ts +0 -37
  224. package/src/devserver/email/wire.ts +0 -55
  225. package/src/devserver/http/cache.ts +0 -0
  226. package/src/devserver/http/envelope.ts +0 -157
  227. package/src/devserver/http/proxy.ts +0 -172
  228. package/src/devserver/http/runtime.ts +0 -387
  229. package/src/devserver/index.ts +0 -43
  230. package/src/devserver/production-ipc.ts +0 -63
  231. package/src/devserver/production-worker.ts +0 -83
  232. package/src/devserver/production.ts +0 -718
  233. package/src/devserver/runtime/crypto.ts +0 -587
  234. package/src/devserver/runtime/host.ts +0 -427
  235. package/src/devserver/runtime/module.ts +0 -442
  236. package/src/devserver/server.ts +0 -251
  237. package/src/devserver/ssr.ts +0 -208
  238. package/src/devserver/stream/catalog.ts +0 -106
  239. package/src/devserver/stream/host.ts +0 -42
  240. package/src/devserver/stream/index.ts +0 -301
  241. package/src/devserver/stream/manager.ts +0 -167
  242. package/src/devserver/stream/router.ts +0 -101
  243. package/src/devserver/stream/wire.ts +0 -58
  244. package/src/devserver/stream/ws.ts +0 -78
  245. package/src/devserver/wasm/sections.ts +0 -59
  246. package/src/devserver/wasm/surface.ts +0 -115
  247. package/src/io/FastMap.ts +0 -151
  248. package/src/io/FastSet.ts +0 -113
  249. package/src/io/codec.ts +0 -362
  250. package/src/io/index.ts +0 -10
  251. package/src/io/lengths.ts +0 -14
  252. package/src/io/types.ts +0 -19
  253. package/src/logger/index.ts +0 -22
  254. package/src/shared/index.ts +0 -46
  255. package/test/analytics-dev.test.ts +0 -113
  256. package/test/assembly/aspect-shim.ts +0 -33
  257. package/test/assembly/cookie.spec.ts +0 -303
  258. package/test/assembly/example.spec.ts +0 -22
  259. package/test/assembly/ssr.spec.ts +0 -95
  260. package/test/assembly.test.ts +0 -59
  261. package/test/auth-emails.test.ts +0 -118
  262. package/test/built-ssr.test.ts +0 -148
  263. package/test/channel.test.ts +0 -21
  264. package/test/config-host-port.test.ts +0 -59
  265. package/test/configure.test.ts +0 -94
  266. package/test/daemon-build.test.ts +0 -278
  267. package/test/daemon-catalog.test.ts +0 -349
  268. package/test/daemon-emulation.test.ts +0 -256
  269. package/test/db.test.ts +0 -0
  270. package/test/devserver-database.test.ts +0 -1102
  271. package/test/devserver-email.test.ts +0 -306
  272. package/test/devserver-pqauth.test.ts +0 -93
  273. package/test/devserver-secrets.test.ts +0 -63
  274. package/test/devserver.test.ts +0 -259
  275. package/test/doctor.test.ts +0 -298
  276. package/test/dom/Image.test.tsx +0 -144
  277. package/test/dom/Link.test.tsx +0 -67
  278. package/test/dom/NavLink.test.tsx +0 -37
  279. package/test/dom/Script.test.tsx +0 -48
  280. package/test/dom/action.test.tsx +0 -146
  281. package/test/dom/error-overlay.test.tsx +0 -44
  282. package/test/dom/loader.test.tsx +0 -121
  283. package/test/dom/navigation.test.ts +0 -59
  284. package/test/dom/revalidate.test.tsx +0 -38
  285. package/test/dom/route-head.test.tsx +0 -56
  286. package/test/dom/router-loading.test.tsx +0 -44
  287. package/test/dom/scroll.test.ts +0 -56
  288. package/test/dom/slot.test.tsx +0 -131
  289. package/test/dom/use-metadata.test.tsx +0 -58
  290. package/test/dom/view-transitions.test.tsx +0 -53
  291. package/test/dynamic-routing.test.ts +0 -132
  292. package/test/email-console.test.ts +0 -86
  293. package/test/email-preview.test.ts +0 -73
  294. package/test/emails.test.ts +0 -58
  295. package/test/errors.test.ts +0 -21
  296. package/test/example-guestbook.test.ts +0 -123
  297. package/test/features.test.ts +0 -149
  298. package/test/fixtures/bignum-wire/spec.ts +0 -25
  299. package/test/fixtures/daemon-app.ts +0 -65
  300. package/test/fixtures/stream-echo.ts +0 -26
  301. package/test/fixtures/stream-gate.ts +0 -28
  302. package/test/fixtures/stream-trap.ts +0 -18
  303. package/test/fixtures/stream-typed.ts +0 -41
  304. package/test/fonts.test.ts +0 -28
  305. package/test/global-setup.ts +0 -17
  306. package/test/head.test.ts +0 -42
  307. package/test/image-blur.test.ts +0 -50
  308. package/test/io.test.ts +0 -117
  309. package/test/metadata.test.ts +0 -40
  310. package/test/navlink.test.ts +0 -28
  311. package/test/pages.test.ts +0 -105
  312. package/test/placeholder.test.ts +0 -9
  313. package/test/pqauth-e2e.test.ts +0 -216
  314. package/test/pqauth-email.test.ts +0 -801
  315. package/test/prerender.test.ts +0 -116
  316. package/test/prettier-plugin.test.ts +0 -46
  317. package/test/routes.test.ts +0 -76
  318. package/test/rpc-bignum-wire.test.ts +0 -164
  319. package/test/rpc-dispatch.test.ts +0 -132
  320. package/test/rpc-kinds.test.ts +0 -18
  321. package/test/rpc.test.ts +0 -66
  322. package/test/search.test.ts +0 -114
  323. package/test/seo.test.ts +0 -175
  324. package/test/slot-layouts.test.ts +0 -69
  325. package/test/sri.test.ts +0 -129
  326. package/test/ssg.test.ts +0 -36
  327. package/test/ssr-hydration.test.tsx +0 -122
  328. package/test/ssr-render.test.ts +0 -197
  329. package/test/ssr-template.test.tsx +0 -482
  330. package/test/stream-emulation.test.ts +0 -479
  331. package/test/ui.test.ts +0 -40
  332. package/test/update.test.ts +0 -58
  333. package/test/validate.test.ts +0 -42
  334. package/test/version-check.test.ts +0 -130
  335. package/tests/data-parity/generated-parity.ts +0 -99
  336. package/tests/data-parity/parity.ts +0 -80
  337. package/tests/data-parity/spec.ts +0 -46
@@ -1,6 +1,6 @@
1
1
  export const TOIL_DOCS = {
2
2
  "README.md": "# toiljs\n\ntoiljs is a full-stack web framework. You write your **frontend in React** and your **backend in\nTypeScript**, and toiljs turns the backend into a tiny, fast **WebAssembly** program that runs at the edge\n(on servers close to your users, all over the world). One language, one project, one deploy.\n\nIf you have used Next.js, this will feel familiar: file-based routes, server code next to client code, a dev\nserver with hot reload. The difference is what happens underneath. Your server code is compiled by\n**toilscript** (a TypeScript-to-WebAssembly compiler) into a sandboxed `.wasm` module, and it runs on the\n**Dacely edge** with a built-in worldwide database (**ToilDB**), streaming, background jobs, and auth all\nincluded.\n\n## The mental model\n\n```mermaid\nflowchart LR\n A[\"Your project<br/>(TypeScript + React)\"] -->|toiljs build| B[\"client bundle<br/>(React, runs in the browser)\"]\n A -->|toilscript compile| C[\"server.wasm<br/>(your backend, sandboxed)\"]\n B --> U[\"User's browser\"]\n C --> E[\"Dacely edge<br/>(worldwide)\"]\n E --> D[(\"ToilDB<br/>global database\")]\n U <-->|HTTP / WebTransport| E\n```\n\n- **client/** is your React app (pages, components, styles). It runs in the browser.\n- **server/** is your backend (routes, database, auth). It compiles to WebAssembly and runs on the edge.\n- **shared/** is the typed bridge: toiljs generates a client here so the browser calls your server with full\n type safety.\n\n## Hello, toiljs\n\n```ts\n// server/routes/Hello.ts (a backend HTTP route)\nimport { Response } from 'toiljs/server/runtime';\n\n@rest('hello')\nclass Hello {\n @get('/')\n public hi(): Response {\n return Response.text('Hello from the edge!\\n');\n }\n}\n```\n\n```tsx\n// client/routes/index.tsx (a frontend page)\nexport default function Home() {\n return <main><h1>Welcome</h1></main>;\n}\n```\n\nRun `toiljs dev`, open the browser, and both are live with hot reload. That is the whole loop.\n\n## Learn toiljs\n\n**Understand toil first**\n- [Understanding toil](./introduction/README.md): what toil is and the one big idea, then\n [why toil and who it is for](./introduction/why-toil.md),\n [the modern stack you get](./introduction/modern-stack.md),\n [how it works](./introduction/how-it-works.md),\n [what makes it hyper-scalable](./introduction/hyperscale.md),\n [how it is distributed](./introduction/distributed.md),\n [toil versus other frameworks](./introduction/vs-other-frameworks.md), and\n [why it is built this way](./introduction/design-principles.md).\n\n**Start here**\n- [Getting started](./getting-started/README.md): install, create a project, project structure, your first\n app, and migrating an existing React app.\n- [The CLI](./cli/README.md): `dev`, `build`, `create`, `doctor`, and every flag.\n- [Deploy](./getting-started/deploy.md): build for production, self-host it, and how the managed edge fits in.\n\n**Build the frontend**\n- [Frontend overview](./frontend/README.md), [Routing](./frontend/routing.md),\n [Navigation](./frontend/navigation.md), [Components](./frontend/components.md),\n [Rendering and SSR](./frontend/rendering.md), [Styling](./frontend/styling.md),\n [Images](./frontend/images.md), [Metadata and SEO](./frontend/metadata.md),\n [Fetching data](./frontend/data-fetching.md), [Scripts](./frontend/scripts.md),\n [Search](./frontend/search.md), [The Toil global (reference)](./frontend/toil-global.md).\n\n**Build the backend**\n- [Backend overview](./backend/README.md), [HTTP routes (`@rest`)](./backend/rest.md),\n [Typed RPC (`@service`/`@remote`)](./backend/rpc.md), [Data types (`@data`)](./backend/data.md).\n\n**The database (ToilDB)**\n- [Database overview and choosing a family](./database/README.md), [Setup (`@database`)](./database/setup.md),\n [Documents](./database/documents.md), [Unique](./database/unique.md), [Counters](./database/counters.md),\n [Events](./database/events.md), [Views and `@derive`](./database/views.md),\n [Membership](./database/membership.md), [Capacity](./database/capacity.md).\n\n**Auth**: [the full auth guide](./auth/README.md) covers post-quantum login, sessions, and `ToilUserId`;\n[customizing the auth emails](./auth/emails.md) shows how to brand the verification, reset, and 2FA mail.\n\n**Realtime and background**\n- [Streams](./realtime/README.md) and [channels](./realtime/channels.md), [Daemons and scheduled\n jobs](./background/daemons.md), [Derived views (`@derive`)](./background/derive.md).\n\n**Platform services**\n- [Caching](./services/caching.md), [Rate limiting](./services/ratelimit.md),\n [Environment and secrets](./services/environment.md), [Email and 2FA](./services/email.md),\n [Analytics](./services/analytics.md), [Crypto](./services/crypto.md), [Cookies](./services/cookies.md),\n [Time](./services/time.md).\n\n**Concepts and reference**\n- [Writing toiljs correctly (AI + human quick rules)](./concepts/ai-guide.md),\n [Compute tiers (L1 to L4)](./concepts/tiers.md), [Types (u64, u256, and friends)](./concepts/types.md),\n [Every decorator](./concepts/decorators.md), [Configuration](./concepts/config.md),\n [Security and SRI](./concepts/security.md).\n",
3
- "auth/configuration.md": "# Configuring auth for production\n\nBuilt-in auth runs locally with **zero configuration**, it falls back to published, insecure DEV secrets\nso `toiljs dev` Just Works. **A deployment MUST replace all three secrets and pin its KEM key.** This page\nis the checklist.\n\n## The secrets\n\nAuth reads these from the tenant environment store (locally, `.env.secrets`; on the edge, the per-host\nsecure env). They resolve **lazily** the first time auth runs, so no startup wiring is needed.\n\n| Key | What it is | Dev fallback |\n| --- | --- | --- |\n| `AUTH_SESSION_SECRET` | HMAC-SHA256 key that signs the session cookie. Must be identical on every edge instance (a cookie minted anywhere must verify everywhere). | a public constant, **anyone can forge a session** |\n| `AUTH_OPRF_SEED` | Master seed for the per-user OPRF salt key. Rotating it invalidates every password (users must re-register). | a hashed public constant |\n| `AUTH_KEM_SK` | The server's ML-KEM-768 **secret** key (hex). Its public half is what the client encapsulates to. | a pinned dev key pair |\n\n```bash\n# .env.secrets (gitignored; mode 0600 on the edge, NEVER under hosts/, NEVER in the .wasm)\nAUTH_SESSION_SECRET=…64 hex chars (32 bytes)…\nAUTH_OPRF_SEED=…64 hex chars (32 bytes)…\nAUTH_KEM_SK=…hex of an ML-KEM-768 secret key…\n```\n\n### Generating them\n\n`AUTH_SESSION_SECRET` and `AUTH_OPRF_SEED` are just 32 random bytes each:\n\n```bash\nnode -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"\n```\n\n`AUTH_KEM_SK` is an ML-KEM-768 key pair. Generate it and keep BOTH halves, the secret goes in the env, the\npublic half is pinned in the client (next section):\n\n```ts\nimport { ml_kem768 } from '@dacely/noble-post-quantum/ml-kem';\nconst { secretKey, publicKey } = ml_kem768.keygen();\nconsole.log('AUTH_KEM_SK =', Buffer.from(secretKey).toString('hex'));\nconsole.log('client serverKemPublicKey =', Buffer.from(publicKey).toString('hex'));\n```\n\n## Pin the client's KEM public key\n\nThe browser must know the server's genuine KEM public key to run the mutual-auth handshake, this is the\nanti-phishing anchor. The `toiljs/client` `Auth` helper ships with the **dev** key pinned, so a deployment\nMUST pass its own:\n\n```ts\nimport { Auth } from 'toiljs/client';\n\nconst SERVER_KEM_PUBLIC_KEY = /* the publicKey bytes from AUTH_KEM_SK */;\n\nawait Auth.login(username, password, { serverKemPublicKey: SERVER_KEM_PUBLIC_KEY });\nawait Auth.register(username, password, { serverKemPublicKey: SERVER_KEM_PUBLIC_KEY });\n```\n\nShip the public key with your client bundle (it's public, safe to embed). If it doesn't match the server's\n`AUTH_KEM_SK`, login's `serverConfirm` check fails and the client aborts.\n\n## Optional: audience & domain\n\nBoth are optional and have sensible defaults; set them for stability across host aliases:\n\n| Key | Meaning | Default |\n| --- | --- | --- |\n| `TOIL_AUTH_AUDIENCE` | The service audience bound into the signed login message. | `\"toil\"` |\n| `TOIL_AUTH_DOMAIN` | The `domain` input of the stable `ToilUserId` (`sha256(pubkey ‖ username ‖ domain)`). | the request `Host` header, else `localhost` |\n\nSet `TOIL_AUTH_DOMAIN` explicitly if your site answers on multiple hostnames, otherwise the same user could\nget different `ToilUserId`s from different aliases. Once users exist, changing it changes everyone's id, so\npick it before launch.\n\n## Email confirmation\n\nTwo plain env vars (tenant-readable, like `PUBLIC_BASE_URL`) control the built-in email-verification flow. They are separate on purpose: sending a verify email at registration is one decision, blocking login until confirmed is another.\n\n| Key | What it does |\n| --- | --- |\n| `AUTH_EMAIL_CONFIRMATION` | Registration emails a one-time confirm link (`/confirm#token=…`) and stores the account **unconfirmed**. Login is **not** blocked: the user can sign in, and you nudge them to verify. Use this for \"verify at signup, but let people in\". |\n| `AUTH_REQUIRE_EMAIL_CONFIRMATION` | The stricter form: does everything above AND **refuses login** until the email is confirmed (a distinguishable status the client surfaces as `EmailNotConfirmedError`, so the UI can prompt \"confirm your email / resend\"). Implies `AUTH_EMAIL_CONFIRMATION`. On the managed edge this is force-set from the per-domain Dacely setting. |\n\nWith neither set, accounts are auto-confirmed at registration and no confirm email is sent. The one-time link is delivered by email; in `toiljs dev` it is drawn in the console (no mailer needed). Both use the standard non-enumerating responses (a generic 200 that never reveals whether an address exists).\n\n## Argon2id strength (known limitation)\n\nThe built-in controller currently uses **demo-light** Argon2id params (32 MiB, 2 iterations, 1 lane) so it\nstays responsive in a browser tab. These are baked into the shipped controller today; **config-driven\ntuning is a planned follow-up.** For a high-value production deployment you should either wait for the\nconfig knob or hand-write your own controller with `≥ 256 MiB / ≥ 3 iterations`. The OPRF still provides the\nprimary offline-attack resistance regardless, but raise these before protecting anything sensitive.\n\nThe client always derives against whatever params the server returns in `/login/start`, so when the config\nknob lands you can raise them server-side with **no client change**.\n\n## Deploy checklist\n\n- [ ] `AUTH_SESSION_SECRET` set (32 random bytes), identical on every edge instance.\n- [ ] `AUTH_OPRF_SEED` set (32 random bytes).\n- [ ] `AUTH_KEM_SK` set (an ML-KEM-768 secret key), and its **public** half pinned in the client via\n `serverKemPublicKey`.\n- [ ] `TOIL_AUTH_DOMAIN` set if you serve multiple hostnames (stable `ToilUserId`).\n- [ ] (Recommended) Argon2id params reviewed for your threat model.\n\nThe CLI doctor warns when `server.auth` is on and the secrets are missing, run it before you ship.\n",
3
+ "auth/configuration.md": "# Configuring auth for production\n\nBuilt-in auth runs locally with **zero configuration**, it falls back to published, insecure DEV secrets\nso `toiljs dev` Just Works. **A deployment MUST replace all three secrets and pin its KEM key.** This page\nis the checklist.\n\n## The secrets\n\nAuth reads these from the tenant environment store (locally, `.env.secrets`; on the edge, the per-host\nsecure env). They resolve **lazily** the first time auth runs, so no startup wiring is needed.\n\n| Key | What it is | Dev fallback |\n| --- | --- | --- |\n| `AUTH_SESSION_SECRET` | HMAC-SHA256 key that signs the session cookie. Must be identical on every edge instance (a cookie minted anywhere must verify everywhere). | a public constant, **anyone can forge a session** |\n| `AUTH_OPRF_SEED` | Master seed for the per-user OPRF salt key. Rotating it invalidates every password (users must re-register). | a hashed public constant |\n| `AUTH_KEM_SK` | The server's ML-KEM-768 **secret** key (hex). Its public half is what the client encapsulates to. | a pinned dev key pair |\n\n```bash\n# .env.secrets (gitignored; mode 0600 on the edge, NEVER under hosts/, NEVER in the .wasm)\nAUTH_SESSION_SECRET=…64 hex chars (32 bytes)…\nAUTH_OPRF_SEED=…64 hex chars (32 bytes)…\nAUTH_KEM_SK=…hex of an ML-KEM-768 secret key…\n```\n\n### Generating them\n\n`AUTH_SESSION_SECRET` and `AUTH_OPRF_SEED` are just 32 random bytes each:\n\n```bash\nnode -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"\n```\n\n`AUTH_KEM_SK` is an ML-KEM-768 key pair. Generate it and keep BOTH halves, the secret goes in the env, the\npublic half is pinned in the client (next section):\n\n```ts\nimport { ml_kem768 } from '@dacely/noble-post-quantum/ml-kem';\nconst { secretKey, publicKey } = ml_kem768.keygen();\nconsole.log('AUTH_KEM_SK =', Buffer.from(secretKey).toString('hex'));\nconsole.log('client serverKemPublicKey =', Buffer.from(publicKey).toString('hex'));\n```\n\n## Pin the client's KEM public key\n\nThe browser must know the server's genuine KEM public key to run the mutual-auth handshake, this is the\nanti-phishing anchor. The `toiljs/client` `Auth` helper ships with the **dev** key pinned, so a deployment\nMUST pass its own:\n\n```ts\nimport { Auth } from 'toiljs/client';\n\nconst SERVER_KEM_PUBLIC_KEY = /* the publicKey bytes from AUTH_KEM_SK */;\n\nawait Auth.login(username, password, { serverKemPublicKey: SERVER_KEM_PUBLIC_KEY });\nawait Auth.register(username, password, { serverKemPublicKey: SERVER_KEM_PUBLIC_KEY });\n```\n\nShip the public key with your client bundle (it's public, safe to embed). If it doesn't match the server's\n`AUTH_KEM_SK`, login's `serverConfirm` check fails and the client aborts.\n\n## Optional: audience & domain\n\nBoth are optional and have sensible defaults; set them for stability across host aliases:\n\n| Key | Meaning | Default |\n| --- | --- | --- |\n| `TOIL_AUTH_AUDIENCE` | The service audience bound into the signed login message. | `\"toil\"` |\n| `TOIL_AUTH_DOMAIN` | The `domain` input of the stable `ToilUserId` (`sha256(pubkey ‖ username ‖ domain)`). | the request `Host` header, else `localhost` |\n\nSet `TOIL_AUTH_DOMAIN` explicitly if your site answers on multiple hostnames, otherwise the same user could\nget different `ToilUserId`s from different aliases. Once users exist, changing it changes everyone's id, so\npick it before launch.\n\n## Email confirmation\n\nTwo plain env vars (tenant-readable, like `PUBLIC_BASE_URL`) control the built-in email-verification flow. They are separate on purpose: sending a verify email at registration is one decision, blocking login until confirmed is another.\n\n| Key | What it does |\n| --- | --- |\n| `AUTH_EMAIL_CONFIRMATION` | Registration emails a one-time confirm link (`/confirm#token=…`) and stores the account **unconfirmed**. Login is **not** blocked: the user can sign in, and you nudge them to verify. Use this for \"verify at signup, but let people in\". |\n| `AUTH_REQUIRE_EMAIL_CONFIRMATION` | The stricter form: does everything above AND **refuses login** until the email is confirmed (a distinguishable status the client surfaces as `EmailNotConfirmedError`, so the UI can prompt \"confirm your email / resend\"). Implies `AUTH_EMAIL_CONFIRMATION`. On the managed edge this is force-set from the per-domain Dacely setting. |\n\nWith neither set, accounts are auto-confirmed at registration and no confirm email is sent. The one-time link is delivered by email; in `toiljs dev` it is drawn in the console (no mailer needed). Both use the standard non-enumerating responses (a generic 200 that never reveals whether an address exists).\n\n## Argon2id strength (known limitation)\n\nThe built-in controller currently uses **demo-light** Argon2id params (32 MiB, 2 iterations, 1 lane) so it\nstays responsive in a browser tab. These are baked into the shipped controller today; **config-driven\ntuning is a planned follow-up.** For a high-value production deployment you should either wait for the\nconfig knob or hand-write your own controller with `≥ 256 MiB / ≥ 3 iterations`. The OPRF still provides the\nprimary offline-attack resistance regardless, but raise these before protecting anything sensitive.\n\nThe client always derives against whatever params the server returns in `/login/start`, so when the config\nknob lands you can raise them server-side with **no client change**.\n\n## Deploy checklist\n\n- [ ] `AUTH_SESSION_SECRET` set (32 random bytes), identical on every edge instance.\n- [ ] `AUTH_OPRF_SEED` set (32 random bytes).\n- [ ] `AUTH_KEM_SK` set (an ML-KEM-768 secret key), and its **public** half pinned in the client via\n `serverKemPublicKey`.\n- [ ] `TOIL_AUTH_DOMAIN` set if you serve multiple hostnames (stable `ToilUserId`).\n- [ ] (Recommended) Argon2id params reviewed for your threat model.\n\nThe CLI doctor warns when `server.auth` is on and the secrets are missing, run it before you ship.\n\n## Upgrading to 0.0.118\n\nLogin now requires `AuthOptions.serverKemPublicKey`, the 1184-byte public key matching\n`AUTH_KEM_SK`. There is no implicit fallback to the published development key. Keep\nexisting OPRF and KEM secrets when upgrading; replacing the OPRF seed invalidates\nexisting password credentials. Project-root `VITE_*` variables are loaded by default.\n\nSessions use a versioned signed payload with a random session identifier and a user\nrevocation generation. Pre-upgrade session cookies and pending login challenges are\nrejected, so users must sign in again. Single-session logout revokes the signed session;\n`POST /auth/logout-all`, password reset, and confirmed two-factor changes revoke all\nsessions. Two-factor changes require a fresh login after success. Existing account IDs\nremain stable after password reset. The compiler includes the new revocation and factor\nsettings database declarations automatically.\n\nAuthenticated factor operations accept `headers` and `onRequestFailed(status)` options\nfor application account guards. HTTP failures include their status in the client error.\n",
4
4
  "auth/emails.md": "# Customizing the auth emails\n\nBuilt-in auth sends three transactional emails: the email-verification link, the password-reset link, and the two-factor code. Each ships with a plain, working default. You can replace any of them with your own branded template, and you never touch the auth code to do it.\n\n## The three emails\n\n| Email | When it sends | The value it gives your template |\n| --- | --- | --- |\n| Email verification | On register, when email confirmation is on | `link` (the confirm URL) |\n| Password reset | On `POST /auth/reset/request` | `link` (the reset URL) |\n| Two-factor code | On login or 2FA setup, for the email method | `code` (the numeric code) and `action` (the flow phrase) |\n\nThe defaults are intentionally minimal so a fresh project sends real, correct mail with no setup. To match your brand, override them.\n\n## How to override\n\nDrop a React template in your project's `emails/` folder with the reserved name for that email. If the file exists, the build uses it; if not, the default stands. This is the same `emails/*.tsx` system you use for your own mail (see [Email](../services/email.md)), so you get full markup and inline CSS.\n\n| Reserved file | Overrides | Prop it must read |\n| --- | --- | --- |\n| `emails/auth-confirm.tsx` | Email verification | `link` |\n| `emails/auth-reset.tsx` | Password reset | `link` |\n| `emails/auth-2fa.tsx` | Two-factor code | `code`, `action` |\n\nEach template is a default-exported React component. Read the one prop it is given, and it becomes a `{{token}}` hole the edge fills per send. Reading any other prop renders empty, so the build warns you.\n\n```tsx\n// emails/auth-confirm.tsx -> overrides the email-verification message.\nexport const subject = 'Verify your Acme account';\n\nexport default function AuthConfirm({ link }: { link: string }) {\n return (\n <div style={{ fontFamily: 'system-ui', padding: 24 }}>\n <h1 style={{ color: '#cb9820' }}>Welcome to Acme</h1>\n <p>Tap below to verify your email and finish signing up.</p>\n <p>\n <a\n href={link}\n style={{ background: '#cb9820', color: '#fff', padding: '10px 18px', borderRadius: 8, textDecoration: 'none' }}\n >\n Verify my email\n </a>\n </p>\n <p style={{ color: '#666', fontSize: 13 }}>Or paste this link: {link}</p>\n </div>\n );\n}\n```\n\nThe reset template works the same way with the `link` prop.\n\n## The subject line\n\nFor verification and reset, set the subject with `export const subject = '...'`. Leave it out and the default subject is used (`Confirm your account`, `Reset your password`).\n\nThe two-factor email is different. It serves three flows (signing in, enabling 2FA, and turning 2FA off), so besides the `code` it also gets an `action` prop: the framework fills it with the flow phrase, `sign in`, `enable two-factor authentication`, or `turn off two-factor authentication`. Write it into a sentence like \"Enter this code to {action}.\" and the one template reads correctly for every flow. The subject is likewise set by the framework and stays contextual, so a `subject` export in this file is ignored.\n\n```tsx\n// emails/auth-2fa.tsx -> overrides the 2FA code message (subject is contextual).\nexport default function Auth2fa({ code, action }: { code: string; action: string }) {\n return (\n <div style={{ fontFamily: 'system-ui', padding: 24, textAlign: 'center' }}>\n <p>Your Acme verification code:</p>\n <p style={{ fontSize: 32, fontWeight: 700, letterSpacing: 6 }}>{code}</p>\n <p>Enter it to {action}.</p>\n <p style={{ color: '#666' }}>It expires in a few minutes.</p>\n </div>\n );\n}\n```\n\n## What you can and cannot put in\n\nEach template is rendered once, at build time, into a static string of HTML with your styles inlined, and the prop becomes a fill hole. So a fixed layout and inline styles work, but per-send logic does not: a `{items.map(...)}` or a conditional runs at build, not per email. For these three messages that is all you need, one link or one code.\n\nThe only values auth hands each template are the ones in the table above (`link`, or `code` plus `action` for 2FA). There is no user name, no app name, no expiry value. Write those into the template text directly.\n\nStyling rules are the same as any toiljs email: write inline `style={{ ... }}`, or import a stylesheet and its rules are inlined for you. A bare CSS import with no inlining has no effect in an email client. See [Email](../services/email.md) for the details.\n\n## Delivery stays safe\n\nYou cannot weaken the security of these flows by overriding a template. Every auth email is sent detached (fire-and-forget), so the response time never reveals whether an address maps to an account. That holds whether the message is the default or your override. Overriding only changes what the email looks like, never how it is sent.\n\n## Where the templates go\n\nThere is nothing to import and nothing to register. The build reads `emails/auth-*.tsx`, bakes the effective templates into the compiled server, and the auth controller sends them. Changing a template and rebuilding (or saving under `toiljs dev`) is all it takes.\n\n## Related\n\n- [Email](../services/email.md): the full `emails/*.tsx` system, `EmailService`, and `EmailTemplate`.\n- [Configuration](./configuration.md): the auth secrets and the email provider a deployment must set.\n- [Usage](./usage.md): enabling auth, the client API, and guarding routes.\n",
5
5
  "auth/extending.md": "# Extending & integrating auth\n\nBuilt-in auth is deliberately opinionated so the common case is one line. This page covers the identity\nyou build ON, `ToilUserId`, and how to go beyond the defaults: keying your own data on a user, a custom\nuser shape, and hand-writing auth from the same primitives.\n\n## `ToilUserId`\n\nThe stable, tenant-scoped user identity: `sha256(mldsaPublicKey ‖ identifier ‖ domain)`, a 256-bit value.\nIt's a **global** (no import), like `crypto`.\n\n```ts\n// Read the current user's id in any handler (gate on hasSession(), see the null note below).\nconst id: ToilUserId = AuthService.userId()!;\n\n// Or derive one yourself.\nconst id2 = ToilUserId.derive(mldsaPublicKey, 'alice@example.com', 'acme.dacely.com');\n```\n\n| Member | Description |\n| --- | --- |\n| `ToilUserId.derive(pk, identifier, domain)` | Derive from an ML-DSA public key + email/username + tenant domain. Deterministic. |\n| `ToilUserId.fromBytes(b)` | Rebuild from a 32-byte digest (from `toBytes()` or storage). |\n| `toBytes(): Uint8Array` | The 32 identity bytes. |\n| `toHex(): string` | Lowercase 64-char hex, a convenient string key. |\n| `isZero(): bool` | True for the unset / anonymous id. |\n| `equals(other): bool` | Value equality. |\n| `a == b` / `a != b` | Overloaded value comparison, **O(1)** (four `u64` word compares, no byte loop, no allocation). |\n\n```ts\nconst a = ToilUserId.derive(pk, 'alice', 'acme.com');\nconst b = ToilUserId.derive(pk, 'alice', 'acme.com');\nconst c = ToilUserId.derive(pk, 'bob', 'acme.com');\na == b; // true, same inputs, same id\na != c; // true, different user\n```\n\n> **Null-check gotcha:** because `ToilUserId` overloads `==`, `AuthService.userId() == null` does NOT\n> type-check (`==` expects a `ToilUserId`). Gate with `AuthService.hasSession()` and then `userId()!`, or\n> compare with `getUser()` (a plain nullable). `===` is reference identity in AssemblyScript and is not\n> overloadable, use `==` for value equality.\n\n## Keying your own data on the user\n\n`toilUserId` is the right key for per-user data, it's stable across sessions/devices and opaque. Use the\nhex as a string key, or the bytes in a `@data` key class:\n\n```ts\n@data\nclass UserKey {\n id: Uint8Array = new Uint8Array(0); // toilUserId bytes\n constructor(id: Uint8Array = new Uint8Array(0)) { this.id = id; }\n}\n\n@data class Profile { displayName: string = ''; bio: string = ''; }\n\n@database\nclass AppDb {\n @collection static profiles: Documents<UserKey, Profile>;\n}\n\n@rest('profile')\nclass ProfileApi {\n @auth\n @post('/')\n public save(ctx: RouteContext): Response {\n const key = new UserKey(AuthService.userId()!.toBytes());\n const p = Profile.decode(ctx.request.body);\n // Save this user's profile. create is insert-only, so the first save creates\n // the record and later saves overwrite the existing one with enqueue.\n if (!AppDb.profiles.create(key, p)) {\n AppDb.profiles.enqueue(key, p);\n }\n return Response.text('saved\\n');\n }\n}\n```\n\n## Extending the user: add your own fields\n\nBuilt-in auth reserves exactly two fields on the authenticated user: `toilUserId` and `username`. To carry\nmore (a role, a display name, a tenant), just declare your OWN `@user` in your server code while\n`server.auth` is on. The build detects it and **extends** it: it injects the reserved `toilUserId` +\n`username` as the first two fields and mints sessions for your shape automatically. You do not opt out of\nanything, and there is still exactly one `@user` per program.\n\n```ts\n@user\nclass Account {\n admin: bool = false;\n displayName: string = '';\n tenant: string = '';\n // toilUserId + username are INJECTED by the build; do not declare them here.\n}\n```\n\nRules:\n\n- Do **not** declare `toilUserId` or `username` yourself. They are reserved and injected; declaring either is\n a compile error (`'username' is reserved by built-in auth`).\n- Your `@user` must be default-constructible (give every field an initializer), like any `@data` class.\n\n`AuthService.getUser()` is now typed to YOUR shape:\n\n```ts\nconst user = AuthService.getUser(); // { toilUserId, username, admin, displayName, tenant } | null\n```\n\n**Populating your fields.** Login fills `toilUserId` + `username`; your extra fields start at their declared\ndefaults (`admin = false`, and so on). The built-in controller cannot know your business fields, so you set\nthem yourself on one of your own `@auth` routes by reading the user, updating it, and re-minting the session:\n\n```ts\n@rest('account')\nclass AccountApi {\n @auth @post('/promote')\n public promote(): Response {\n const user = AuthService.getUser()!;\n user.admin = true;\n const resp = Response.text('promoted\\n');\n resp.setCookie(AuthService.mintSession(user.encode())); // re-sign the session with the new fields\n resp.setCookie(AuthService.userCookie(user.encode())); // update the readable companion cookie\n return resp;\n }\n}\n```\n\nIf you would rather hand-write the whole controller instead, you still can: do **not** enable `server.auth`,\nand build your own `@user` + routes from the [AuthService primitives](#the-authservice-primitive-reference)\nbelow. But for most apps, extending is all you need.\n\n## Adding email verification / 2FA\n\nLayer a second factor on top of the session with `TwoFactor` (stateless email codes, no DB; see\n[email](../services/email.md)). Typical flow: after login, require a verified email before granting access to\nsensitive routes.\n\n```ts\n@rest('2fa')\nclass TwoFactorApi {\n // Step 1: email a code to the logged-in user, hand back the signed token.\n @auth @post('/send')\n public send(): Response {\n const email = /* the user's email, e.g. their username, or a stored profile field */;\n const ch = TwoFactor.send(email, 'login'); // emails the code, returns { token, status }\n return Response.bytes(new DataWriter().writeString(ch.token).toBytes());\n }\n\n // Step 2: verify the code the user typed against the token.\n @auth @post('/verify')\n public verify(ctx: RouteContext): Response {\n const r = new DataReader(ctx.request.body);\n const token = r.readString(); const email = r.readString(); const code = r.readString();\n if (!TwoFactor.verify(token, email, code)) return Response.text('bad code\\n', 401);\n // Mark this session 2FA-verified: re-mint the session with a flag in your own @user, or store a\n // per-user \"verified\" record keyed on AuthService.userId().\n return Response.text('verified\\n');\n }\n}\n```\n\n`TwoFactor` gives integrity + expiry but not single-use (a code re-verifies within its TTL); keep the TTL\nshort. For a branded email, use `TwoFactor.issue(...)` (returns the code without sending) + your own\n`Emails.*` template. Call `TwoFactor.setSecret(...)` once at startup in production.\n\n## The `AuthService` primitive reference\n\nEverything the built-in controller is built from, available for hand-written auth. All are ambient globals\n(no import).\n\n**Sessions & cookies**\n- `mintSession(userData: Uint8Array, ttlSecs?: u64): Cookie`: the signed `__Host-toil_sess` cookie.\n- `userCookie(userData, ttlSecs?): Cookie`: the readable `__Secure-toil_user` companion.\n- `clearSession(): Cookie` / `clearUserCookie(): Cookie`.\n- `hasSession(): bool`: the `@auth` predicate.\n- `getSessionBytes(): Uint8Array | null`: the verified `@user` payload bytes.\n- `getUser(): <your @user> | null`: decoded, typed to your `@user`.\n- `userId(): ToilUserId | null`: the stable id (built-in `@user` layout).\n- `setSecret(secret: Uint8Array)`: override the session HMAC key programmatically.\n\n**Post-quantum login crypto**\n- `oprfEvaluate(username, blinded): Uint8Array`: server-keyed OPRF eval.\n- `buildRegisterMessage(username, pk)` / `verifyRegister(pk, msg, sig): bool`: proof-of-possession.\n- `buildLoginMessage(sub, aud, cid, nonce, iat, exp, ct, memKiB, iterations, parallelism, serverKemKeyId)`\n / `verifyLogin(pk, msg, sig): bool`.\n- `mlkemDecapsulate(ct): Uint8Array`, `serverKemKeyId(): Uint8Array`.\n- `deriveSessionKey(sharedSecret, transcriptHash)`, `serverConfirmTag(sessionKey, transcriptHash)`: the\n mutual-auth confirmation.\n- `sha256(data): Uint8Array`.\n- `setOprfSeed(seed)`, `setServerKemSecretKey(sk)`, `setServerKemPublicKey(pk)`: override the seeds/keys.\n- Sizes: `PUBLIC_KEY_LEN`, `SIGNATURE_LEN`, `OPRF_ELEMENT_LEN`, `KEM_CIPHERTEXT_LEN`, `SHARED_SECRET_LEN`, …\n\n**Related globals**, `TwoFactor` (email codes; see [email](../services/email.md)), `RateLimitService` (used by\n`@ratelimit`), `Environment` (the secret store).\n\n## Two ways in, one behavior\n\nThe config flag and the import are identical at build time, both make the build append the shipped\n`@user` + `@rest('auth')` controller to the toilscript **entry** set, where their decorators weave and the\n`@rest` class self-mounts. (A framework decorator source only weaves as an entry; that's why a plain\n`import` of the controller isn't enough on its own, the marker import is detected by the build, which does\nthe entry injection.) This is why built-in auth needs no runtime registration call.\n",
6
6
  "auth/how-it-works.md": "# How Toil auth works\n\nToil auth is a **post-quantum, password-based, mutually-authenticated** login. The design goal: the user\ntypes a password, but the server never sees it, never stores it, and can't be phished into leaking a\nverifier, and every primitive is quantum-resistant.\n\nThis page explains the protocol. You do not need to understand it to use auth (see [Usage](./usage.md)),\nbut you should understand the guarantees before you ship.\n\n## The building blocks\n\n| Primitive | Role |\n| --- | --- |\n| **OPRF** (RFC 9497, ristretto255-SHA512) | A *server-keyed* salt. The client blinds its password, the server evaluates it under a per-user key, the client unblinds. The result can't be computed without the server, so a stolen database can't be brute-forced offline. |\n| **Argon2id** | Stretches the OPRF output into key material (memory-hard, GPU/ASIC-resistant). Runs in the browser. |\n| **ML-DSA-44** (FIPS 204) | The user's identity key pair is *derived* from the Argon2id output. The server stores only the **public** key. Login is proved by an ML-DSA signature. |\n| **ML-KEM-768** (FIPS 203) | Key encapsulation on login: the server proves it holds its KEM secret by returning a confirmation tag only derivable from the decapsulated shared secret → **mutual** auth (anti-phishing). |\n| **HMAC-SHA256** | Signs the session cookie (`AUTH_SESSION_SECRET`). |\n\n> The whole thing is sometimes called an *aPAKE* (asymmetric password-authenticated key exchange). Do not\n> call it OPAQUE, this is Toil's own OPRF + KEM + signature construction.\n\n## What the server stores\n\nPer account, in ToilDB (`@database AuthDb`):\n\n- `username`, a deterministic `salt`, the **ML-DSA public key**, and the Argon2id params.\n\nThat's it. **No password, no password hash, no verifier that can be brute-forced without the OPRF key.**\nLogin challenges are a second collection, consumed exactly once (atomic `getDelete`).\n\n## Registration\n\n```\n Browser (client) Toil edge (server)\n ───────────────── ──────────────────\n blind(password) OPRF key = f(seed, username)\n │ username, blinded │\n │ ───────── POST /auth/register/start ───────────► │\n │ │ evaluated = OPRF(blinded)\n │ ◄──── {mem,iters,par, salt, evaluated} ───────── │ (salt is deterministic per user)\n │ │\n unblind → oprfOut │\n seed = Argon2id(oprfOut, salt, params) │\n (pk, sk) = ML-DSA-44.keygen(seed) │\n proof = ML-DSA.sign(sk, \"register|username|pk\") │\n │ username, pk, proof │\n │ ───────── POST /auth/register/finish ──────────► │\n │ │ verifyRegister(pk, msg, proof) ✓ proof-of-possession\n │ ◄──────────── {status: 0 ok | 1 taken} ──────── │ store AuthAccount{username, salt, pk, params}\n```\n\nThe server never learns the password or the secret key `sk`, only the public key `pk` and a proof the\nclient holds the matching `sk`. A duplicate username returns a **distinguishable** `status = 1` (so the UI\ncan say \"taken, log in instead\"); everything else fails generically.\n\n## Login (with mutual auth)\n\n```\n Browser (client) Toil edge (server)\n ───────────────── ──────────────────\n blind(password) │\n │ username, blinded │\n │ ───────── POST /auth/login/start ──────────────► │ evaluated = OPRF(blinded) (ALWAYS, even unknown user)\n │ │ if known: store Challenge{cid, nonce, iat, exp}\n │ ◄─ {cid, aud, params, salt, nonce, iat, exp, ── │ (identical response whether the user exists or not)\n │ evaluated} │\n unblind → Argon2id → (pk, sk) = ML-DSA.keygen │\n (ct, ssС) = ML-KEM-768.encapsulate(serverKemPublicKey) │\n msg = \"login|username|aud|cid|nonce|iat|exp|ct|params|kid\" │\n sig = ML-DSA.sign(sk, msg) │\n │ cid, ct, sig │\n │ ───────── POST /auth/login/finish ─────────────► │ ch = challenges.getDelete(cid) (consume once; check exp)\n │ │ rebuild msg from OUR stored values + ct\n │ │ verifyLogin(acct.pk, msg, sig) ✓ it's really this user\n │ │ ssS = ML-KEM.decapsulate(ct) ✓ we hold the KEM key\n │ │ K = deriveSessionKey(ssS, H(msg))\n │ ◄──── {0, sessionToken, serverConfirm} ──────── │ serverConfirm = tag(K, H(msg))\n │ + Set-Cookie: __Host-toil_sess=… │ mint session cookie\n verify serverConfirm using ssC ✓ the server is genuine │\n```\n\nTwo verifications, both required:\n1. **The server verifies the client**: the ML-DSA signature over a message bound to the challenge, the\n Argon2id params, and the server's KEM key id. Replays fail (the challenge is consumed).\n2. **The client verifies the server**: the `serverConfirm` tag is derivable only from the ML-KEM shared\n secret, which only the holder of the KEM secret key can decapsulate. A phishing site can't forge it.\n\n## Anti-enumeration\n\n`/login/start` behaves identically for a known and an unknown user: it always runs the OPRF (a decoy key\nfor unknown users), returns a **deterministic** per-user salt and constant params, and a fresh challenge.\nThe challenge is persisted only for a real account, and `/login/finish` fails generically at consume for an\nunknown user. So an attacker can't probe which usernames exist. Every failure path returns the same\n`401 auth: request failed`.\n\n## Sessions & cookies\n\nOn successful login the server mints **two** cookies:\n\n- **`__Host-toil_sess`**: the authoritative session. HMAC-SHA256 signed with `AUTH_SESSION_SECRET`,\n `HttpOnly`, `Secure`, `SameSite=Lax`. It carries the `@user` codec payload. `@auth` and\n `AuthService.getUser()` open + verify this cookie server-side; a forged or tampered cookie fails.\n- **`__Secure-toil_user`**: a **readable** companion carrying the same payload, so the browser can show\n \"logged in as …\" via the client's `getUser()`. The server **never trusts it**, it is display-only.\n\nEach request runs in a fresh wasm instance, but the signed cookie is self-contained, so no server-side\nsession store is needed. `AUTH_SESSION_SECRET` must be identical across every edge instance (it is, via the\nenv store) so a cookie minted anywhere verifies everywhere.\n\n## The stable user identity: `ToilUserId`\n\nAt login the server derives a stable, tenant-scoped id and stores it in the session (the first field of the\nbuilt-in `@user`):\n\n```\ntoilUserId = SHA-256( mldsaPublicKey ‖ username ‖ domain ) // 256 bits\n```\n\n- **Stable:** same login key + username on the same tenant `domain` → same id, forever, across sessions\n and devices. Key your own data on it.\n- **Opaque + one-way:** it's a hash: safe to store, log, or expose without leaking the key or the address.\n- Read it anywhere with `AuthService.userId()`. See [Extending](./extending.md#toiluserid) for the\n `ToilUserId` API (O(1) `==` / `!=`, `toHex()`, …).\n\n## Threat-model summary\n\n- **Server database stolen** → attacker gets public keys + salts, not passwords. Brute-forcing needs the\n OPRF key (server-side) *and* Argon2id work per guess.\n- **Server compromised / malicious** → still can't recover passwords (only public keys) and can't forge a\n past session without `AUTH_SESSION_SECRET`.\n- **Phishing site** → can't produce the `serverConfirm` tag (no KEM secret), so a correct client aborts.\n- **Quantum adversary** → ML-DSA + ML-KEM are post-quantum; the OPRF/Argon2id/HMAC pieces are classical but\n not the long-term identity or key-exchange.\n- **Replay** → challenges are single-use (`getDelete`) with a short TTL.\n\nResidual responsibilities are yours: set the [secrets](./configuration.md), pin your deployment's KEM\npublic key in the client, and raise the Argon2id params for production.\n",
@@ -13,7 +13,7 @@ export const TOIL_DOCS = {
13
13
  "background/daemons.md": "# Daemons (`@daemon` / `@scheduled`)\n\nA `@daemon` is a single, long-lived background worker for your whole app. You mark a class `@daemon`, add `@scheduled` methods that fire on a timer, and the Dacely edge keeps exactly **one** copy of it running worldwide, restarting it elsewhere if the machine it is on fails.\n\n## What a daemon is\n\nThe word \"daemon\" (say \"DEE-mon\") is an old computing term for a program that runs quietly in the background, not tied to any single user. That is exactly what this is.\n\nCompare the three ways your server code can run:\n\n| Kind | How many run | Lives for |\n| ------------------------------- | ------------------------------------ | ----------------------------- |\n| [Request handler](../backend/rest.md) (`@rest`) | a fresh one per request | one request |\n| [Stream box](../realtime/streams.md) (`@stream`) | one per open connection | one connection |\n| **Daemon** (`@daemon`) | **exactly one for the whole app** | as long as it holds the lease |\n\nBecause it is a single, resident instance, its fields persist across scheduled runs (a request handler forgets everything after each request; a daemon does not). It is the right home for work that must happen **once globally on a cadence**, not once per user and not once per server.\n\n```ts\n@daemon\nclass Jobs {\n @scheduled('1h')\n hourly(): void {\n // Runs once an hour, on the one elected worker. Put recurring background\n // work here: rollups, cleanup, polling an upstream, and so on.\n }\n}\n```\n\n## `@scheduled`: run on a cadence\n\nA `@scheduled` method fires on a schedule. The single string argument is the cadence, and it comes in two flavours.\n\n### Interval schedules\n\nAn **interval** fires every fixed span of time. Write a number followed by a unit: `s` (seconds), `m` (minutes), `h` (hours), or `d` (days). The number must be at least 1, and the span may not exceed 7 days.\n\n```ts\n@scheduled('30s') everyHalfMinute(): void { /* ... */ }\n@scheduled('5m') everyFiveMinutes(): void { /* ... */ }\n@scheduled('1h') hourly(): void { /* ... */ }\n@scheduled('1d') daily(): void { /* ... */ }\n```\n\n### Cron schedules\n\nA **cron** expression fires at wall-clock times (\"every weekday at 9:15\", \"midnight on the first of the month\"). Use it when you care about the actual time of day, not just a repeating gap. A cron spec is five fields separated by spaces, in this order:\n\n```\nminute hour day-of-month month day-of-week\n```\n\ntoiljs recognises a cron spec by the spaces in it (an interval has none).\n\n```ts\n@scheduled('15 9 * * 1-5') // 09:15, Monday to Friday\nweekdayMorning(): void { /* ... */ }\n\n@scheduled('0 0 1 * *') // 00:00 on the 1st of every month\nmonthlyReset(): void { /* ... */ }\n```\n\nCron times are evaluated in **UTC** and are **minute-granular** (the smallest cron step is one minute). A `*` means \"every value\" for that field.\n\n### Rules\n\n- A `@scheduled` method takes **no arguments and returns `void`**.\n- A daemon class may have **several** `@scheduled` methods, each on its own cadence.\n- Because only the one elected worker fires them, a task runs **once per tick for the whole app**, never once per server.\n\n### `onStart`: run once at boot\n\nA daemon may also declare a plain `onStart(): void` method (not decorated). It runs **once**, when the daemon box first starts on the elected worker. Use it to set up state or kick off a long-running loop.\n\n```ts\n@daemon\nclass Jobs {\n onStart(): void {\n // one-time setup when this daemon becomes active\n }\n\n @scheduled('1h')\n hourly(): void { /* ... */ }\n}\n```\n\n## No backfill: missed runs are skipped, not replayed\n\nThis is the single most important thing to understand about scheduling.\n\nIf the daemon is down when a tick was due (say the leader failed and a standby is still taking over), or if the clock jumps forward, toiljs does **not** go back and run all the ticks you missed. It simply fires the **next** due run and moves on. This is called a **no-backfill** policy.\n\nTwo practical consequences:\n\n1. **Design tasks to be safe to skip.** \"Recompute the summary\" is fine to miss (the next run fixes it). \"Charge every user once\" is not, unless you make it idempotent.\n2. **Make tasks idempotent where a missed run matters.** Idempotent means running it twice (or catching up later) has the same effect as running it once. For example, \"set yesterday's total to X\" is idempotent; \"add 1 to a counter\" is not.\n\n## One global worker, with safe failover\n\nThere is exactly **one** daemon running for your app at any moment. A second machine keeps a **warm standby** ready but idle. If the active worker's hold on the job expires (it crashed, lost the network, or was shut down), the standby takes over and fires the following runs.\n\nThe mechanism is a **lease**. Think of the lease as a \"who is in charge\" token that only one worker can hold at a time, and that has to be renewed to keep. Only the worker holding the lease (the **leader**) runs the schedule.\n\n```mermaid\nsequenceDiagram\n participant A as Worker A (leader)\n participant L as Lease\n participant B as Worker B (standby)\n A->>L: hold the lease, renew it\n Note over A: A fires @scheduled ticks\n Note over B: B stays idle, watching\n A--xL: A crashes, stops renewing\n Note over L: lease expires\n B->>L: acquire the lease\n Note over B: B is now leader, fires the next ticks\n```\n\nThe important guarantee: **two workers never run the same tick at the same time.** This is called **at-most-once** scheduling. The trade-off is the no-backfill behaviour above: to be sure a tick is never run twice, the edge would rather skip the in-flight tick when a leader is lost than risk running it on two machines. You never start, stop, or place the daemon yourself; the edge elects the leader and drives it.\n\n## Leadership fencing: side effects only run on the leader\n\nA subtle risk with a warm standby is a \"split brain\": for a brief moment, two workers might both think they are the leader. To make that harmless, toiljs **fences** every side effect behind a leadership check. A **side effect** is anything that changes the outside world:\n\n- **Database writes** (creating, patching, deleting rows, adding to counters, appending events, publishing views).\n- **Outbound HTTP calls** (`http_call`, described below).\n\nThese run **only** on the confirmed leader. If code that is not the leader tries one, the edge refuses it (a \"not leader\" error) rather than let it happen twice. Plain **reads** and computation are not fenced (they are safe to do anywhere). So even if two workers briefly overlap, only one of them can actually write or call out. You do not write the fencing yourself; it is automatic. The upshot: put your writes and outbound calls in `@scheduled` methods freely, and trust that they happen once.\n\n## `daemon.*` host calls\n\nA daemon has a small set of host abilities beyond ordinary computation:\n\n- **Database access.** A daemon reads and writes [ToilDB](../database/README.md) with the same collection handles you use in a route or a derive (`.get`, `.add`, `.append`, `.publish`, and so on). Writes are leader-fenced as described above.\n- **Outbound HTTP (`http_call`).** A daemon can call an external service (to poll an API, post to a webhook). This is the one place your server code reaches out to the internet, so it is deliberately restricted:\n - It is **leader-only** (fenced, like any side effect).\n - It is **SSRF-bounded**. SSRF (server-side request forgery) is an attack where code is tricked into calling internal addresses it should not reach. The edge resolves the target host and blocks private or internal addresses, so a daemon cannot use `http_call` to poke around inside the network.\n - It is **metered**: making many calls or pulling huge responses costs budget, which caps abuse.\n- **Leadership info** (`is_leader`, `current_epoch`). A daemon can check whether it is currently the leader, which is useful for guarding a long `onStart` loop.\n\n> **Note:** In `toiljs dev` (the single-process local emulator), the daemon is always the leader (there is nothing to fail over to), and `http_call` is stubbed to return a \"call failed\" result rather than make real network requests. Everything else, including the schedule and your database writes, runs exactly as it does on the edge.\n\n### The `Daemon` global\n\nInside a `@daemon` class, `Daemon` is an ambient global (no import), like `Analytics`. It needs `toilscript` 0.1.60 or newer.\n\n| Member | Description |\n| --- | --- |\n| `Daemon.isLeader(): bool` | Whether this worker currently holds the lease. A snapshot: re-check it inside a long task rather than trusting the value it started with. |\n| `Daemon.epoch(): i64` | The monotonic fencing token, bumped on every (re)acquire, or `-1` when this worker is not the leader. Stamp it into work that must not outlive the lease that authorized it. |\n| `Daemon.taskCount(): i32` | How many `@scheduled` tasks are registered. |\n| `Daemon.nextFireMs(taskId: i32): i64` | The next fire time for a task, in epoch milliseconds, or `-1` when the id is unknown or the cron never fires again. |\n| `Daemon.yieldNow(): DaemonError` | Give the edge a chance to observe a lost lease. `DaemonError.None` means you are still the leader. |\n| `Daemon.sleep(ms: i64): DaemonError` | Park the task. The edge clamps this to 3 seconds. `LeaseLost` means the lease went away while you slept, and the task should stop. |\n| `Daemon.httpCall(request, responseCap?)` | Make an outbound HTTP request. Returns a `DaemonHttpResponse`, or `null` on failure. |\n| `Daemon.lastError(): DaemonError` | The failure recorded by the most recent `Daemon` call. |\n\n### Outbound HTTP\n\n`Daemon.httpCall` performs the leader-fenced, SSRF-bounded, metered call described above. Its usage shows up in the `Analytics` counters `daemonHttpCallAttempts` / `daemonHttpCallFailures`.\n\n```ts\n@daemon\nclass Jobs {\n @scheduled('5m')\n poll(): void {\n const req = new DaemonHttpRequest('POST', 'https://api.example.com/events');\n req.header('content-type', 'application/json');\n req.body = Uint8Array.wrap(String.UTF8.encode('{\"ping\":true}'));\n\n const res = Daemon.httpCall(req);\n if (res == null) {\n // Daemon.lastError(): NotLeader, CallFailed, ResponseTooLarge, BadEnvelope\n return;\n }\n if (res.status == 200) App.events.add(new Event(res.text()));\n }\n}\n```\n\nA `null` result is never ambiguous: read `Daemon.lastError()`.\n\n| `DaemonError` | Meaning |\n| --- | --- |\n| `NotLeader` | This worker does not hold the lease. The edge refused without touching the network. |\n| `LeaseLost` | The lease went away mid-call. |\n| `CallFailed` | The request did not complete: blocked by the SSRF guard, timed out, or the transport failed. |\n| `ResponseTooLarge` | The response did not fit your buffer. **The call already happened**, so do not blindly retry it: pass a larger `responseCap` instead. |\n| `BadEnvelope` | Your request broke a host cap (nothing was sent), or the response could not be parsed. |\n\nThe caps the edge enforces, which `httpCall` checks in the guest so an over-cap request never costs you a round trip: the method is 1 to 16 bytes, the URL at most 8 KiB, at most 64 headers of at most 8 KiB each, and a request body of at most 256 KiB. Responses are truncated at 1 MiB; `responseCap` defaults to 64 KiB.\n\nIn `toiljs dev` the emulator returns `CallFailed` for every `httpCall` rather than reaching the network, so a daemon that polls an external service does nothing locally. Everything else behaves as it does on the edge.\n\n## The `main.daemon.ts` file (a separate tier)\n\nLike streams, daemons live in their **own entry file**, `server/main.daemon.ts`, and compile into their **own artifact**, `build/server/release-cold.wasm`. Importing your `@daemon` module there pulls it into that artifact.\n\n```ts\n// server/main.daemon.ts\nimport { revertOnError } from 'toiljs/server/runtime/abort/abort';\n\nimport './daemon/Jobs'; // add each @daemon module here\n\n// NOTE: unlike main.ts / main.stream.ts, the daemon entry does NOT re-export the\n// request runtime. A daemon artifact exposes its schedule hooks, not the request handler.\nexport function abort(message: string, fileName: string, line: u32, column: u32): void {\n revertOnError(message, fileName, line, column);\n}\n```\n\n`toiljs build` produces `release-cold.wasm` automatically when your project declares a `@daemon` surface:\n\n```sh\n$ ls build/server/*.wasm\nbuild/server/release.wasm # L1 request (@rest / @service)\nbuild/server/release-stream.wasm # L2/L3 stream (@stream)\nbuild/server/release-cold.wasm # L4 daemon (@daemon)\n```\n\nThere is **at most one** `@daemon` class per project, and it is a compile error to put a `@daemon` in the request build. See [Compute tiers](../concepts/tiers.md) for how one source tree becomes three artifacts.\n\n## Worked example: a periodic rollup\n\nA common daemon job: every hour, read a running total and store a small summary so pages can show it with one cheap read. This reads a [counter](../database/counters.md) and writes a summary [document](../database/documents.md); the write only takes effect on the leader.\n\n```ts\n// server/models/StatKey.ts and Summary.ts (@data types)\n@data\nclass StatKey {\n name: string = 'signups';\n constructor(name: string = 'signups') { this.name = name; }\n}\n\n@data\nclass Summary {\n total: u64 = 0;\n updatedAt: u64 = 0;\n}\n\n// server/data/StatsDb.ts\n@database\nclass StatsDb {\n @collection static signups: Counter<StatKey>;\n @collection static summary: Documents<StatKey, Summary>;\n}\n\n// server/daemon/Jobs.ts\n@daemon\nclass Jobs {\n @scheduled('1h')\n rollup(): void {\n const key = new StatKey('signups');\n const total = StatsDb.signups.get(key); // a read (allowed anywhere)\n\n const s = new Summary();\n s.total = total;\n s.updatedAt = <u64>(Date.now() / 1000);\n StatsDb.summary.patch(key, s); // a write: runs only on the leader\n }\n}\n```\n\nNow any request handler can serve the summary with a single keyed read, and it is never more than an hour stale. Because the write is fenced to the leader and the rollup is idempotent (it *sets* the total rather than adding to it), a missed or failed-over tick is harmless: the next hourly run simply refreshes it.\n\n## When not to use a daemon\n\n- **When a user is waiting for the result.** Do that work in a [route](../backend/rest.md) or [RPC](../backend/rpc.md), on the request path.\n- **When you need a fast read-view kept in sync with data changes.** That is a [`@derive`](./derive.md), which runs on every write rather than on a timer.\n- **When you need exactly-once, never-skipped execution.** Scheduling is at-most-once with no backfill, so make tasks idempotent or safe to skip. A daemon is not a guaranteed job queue.\n\n## Related\n\n- [Background overview](./README.md): daemons versus `@derive`, and which to reach for.\n- [Derived views (`@derive`)](./derive.md): keep a read-view in sync on every write.\n- [Compute tiers (L1 to L4)](../concepts/tiers.md): the daemon runs on the L4 global tier.\n- [Counters](../database/counters.md), [Documents](../database/documents.md), [Events](../database/events.md): the data a daemon reads and writes.\n",
14
14
  "background/derive.md": "# Derived views (`@derive`)\n\nA `@derive` keeps a fast, precomputed **View** of your data in sync automatically. You write a method that reads your source data and publishes a summary; toiljs re-runs it whenever the source data changes, so your pages can read the summary with one cheap lookup instead of doing the work on every request.\n\n## The problem it solves\n\nSome reads are expensive. \"Show the 10 newest comments\" means scanning an [events](../database/events.md) log. \"Show the leaderboard\" means totalling up scores. A **scan** (walking many rows to build a result) can fan out across an unbounded amount of data, and that is too slow and too unpredictable to do while a user waits.\n\nSo toiljs **bars scans on the request path**. A [route](../backend/rest.md) handler runs under a restricted mode:\n\n- A `@get` runs as a **query** (reads only, no scans).\n- A `@post` / `@put` / `@patch` / `@del` runs as an **action** (keyed reads and writes, still no scans).\n\nIf you cannot scan in a route, how do you show \"the latest 10\"? You precompute it. A `@derive` does the scan **off** the request path, folds the result into a [View](../database/views.md) (a read-optimized snapshot stored by key), and your route reads that View with a single keyed lookup, which is not a scan and so is allowed.\n\n```mermaid\nflowchart LR\n W[\"POST /guestbook<br/>(action: append + count)\"] --> S[(Events + Counter<br/>source data)]\n S -->|write triggers| D[\"@derive recompute()<br/>scans, builds, publishes\"]\n D --> V[(View<br/>precomputed snapshot)]\n G[\"GET /guestbook<br/>(query: one keyed read)\"] --> V\n```\n\n## A worked example: a guestbook\n\nHere is the whole pattern in one database class: an [events](../database/events.md) log of signatures, a [counter](../database/counters.md) of how many there are, and a [View](../database/views.md) that holds the ready-to-serve page.\n\n```ts\n@data\nclass GuestKey {\n room: string = 'main';\n constructor(room: string = 'main') { this.room = room; }\n}\n\n@database\nclass GuestbookDb {\n @collection static entries: Events<GuestKey, GuestEntry>; // a source: the log\n @collection static totals: Counter<GuestKey>; // a source: the count\n @collection static book: View<GuestKey, GuestbookView>; // the view we publish\n\n // Recompute the view from the sources. It MAY scan and publish; a route may not.\n @derive\n recompute(): void {\n const key = new GuestKey('main');\n const view = new GuestbookView();\n view.total = GuestbookDb.totals.get(key); // a keyed read\n view.entries = GuestbookDb.entries.latest(key, 10); // a scan: allowed here\n GuestbookDb.book.publish(key, view); // publish the snapshot\n }\n}\n```\n\nThe route then writes to the sources and reads the view:\n\n```ts\n@rest('guestbook')\nclass Guestbook {\n @get('/')\n list(): GuestbookView {\n const key = new GuestKey('main');\n const view = GuestbookDb.book.get(key); // one keyed read, not a scan\n return view == null ? new GuestbookView() : view; // empty until first publish\n }\n\n @post('/')\n sign(input: NewMessage): GuestbookView {\n const key = new GuestKey('main');\n GuestbookDb.entries.append(key, new GuestEntry(input.author, input.message, 0));\n GuestbookDb.totals.add(key, 1);\n // The @derive republishes `book` right after this action returns, so GET\n // serves the new entry. The action just acks with the new total (a counter\n // read is allowed here; scanning the entries list is not).\n const view = new GuestbookView();\n view.total = GuestbookDb.totals.get(key);\n return view;\n }\n}\n```\n\nSign the guestbook twice and the total climbs across requests, because the data lives in the database (and its view), not in module memory.\n\n## What a `@derive` may do\n\nA `@derive` runs under a special **derive** mode that is more powerful than a route:\n\n| Ability | Query (`@get`) | Action (`@post` ...) | Derive |\n| -------------------------------------------- | :------------: | :------------------: | :----------: |\n| Keyed reads (`.get`, counter total) | yes | yes | yes |\n| Writes (`create`, `patch`, `add`, `append`) | no | yes | yes |\n| **Scans** (`events.latest`, membership list) | **no** | **no** | **yes** |\n| `view.publish` / `view.append` | no | no | yes |\n\nSo a derive is exactly the place to do the reads and scans a route cannot, and to publish the result.\n\n## Declaring a derive\n\nA `@derive` is a method on your [`@database`](../database/setup.md) class, next to the collections it reads and the View it writes.\n\n```ts\n@database\nclass MyDb {\n @collection static events: Events<Key, Fact>; // a source\n @collection static home: View<Key, HomePage>; // the materialized view\n\n @derive\n rebuild(): void {\n // read sources, build the value, publish it\n }\n}\n```\n\nRules:\n\n- A `@derive` method takes **no arguments and returns `void`**.\n- A database may declare **multiple** `@derive` methods; each runs independently.\n- The View value and its key are ordinary [`@data`](../backend/data.md) types, so they round-trip through the codec like any other stored value.\n\n## When a derive runs\n\nYou never call a derive yourself. The runtime runs it for you at two moments:\n\n1. **After a write to a source.** When a request writes one of the database's source collections (an `events.append`, a `counter.add`, a document `create` or `patch`), that database's derives run **right after the response is produced**, so the view reflects the new data on the next read. Many writes to one database in a single request are **coalesced** into one recompute (it does not run once per write).\n\n2. **On box load.** When a server box starts, hot-reloads, or notices the underlying data changed out of band, the views are rebuilt from their sources **before the first read is served**. This is also where a value type's [`@migrate`](../backend/data.md) runs against old stored events, as the derive re-reads and republishes them.\n\nA derive's own `view.publish` never re-triggers it, so there is no infinite loop.\n\nThe same code runs under `toiljs dev` (the in-process emulator) and on the production edge, with no flags or wiring to change.\n\n## Folding a growing log incrementally\n\nThe guestbook above uses `events.latest` and **recomputes** the view from scratch on every change. That is simple, correct, and the right default for a **bounded** read: the latest N, a counter total, a small set.\n\nSome views instead fold an **unbounded** log: a running total over every event ever, an activity rollup, an audit summary. Rescanning the whole log on every change gets slower as it grows. For those, read the source with [`events.since`](../database/events.md) instead of `latest`. `since` hands you only the events you have **not folded yet**, so the derive folds forward incrementally.\n\n```ts\n@derive\nrollup(): void {\n const key = new StatsKey('all');\n const view = StatsDb.summary.get(key) ?? new Summary(); // the running view so far\n let batch = StatsDb.events.since(key, 500);\n while (batch.length > 0) { // drain the new events in bounded batches\n for (let i = 0; i < batch.length; i++) view.apply(batch[i]);\n batch = StatsDb.events.since(key, 500);\n }\n StatsDb.summary.publish(key, view);\n}\n```\n\nThe host owns the cursor, you never manage it: it seeds `since` from a durable checkpoint, advances it as it hands you events, and saves it **only after** your `publish` lands (so a crash re-folds the batch instead of skipping it). On the next trigger the derive resumes exactly where it left off.\n\n**One rule: the fold must be idempotent per event.** A rare crash-recovery case (an event that \"heals\" at an older position after the derive already passed it) makes the host re-read the whole log that one run, so applying an event twice must not change the result. Use set-style updates keyed by the event's id, not blind `count += 1` accumulation. If your fold cannot be idempotent, stay on the simple `latest` recompute path.\n\n## Guarantees and limitations\n\n**Guarantees**\n\n- **It converges to a correct snapshot.** Publishes are last-writer-wins (the host versions each publish so a later one always supersedes an earlier one), and a derive recomputes from the source of truth, so the view always ends up matching its sources.\n- **Reads stay cheap.** A route serves the view with a single keyed lookup, never a scan.\n\n**Limitations (read these)**\n\n- **By default it recomputes from scratch each time.** A derive re-reads its sources and republishes on every trigger. That is the simple path and a great fit for a **bounded** read: the latest N, a counter total, a small set. To fold an **unbounded, ever-growing** log efficiently, read it with [`events.since`](../database/events.md) instead of `latest` (see [Folding a growing log incrementally](#folding-a-growing-log-incrementally) above); it folds only the new events since a checkpoint.\n- **It is eventually consistent, by a moment.** The view is republished right after the writing request finishes, so there is a tiny window where a reader could see the pre-write view. For most pages (a feed, a leaderboard) that is invisible and fine.\n- **`view.publish` is derive-only.** Routes cannot publish; they can only read the view. That is the whole point: the expensive build happens off the request path.\n\n## When not to use a derive\n\n- **When the read is already cheap.** If a route can answer with a plain keyed `.get`, you do not need a view at all.\n- **When you need it on a timer, not on a data change.** Recomputing \"yesterday's report\" at 2am is a [daemon](./daemons.md) job, not a derive (a derive is triggered by writes, not by the clock).\n- **When the source is an unbounded full-history fold.** See the limitation above; keep derives to bounded reads.\n\n## Related\n\n- [Views](../database/views.md): the `View<K, V>` family a derive publishes into, and how to read it.\n- [Events](../database/events.md): the append-only log a derive commonly folds into a view.\n- [Counters](../database/counters.md): running totals a derive can read.\n- [Background overview](./README.md): `@derive` versus `@daemon`, and which to reach for.\n- [Data types (`@data`)](../backend/data.md): the value and key types a view stores.\n",
15
15
  "background/README.md": "# Background work\n\nBackground work is code that runs **without a user waiting for it**: on a timer, once globally, or automatically after your data changes. toiljs gives you two tools for it, `@daemon` and `@derive`, and this page helps you pick the right one.\n\n## Why background work\n\nMost of your server code runs **because a user asked**: a browser hits a [route](../backend/rest.md) or calls an [RPC](../backend/rpc.md), your handler runs, and a response goes back. But some work does not belong on that path:\n\n- It should happen **on a schedule** (every hour, every night at 2am), not when a request happens to arrive.\n- It would be **too slow** to do inside a request, so you want it precomputed and ready.\n- It must happen **exactly once across the whole world**, not once per user and not once per server.\n\nThat is background work. In toiljs there are two kinds, and they solve two different problems.\n\n## The two tools\n\n```mermaid\nflowchart TD\n Q{What do you need?}\n Q -->|Run on a timer, or once globally| D[\"@daemon + @scheduled<br/>a single background worker\"]\n Q -->|Keep a fast read-view in sync with your data| V[\"@derive<br/>maintains a materialized View\"]\n D --> DD[\"e.g. nightly cleanup, hourly poll,<br/>periodic rollup\"]\n V --> VV[\"e.g. a leaderboard, a 'latest 10' feed,<br/>a home-page summary\"]\n```\n\n### `@daemon`: a scheduled, global worker\n\nA [daemon](./daemons.md) is one long-lived background worker for your whole app. It runs on a **schedule** you set (an interval like every 5 minutes, or a cron time like \"9:15 on weekdays\"), and there is exactly **one** of it worldwide at any moment (a \"singleton\"). Use it for work that is driven by **time** or that must run **once globally**:\n\n- clean up stale rows every night;\n- poll an external API every few minutes;\n- send a daily digest email;\n- roll up yesterday's numbers into a summary.\n\n### `@derive`: keep a read-view up to date\n\nA [derive](./derive.md) is not on a timer. It runs **automatically whenever the data it depends on changes**, and its job is to keep a precomputed **View** (a read-optimized copy of your data) fresh. Use it when a page needs data that is **expensive to compute on every read**, like a leaderboard or a \"latest 10 comments\" list, so the read itself stays a single cheap lookup:\n\n- fold an [events](../database/events.md) log into a \"latest N\" list;\n- total up [counters](../database/counters.md) into a scoreboard;\n- assemble a home-page summary from several sources.\n\n## Which one do I reach for?\n\n| Question | Use |\n| ----------------------------------------------------- | ------------ |\n| \"Run this every hour / at midnight.\" | `@daemon` |\n| \"Do this once for the whole app, not per server.\" | `@daemon` |\n| \"Poll or call an outside service on a schedule.\" | `@daemon` |\n| \"This page's data is too slow to compute per request.\"| `@derive` |\n| \"Keep a leaderboard / feed in sync as data changes.\" | `@derive` |\n\nA simple rule of thumb: if the trigger is **the clock**, use a `@daemon`. If the trigger is **a change to your data** and the goal is a fast read, use a `@derive`.\n\nThey also combine well. A `@daemon` might do a heavy nightly aggregation and write a summary row, while a `@derive` keeps a small live view fresh on every write. They are different tiers of the edge and are covered on their own pages.\n\n## `@job`: the widest database surface for background work\n\n`@daemon` and `@derive` decide **where and when** background code runs. `@job` answers a different question: **what a function is allowed to do to the database.** It is one of the ToilDB **function kinds** (the same family as `@query`, `@action`, and `@derive`), and it grants the **widest** data surface of them all.\n\nA **function kind** is a label the compiler puts on a backend function to gate which database operations it may issue. This is a safety rail: a read-only endpoint physically cannot write, and an expensive scan cannot run on the hot request path. The kinds line up from narrowest to widest:\n\n| Kind | Typical trigger | Point reads | **Scan** (`latest`, membership `list`) | Writes | `publish` a View |\n| ---------- | ------------------------- | ----------- | -------------------------------------- | ----------------- | ---------------- |\n| `@query` | a `@get` / plain `@remote`| yes | no | no | no |\n| `@action` | a `@post` / `@action` | yes | no | yes (bounded) | no |\n| `@derive` | your data changed | yes | yes | append / counter add only | yes |\n| `@job` | you drive it (background) | yes | yes | yes (all) | yes |\n\nA **scan** is a read that can fan out across many rows (like \"the newest 50 events\" via `events.latest`, or \"every member of this set\" via `membership.list`). Scans are **barred from request handlers** because a request must stay fast and bounded. `@derive` and `@job` run **off the request path**, so they are the only kinds allowed to scan.\n\n### When to reach for `@job`\n\nUse `@job` when a piece of background work needs **more** database power than the default:\n\n- it must **scan** (fold `events.latest`, walk `membership.list`), and/or\n- it must **publish a View** *while also* doing arbitrary writes (`create`, `patch`, `delete`). A `@derive` can publish a View but cannot `patch` or `delete` a record; a `@job` can do everything.\n\nThe compiler accepts `@job` on a method, including a `@daemon`'s `@scheduled` method. Tag a scheduled task `@job` when it needs that full surface (say, a nightly repair that scans a log, fixes rows, and republishes a summary View). A plain, **untagged** `@scheduled` method runs with the **`@action`** surface instead: point reads plus bounded writes, which is all most rollups and cleanups need.\n\n```ts\n@daemon\nclass Jobs {\n // A plain scheduled method: point reads + bounded writes (the @action surface).\n @scheduled('1h')\n rollup(): void { /* get a counter, patch a summary row */ }\n\n // A scheduled method that also needs SCANS and to PUBLISH a View: tag it @job.\n @scheduled('1d')\n @job\n nightlyRepair(): void {\n // @job unlocks scan-class reads (events.latest / membership.list)\n // and publishing a View, on top of ordinary reads and writes.\n }\n}\n```\n\n### `@job` versus `@derive`\n\nThey overlap (both run off the request path, both may scan, both may publish a View), but they are triggered differently and sized differently:\n\n- **`@derive`** is **change-triggered**: it re-runs automatically whenever its source data changes, and its narrow job is to keep one **View** in sync. It cannot `create` / `patch` / `delete` records (only append, counter-add, and publish). Reach for it when a read is too slow and you want it kept fresh on every write.\n- **`@job`** is **you-drive-it** background work (typically a `@scheduled` daemon method) with the **full** write surface. Reach for it when the work is clock-driven or one-off and needs to both scan and mutate freely.\n\n> **Rule of thumb.** Clock-driven and needs the full database surface: a `@job` (usually inside a `@daemon`). Change-driven and only maintains a View: a `@derive`. Clock-driven with modest reads and writes: a plain `@scheduled` method (no `@job` needed).\n\nFor the complete permission grid see the [function-kind matrix](../database/setup.md#how-access-is-gated-query-action-and-friends), and for the decorator itself see [every decorator](../concepts/decorators.md#database-function-kinds-data-access-policy).\n\n## Related\n\n- [Daemons and scheduled jobs](./daemons.md): `@daemon`, `@scheduled`, interval vs cron, and how a single global worker fails over safely.\n- [Derived views (`@derive`)](./derive.md): keeping a materialized View in sync with its source data.\n- [Compute tiers (L1 to L4)](../concepts/tiers.md): where daemons and request handlers each run on the edge.\n- [Views](../database/views.md) and [Events](../database/events.md): the database families a `@derive` reads from and writes to.\n",
16
- "cli/README.md": "# The toiljs CLI\n\nThe `toiljs` command is how you scaffold, run, build, self-host, and diagnose a toiljs app. This page lists every command and every flag, with copy-pasteable examples.\n\n## What it is\n\nWhen you install the `toiljs` package, it adds one executable to your project: `toiljs`. Everything you do day to day (start the dev server, produce a production build, check your setup) goes through it.\n\nYou almost never type the full path. A freshly scaffolded project already has npm scripts that call it, so you run `npm run dev` and `npm run build`. When you want a command that has no script (like `doctor` or `db`), run it with `npx`:\n\n```bash\nnpx toiljs doctor\n```\n\n`toiljs` needs **Node.js 24 or newer** (older versions will not run it).\n\n## Command overview\n\n| Command | What it does |\n| --- | --- |\n| `toiljs create [name]` | Scaffold a brand new toiljs app in a new folder. |\n| `toiljs dev` | Start the local development server with hot reload. |\n| `toiljs build` | Produce the optimized production build (client bundle + server WebAssembly). |\n| `toiljs start` | Self-host the built app on a fast production HTTP server. |\n| `toiljs configure` | Turn styling features (Sass/Less/Stylus, Tailwind) on or off in an existing project. |\n| `toiljs doctor` | Diagnose your project setup and (with `--fix`) repair common wiring. |\n| `toiljs update` | Check npm for newer dependency versions and apply the ones you pick. |\n| `toiljs db <action>` | Inspect, reset, snapshot, or restore the local dev database. |\n| `toiljs help` | Print the built-in help. Also `--help` or `-h`. |\n| `toiljs --version` | Print the installed toiljs version. Also `-v`. |\n\nRun `toiljs help` any time to see this list in your terminal.\n\n## Global behavior\n\nA few things apply to every command.\n\n- **`--root <dir>`**: run the command against a project in another directory instead of the current one. Every command accepts it.\n- **Automatic update check**: on every run (including `npm run dev`, which calls the CLI under the hood), toiljs quietly asks the npm registry whether a newer `toiljs` exists and prints a one-line notice on stderr if you are behind. It never blocks or slows the command in a meaningful way (the answer is cached for an hour and the network call is capped at two seconds). To turn it off, set the environment variable `TOILJS_NO_UPDATE_CHECK=1`. It also respects the common `NO_UPDATE_NOTIFIER` and `CI` variables.\n\n```bash\n# Run any command against a project in another folder.\ntoiljs build --root ./apps/marketing\n\n# Silence the \"newer version available\" notice.\nTOILJS_NO_UPDATE_CHECK=1 toiljs dev\n```\n\n## `toiljs create`\n\nScaffolds a new project into a new folder. By default it is interactive: it asks a short series of questions (project name, template, styling, and so on), then writes the files and installs dependencies. Pass flags to skip questions, or `-y` to accept every default and run with no questions at all.\n\n```bash\n# Interactive: answer the prompts.\nnpx toiljs create\n\n# Give it a name up front.\nnpx toiljs create my-app\n\n# Fully non-interactive (great for scripts and CI).\nnpx toiljs create my-app --yes --template app --style css\n```\n\n### What it sets up\n\nEvery new project comes wired for you: the enforced TypeScript, ESLint, and Prettier presets, file-based routing, a `toil.config.ts`, a `toilconfig.json` (the server compiler settings), a `.gitignore`, and the editor settings that make the toilscript language plugin work. It also scaffolds a `server/migrations/` folder (where ToilDB schema migrations live) and, unless you opt out, a set of AI assistant helper files.\n\nThe scaffolded `package.json` includes these scripts:\n\n| Script | Runs |\n| --- | --- |\n| `npm run dev` | `toiljs dev` |\n| `npm run build` | `toiljs build` |\n| `npm run build:server` | `toiljs build --server` |\n| `npm run lint` | `eslint client` |\n| `npm run typecheck` | `tsc --noEmit` |\n| `npm run format` | `prettier --write ...` |\n\n### Generated docs and AI-assistant pointers\n\nEvery project carries a full copy of this documentation set at `.toil/docs/`. You do not maintain it: toiljs regenerates it from the installed toiljs version on every `toiljs dev` and `toiljs build`, so it always matches the version you are on. Do not edit those files by hand (your changes are overwritten on the next dev or build).\n\nUnless you opt out, `toiljs create` also writes small **pointer files** at the project root that tell AI coding assistants to read `.toil/docs/` before touching the project: `CLAUDE.md` (Claude Code), `AGENTS.md` (Codex and others), `.cursor/rules/toiljs.mdc` (Cursor), and `.github/copilot-instructions.md` (GitHub Copilot). These are written once, committed, and yours to edit. Control them with `--ai` / `--no-ai` (when you pass neither, `create` asks).\n\n### create options\n\n| Flag | Meaning |\n| --- | --- |\n| `[name]` | The project folder name (a positional argument). If omitted, you are asked. |\n| `-t, --template <app\\|minimal>` | `app` is the full starter (landing page, layout, styles, demo routes). `minimal` is just a layout and a home route. Default `app`. |\n| `--style <css\\|sass\\|less\\|stylus>` | Which CSS flavor to set up. Default `css` (plain CSS). |\n| `--tailwind` / `--no-tailwind` | Add or skip Tailwind CSS (v4). Off by default. |\n| `--ai` / `--no-ai` | Include or skip AI assistant files (like `CLAUDE.md`). When omitted, you are asked. |\n| `--images` / `--no-images` | Enable or skip build-time image optimization. On by default. |\n| `--git` / `--no-git` | Initialize a git repository. When omitted, you are asked (default yes). |\n| `--install` / `--no-install` | Install dependencies after scaffolding. When omitted, you are asked (default yes). |\n| `--pm <npm\\|pnpm\\|yarn\\|bun>` | Which package manager to install with. Default `npm`. |\n| `-y, --yes` | Accept all defaults and skip every prompt. |\n\nFor a walkthrough, see [Create a project](../getting-started/create-project.md).\n\n## `toiljs dev`\n\nStarts the local development server with hot reload, so you edit a file and the browser updates in place. This is the command you leave running while you build.\n\n```bash\nnpm run dev\n# or, to pick a port:\nnpx toiljs dev --port 4000\n```\n\n### What the dev server emulates\n\nThe whole point of the dev server is to run your app locally the same way the real Dacely edge runs it in production, so what you see locally is what you ship. It emulates three things:\n\n1. **The edge.** For a project with a server (any project with a `toilconfig.json`), a small local HTTP server takes the public port and dispatches incoming requests into your compiled `server.wasm` using the exact same request envelope the production edge uses. Anything your server does not claim (page routes, static assets, the hot-reload websocket) is proxied to Vite behind the scenes. So your React frontend and your WebAssembly backend run together, on one URL, exactly like production.\n2. **The database.** ToilDB runs in-process as a local emulator. Every family (documents, views, unique, events, counters, membership, capacity) works, and the data is written to `.toil/devdata.json` so it survives restarts. You manage that file with [`toiljs db`](#toiljs-db).\n3. **The host functions.** The platform services your server code calls (email, environment variables and secrets, time, crypto, rate limiting, auth) are wired up locally so they behave like the edge. Email actually sends if you configure a provider (see [Email](../services/email.md)).\n\nA **client-only** project (no `toilconfig.json`) just gets the plain Vite dev server on your port, unchanged.\n\n### How hot reload works\n\n```mermaid\nflowchart TD\n E[\"You edit a file\"] --> Q{\"Which file?\"}\n Q -->|\"client/ (React, CSS)\"| V[\"Vite hot-swaps it in the browser\"]\n Q -->|\"server/ (a @rest / @data / @service file)\"| S[\"toilscript recompiles the server\"]\n S --> G[\"shared/server.ts is regenerated\"]\n G --> V2[\"Vite hot-swaps the new typed client\"]\n S --> W[\"the dev server hot-swaps the new server.wasm\"]\n V --> B[\"Browser updates, no full reload\"]\n V2 --> B\n W --> B\n```\n\nClient edits go straight through Vite's hot module replacement. Server edits trigger a toilscript rebuild: toiljs recompiles your backend, regenerates `shared/server.ts` (the typed client the browser imports to call your server), and hot-swaps the recompiled WebAssembly, all without you touching the browser. Rebuilds are debounced (grouped over about 150 milliseconds) so a \"save all\" or a formatter pass does not trigger a storm of builds.\n\nIf your project has an `emails/` folder, the dev server also prints an email-preview URL at `/__toil/emails`.\n\n### dev options\n\n| Flag | Meaning |\n| --- | --- |\n| `--port <n>` | Port to listen on. Default `3000` (or `client.port` from your config). |\n| `--root <dir>` | Run against a project in another directory. |\n\nPress `Ctrl+C` to stop. toiljs restores your terminal and force-exits even if a native listener is slow to close, so you never end up with an orphaned dev server rebuilding in the background.\n\n## `toiljs build`\n\nProduces the optimized production build. Run this before you deploy or before `toiljs start`.\n\n```bash\nnpm run build\n# or build only the server:\nnpx toiljs build --server\n```\n\n### What it produces\n\nA full build runs in a careful order so the pieces line up:\n\n```mermaid\nflowchart LR\n A[\"Your project\"] --> S[\"1. Compile the server<br/>(toilscript)\"]\n S --> W[\"build/server/release.wasm\"]\n S --> R[\"shared/server.ts<br/>(typed client, regenerated)\"]\n R --> C[\"2. Bundle the client<br/>(Vite)\"]\n C --> O[\"build/client/<br/>(HTML, JS, CSS, assets)\"]\n C --> X[\"3. Prerender + SEO<br/>(SSG pages, robots.txt,<br/>sitemap.xml, llms.txt)\"]\n```\n\n1. **The server is built first.** toilscript compiles every decorated server file (not just the entry) into `build/server/release.wasm`, and regenerates `shared/server.ts`. Doing this first means the client always bundles against a current, correct typed server client. A project that also declares `@stream` or `@daemon` surfaces compiles those into their own artifacts (`release-stream.wasm`, `release-cold.wasm`).\n2. **The client is bundled** by Vite into `build/client/` (your HTML, JavaScript, CSS, and optimized assets). The dev toolbar and error overlay are stripped out of the production bundle.\n3. **Static pages and SEO files are generated**: any route that opts into static generation is prerendered to HTML, and if you configured `client.seo`, toiljs writes `robots.txt`, `sitemap.xml`, and `llms.txt`. Routes that opt into server-side rendering get their HTML-with-holes templates baked for the edge.\n\n### build options\n\n| Flag | Meaning |\n| --- | --- |\n| `--server` | Build **only** the server (recompile the wasm and regenerate `shared/server.ts`), and skip the client bundle. Fast when you only touched backend code. This is what `npm run build:server` runs. |\n| `--root <dir>` | Run against a project in another directory. |\n\nA client-only project (no `toilconfig.json`) skips step 1 and just bundles the client.\n\n## `toiljs start`\n\nSelf-hosts the app you just built, on a fast production HTTP server (hyper-express, backed by uWebSockets.js). It serves your static client, runs your `server.wasm` for dynamic requests, does server-side rendering, supports daemons, and exposes a `/_toil` websocket channel. Use it to run your app on your own machine or server instead of deploying to the Dacely edge.\n\n```bash\nnpm run build # start needs a build to serve\nnpx toiljs start\nnpx toiljs start --port 8080 --host 0.0.0.0 --threads 4\n```\n\n`start` fails fast if there is no build yet (it looks for `build/client/index.html`), so run `toiljs build` first.\n\n### start options\n\n| Flag | Meaning |\n| --- | --- |\n| `--port <n>` | Port to listen on. Default `3000` (or `client.port`). |\n| `--host <host>` | Address to bind. Default `127.0.0.1` (loopback only). Pass `0.0.0.0` to accept connections from other machines. |\n| `--threads <n>` | Number of HTTP worker processes. Default is automatic (one per available CPU). Pass `1` to disable the worker pool. `--workers` is an accepted alias. This can also be set as `server.threads` in your config. |\n| `--root <dir>` | Run against a project in another directory. |\n\n## `toiljs configure`\n\nToggles a project's client styling features (the CSS preprocessor and Tailwind) after the fact, on an existing app. It detects your current setup, asks what you want, then rewrites the stylesheets and your app entry's imports, edits `package.json`, and syncs `node_modules` so removed packages are actually uninstalled.\n\n```bash\n# Interactive.\nnpx toiljs configure\n\n# Non-interactive: switch to Sass and turn Tailwind on.\nnpx toiljs configure --style sass --tailwind\n```\n\n### configure options\n\n| Flag | Meaning |\n| --- | --- |\n| `--style <css\\|sass\\|less\\|stylus>` | Switch the CSS preprocessor. |\n| `--tailwind` / `--no-tailwind` | Turn Tailwind on or off. |\n| `--images` / `--no-images` | Turn build-time image optimization on or off (sets `client.images` in your config). |\n| `--no-install` | Edit the files but do not run the package manager. You then run install yourself. |\n| `--root <dir>` | Run against a project in another directory. |\n\nPassing any of `--style`, `--tailwind`, or `--images` makes the command non-interactive (it skips the prompts you did not answer with a flag). See [Styling](../frontend/styling.md) for the full picture.\n\n## `toiljs doctor`\n\nRead-only project diagnostics. It gathers facts from disk (your `package.json`, lockfiles, the resolved config, your app entry, `index.html`, your routes, and the server target), runs a set of checks, and prints a grouped report. It never changes anything unless you pass `--fix`, and it never crashes on a partial or non-toiljs project (missing pieces just become warnings or failures). It exits with a non-zero status when any check **fails** (warnings do not fail), so it is safe to run in CI.\n\n```bash\n# Human-readable report.\nnpx toiljs doctor\n\n# Machine-readable, for CI.\nnpx toiljs doctor --json\n\n# Auto-repair the common wiring.\nnpx toiljs doctor --fix\n```\n\n### What it checks\n\nThe report is grouped:\n\n| Group | Example checks |\n| --- | --- |\n| **Environment** | Node.js version, that `toiljs` and its peer dependencies (React and so on) are installed and new enough, that your TypeScript is a supported 6.x rather than the unsupported native 7.x, that a lockfile exists, and that your scripts do not wrap `toiljs` in a stray `npx`. |\n| **Project + routing** | The `client/` and `routes/` folders exist, `index.html` has a `<div id=\"root\">`, your app entry calls `mount(...)` with the `slots` argument, at least one route exists, no two routes collide on the same URL, and no asset paths are written in a way that 404s on nested routes. |\n| **Config + assets** | Your `toil.config` loads, the base path is well formed, `client.seo` has a `url` if SEO is configured, and your styling packages are actually installed. |\n| **Server / WASM** | The `toilconfig.json` and its entry files exist, `toilscript` is installed, a compiled `.wasm` exists, the typed-RPC wiring is in place, your `@rest` controllers are actually dispatched, the Prettier and editor plugins are wired, and a `migrations/` folder exists. |\n| **Security** | If your server uses auth, whether `AUTH_SESSION_SECRET` is set (an unset secret means sessions fall back to a published dev key, which is forgeable). |\n\n### What `--fix` repairs\n\n`--fix` pins an unsupported TypeScript (the native 7.x, which ships no compiler API) back to `^6.0.3` in any project. The rest only touch a server project (one with a `toilconfig.json`), repairing the wiring that is easy to get wrong or that older projects predate:\n\n- adds `--rpcModule shared/server.ts` to your server build scripts,\n- adds `shared` and the `shared/*` path alias to `tsconfig.json`,\n- adds `shared/server.ts` to `.gitignore`,\n- lifts the `toilscript` version floor if it is too old,\n- adds the `toiljs/prettier-plugin` to your Prettier config (so Prettier does not choke on server decorators),\n- adds the toilscript language-service plugin to your server `tsconfig.json` and points VS Code at the workspace TypeScript (so the editor stops false-flagging `@database` collections and `@data` members),\n- refreshes the editor-only server globals declaration file.\n\nIt is idempotent: it only writes files it actually needs to change, and it tells you which ones changed and which need a manual edit (for example a `tsconfig.json` that contains comments). If it changed `package.json`, run your installer afterward.\n\n### doctor options\n\n| Flag | Meaning |\n| --- | --- |\n| `--json` | Emit machine-readable JSON instead of the human report (the banner is suppressed so stdout stays valid JSON). |\n| `--fix` | Repair the server wiring in place, as above. |\n| `--root <dir>` | Run against a project in another directory. |\n\n## `toiljs update`\n\nA friendly wrapper over `npm-check-updates`. It checks the registry for newer versions of your dependencies, groups them by how big the jump is (major, minor, patch), lets you pick which to apply (or `-y` to apply all), bumps `package.json`, and runs your package manager's install. It also makes sure your `server/migrations/` folder exists (older projects predate it). `npm-check-updates` runs via `npx`, so it never becomes a permanent dependency of your project.\n\nUpgrades into a major toiljs does not support are held back and listed separately, so neither the picker nor `-y` can install one. Today that means **TypeScript 7**, the native port, which ships no JavaScript compiler API (see [Installation](../getting-started/installation.md)). Bumps inside TypeScript 6 are still offered.\n\n```bash\n# Interactive picker.\nnpx toiljs update\n\n# Apply everything, non-interactively.\nnpx toiljs update --yes\n\n# Only patch-level updates.\nnpx toiljs update --target patch\n```\n\n### update options\n\n| Flag | Meaning |\n| --- | --- |\n| `-y, --yes` | Apply all available updates without the picker. |\n| `--target <latest\\|minor\\|patch\\|newest\\|greatest>` | How far to bump. Default `latest`. |\n| `--root <dir>` | Run against a project in another directory. |\n\n## `toiljs db`\n\nManages the local dev database: the on-disk ToilDB store your dev server writes to `.toil/devdata.json`. Use it to inspect data, wipe a corrupt state, save a snapshot to share as a fixture, or restore one. The snapshot is exactly the JSON the dev database uses, so an exported file imports cleanly.\n\n```bash\n# See what is stored.\nnpx toiljs db status\n\n# Wipe all dev data.\nnpx toiljs db reset\n\n# Save a snapshot (to a file, or to stdout if you omit the file).\nnpx toiljs db export fixture.json\n\n# Restore a snapshot.\nnpx toiljs db import fixture.json\n\n# Print the on-disk path (scriptable).\nnpx toiljs db path\n```\n\n### db actions\n\n| Action | What it does |\n| --- | --- |\n| `status` (alias `info`) | Show the database path, its size, and per-family row counts. |\n| `reset` (alias `purge`) | Delete all dev data (removes `devdata.json`). |\n| `export [file]` | Write a formatted snapshot to `file`, or to stdout if you omit it (pipe-friendly). |\n| `import <file>` | Replace the dev database with the snapshot in `file`. It refuses a file that is not a valid snapshot. |\n| `path` | Print the `devdata.json` path and nothing else. |\n\nThe dev database is per-project and lives under `.toil/`, which is gitignored. See [Database setup](../database/setup.md) for what actually populates it.\n\n## The dev, build, and deploy flow\n\nHere is how the commands fit together across your workflow.\n\n```mermaid\nflowchart LR\n subgraph Develop\n D[\"toiljs dev<br/>(hot reload, local edge + DB)\"]\n end\n subgraph Ship\n B[\"toiljs build<br/>(client bundle + server.wasm)\"]\n end\n subgraph Run\n ST[\"toiljs start<br/>(self-host locally)\"]\n DP[\"deploy to the Dacely edge\"]\n end\n D --> B\n B --> ST\n B --> DP\n```\n\n- **Develop** with `toiljs dev`. Everything runs locally and reloads as you type.\n- **Ship** with `toiljs build`. This produces the artifacts: the client bundle in `build/client/` and the server WebAssembly in `build/server/`.\n- **Run** the build two ways: `toiljs start` self-hosts it on your own machine, or you deploy the same build to the Dacely edge to serve it worldwide.\n\n## Gotchas\n\n- **`start` needs a build.** `toiljs start` serves what is in `build/`. Run `toiljs build` first, or it exits with an error.\n- **Run `toiljs`, not `npx toiljs`, inside npm scripts.** Under `npm run`, `node_modules/.bin` is already on your PATH, so an extra `npx` layer is redundant and can leave your terminal in a broken input mode after `Ctrl+C`. `doctor` warns about this; the scaffold gets it right.\n- **`--host` and `--threads` are `start`-only.** `toiljs dev` always binds locally and does not take `--host` or `--threads`.\n- **`--server` builds the backend only.** Use it while iterating on server code, but run a full `toiljs build` before you deploy so the client bundle is current.\n- **The update notice is not an error.** The \"newer toiljs available\" line is informational and prints to stderr. Set `TOILJS_NO_UPDATE_CHECK=1` to hide it.\n\n## Related\n\n- [Installation](../getting-started/installation.md) and [Create a project](../getting-started/create-project.md)\n- [Project structure](../getting-started/project-structure.md)\n- [Configuration reference (`toil.config.ts`)](../concepts/config.md)\n- [Styling](../frontend/styling.md)\n- [Database setup](../database/setup.md) and the [database overview](../database/README.md)\n- [Compute tiers (L1 to L4)](../concepts/tiers.md)\n",
16
+ "cli/README.md": "# The toiljs CLI\n\nThe `toiljs` command is how you scaffold, run, build, self-host, and diagnose a toiljs app. This page lists every command and every flag, with copy-pasteable examples.\n\n## What it is\n\nWhen you install the `toiljs` package, it adds one executable to your project: `toiljs`. Everything you do day to day (start the dev server, produce a production build, check your setup) goes through it.\n\nYou almost never type the full path. A freshly scaffolded project already has npm scripts that call it, so you run `npm run dev` and `npm run build`. When you want a command that has no script (like `doctor` or `db`), run it with `npx`:\n\n```bash\nnpx toiljs doctor\n```\n\n`toiljs` needs **Node.js 24 or newer** (older versions will not run it).\n\n## Command overview\n\n| Command | What it does |\n| --- | --- |\n| `toiljs create [name]` | Scaffold a brand new toiljs app in a new folder. |\n| `toiljs dev` | Start the local development server with hot reload. |\n| `toiljs build` | Produce the optimized production build (client bundle + server WebAssembly). |\n| `toiljs start` | Self-host the built app on a fast production HTTP server. |\n| `toiljs configure` | Turn styling features (Sass/Less/Stylus, Tailwind) on or off in an existing project. |\n| `toiljs doctor` | Diagnose your project setup and (with `--fix`) repair common wiring. |\n| `toiljs update` | Check npm for newer dependency versions and apply the ones you pick. |\n| `toiljs db <action>` | Inspect, reset, snapshot, or restore the local dev database. |\n| `toiljs help` | Print the built-in help. Also `--help` or `-h`. |\n| `toiljs --version` | Print the installed toiljs version. Also `-v`. |\n\nRun `toiljs help` any time to see this list in your terminal.\n\n## Global behavior\n\nA few things apply to every command.\n\n- **`--root <dir>`**: run the command against a project in another directory instead of the current one. Every command accepts it.\n- **Automatic update check**: on every run (including `npm run dev`, which calls the CLI under the hood), toiljs quietly asks the npm registry whether a newer `toiljs` exists and prints a one-line notice on stderr if you are behind. It never blocks or slows the command in a meaningful way (the answer is cached for an hour and the network call is capped at two seconds). To turn it off, set the environment variable `TOILJS_NO_UPDATE_CHECK=1`. It also respects the common `NO_UPDATE_NOTIFIER` and `CI` variables.\n\n```bash\n# Run any command against a project in another folder.\ntoiljs build --root ./apps/marketing\n\n# Silence the \"newer version available\" notice.\nTOILJS_NO_UPDATE_CHECK=1 toiljs dev\n```\n\n## `toiljs create`\n\nScaffolds a new project into a new folder. By default it is interactive: it asks a short series of questions (project name, template, styling, and so on), then writes the files and installs dependencies. Pass flags to skip questions, or `-y` to accept every default and run with no questions at all.\n\n```bash\n# Interactive: answer the prompts.\nnpx toiljs create\n\n# Give it a name up front.\nnpx toiljs create my-app\n\n# Fully non-interactive (great for scripts and CI).\nnpx toiljs create my-app --yes --template app --style css\n```\n\n### What it sets up\n\nEvery new project comes wired for you: the enforced TypeScript 7, Oxlint + tsgolint, and Prettier presets, file-based routing, a `toil.config.ts`, a `toilconfig.json` (the server compiler settings), a `.gitignore`, and the native TypeScript 7 editor settings. It also scaffolds a `server/migrations/` folder (where ToilDB schema migrations live) and, unless you opt out, a set of AI assistant helper files.\n\nThe scaffolded `package.json` includes these scripts:\n\n| Script | Runs |\n| --- | --- |\n| `npm run dev` | `toiljs dev` |\n| `npm run build` | `toiljs build` |\n| `npm run build:server` | `toiljs build --server` |\n| `npm run lint` | `oxlint --type-aware client` |\n| `npm run typecheck` | `tsc --noEmit` |\n| `npm run format` | `prettier --write ...` |\n\n### Generated docs and AI-assistant pointers\n\nEvery project carries a full copy of this documentation set at `.toil/docs/`. You do not maintain it: toiljs regenerates it from the installed toiljs version on every `toiljs dev` and `toiljs build`, so it always matches the version you are on. Do not edit those files by hand (your changes are overwritten on the next dev or build).\n\nUnless you opt out, `toiljs create` also writes small **pointer files** at the project root that tell AI coding assistants to read `.toil/docs/` before touching the project: `CLAUDE.md` (Claude Code), `AGENTS.md` (Codex and others), `.cursor/rules/toiljs.mdc` (Cursor), and `.github/copilot-instructions.md` (GitHub Copilot). These are written once, committed, and yours to edit. Control them with `--ai` / `--no-ai` (when you pass neither, `create` asks).\n\n### create options\n\n| Flag | Meaning |\n| --- | --- |\n| `[name]` | The project folder name (a positional argument). If omitted, you are asked. |\n| `-t, --template <app\\|minimal>` | `app` is the full starter (landing page, layout, styles, demo routes). `minimal` is just a layout and a home route. Default `app`. |\n| `--style <css\\|sass\\|less\\|stylus>` | Which CSS flavor to set up. Default `css` (plain CSS). |\n| `--tailwind` / `--no-tailwind` | Add or skip Tailwind CSS (v4). Off by default. |\n| `--ai` / `--no-ai` | Include or skip AI assistant files (like `CLAUDE.md`). When omitted, you are asked. |\n| `--images` / `--no-images` | Enable or skip build-time image optimization. On by default. |\n| `--git` / `--no-git` | Initialize a git repository. When omitted, you are asked (default yes). |\n| `--install` / `--no-install` | Install dependencies after scaffolding. When omitted, you are asked (default yes). |\n| `--pm <npm\\|pnpm\\|yarn\\|bun>` | Which package manager to install with. Default `npm`. |\n| `-y, --yes` | Accept all defaults and skip every prompt. |\n\nFor a walkthrough, see [Create a project](../getting-started/create-project.md).\n\n## `toiljs dev`\n\nStarts the local development server with hot reload, so you edit a file and the browser updates in place. This is the command you leave running while you build.\n\n```bash\nnpm run dev\n# or, to pick a port:\nnpx toiljs dev --port 4000\n```\n\n### What the dev server emulates\n\nThe whole point of the dev server is to run your app locally the same way the real Dacely edge runs it in production, so what you see locally is what you ship. It emulates three things:\n\n1. **The edge.** For a project with a server (any project with a `toilconfig.json`), a small local HTTP server takes the public port and dispatches incoming requests into your compiled `server.wasm` using the exact same request envelope the production edge uses. Anything your server does not claim (page routes, static assets, the hot-reload websocket) is proxied to Vite behind the scenes. So your React frontend and your WebAssembly backend run together, on one URL, exactly like production.\n2. **The database.** ToilDB runs in-process as a local emulator. Every family (documents, views, unique, events, counters, membership, capacity) works, and the data is written to `.toil/devdata.json` so it survives restarts. You manage that file with [`toiljs db`](#toiljs-db).\n3. **The host functions.** The platform services your server code calls (email, environment variables and secrets, time, crypto, rate limiting, auth) are wired up locally so they behave like the edge. Email actually sends if you configure a provider (see [Email](../services/email.md)).\n\nA **client-only** project (no `toilconfig.json`) just gets the plain Vite dev server on your port, unchanged.\n\n### How hot reload works\n\n```mermaid\nflowchart TD\n E[\"You edit a file\"] --> Q{\"Which file?\"}\n Q -->|\"client/ (React, CSS)\"| V[\"Vite hot-swaps it in the browser\"]\n Q -->|\"server/ (a @rest / @data / @service file)\"| S[\"toilscript recompiles the server\"]\n S --> G[\"shared/server.ts is regenerated\"]\n G --> V2[\"Vite hot-swaps the new typed client\"]\n S --> W[\"the dev server hot-swaps the new server.wasm\"]\n V --> B[\"Browser updates, no full reload\"]\n V2 --> B\n W --> B\n```\n\nClient edits go straight through Vite's hot module replacement. Server edits trigger a toilscript rebuild: toiljs recompiles your backend, regenerates `shared/server.ts` (the typed client the browser imports to call your server), and hot-swaps the recompiled WebAssembly, all without you touching the browser. Rebuilds are debounced (grouped over about 150 milliseconds) so a \"save all\" or a formatter pass does not trigger a storm of builds.\n\nIf your project has an `emails/` folder, the dev server also prints an email-preview URL at `/__toil/emails`.\n\n### dev options\n\n| Flag | Meaning |\n| --- | --- |\n| `--port <n>` | Port to listen on. Default `3000` (or `client.port` from your config). |\n| `--root <dir>` | Run against a project in another directory. |\n\nPress `Ctrl+C` to stop. toiljs restores your terminal and force-exits even if a native listener is slow to close, so you never end up with an orphaned dev server rebuilding in the background.\n\n## `toiljs build`\n\nProduces the optimized production build. Run this before you deploy or before `toiljs start`.\n\n```bash\nnpm run build\n# or build only the server:\nnpx toiljs build --server\n```\n\n### What it produces\n\nA full build runs in a careful order so the pieces line up:\n\n```mermaid\nflowchart LR\n A[\"Your project\"] --> S[\"1. Compile the server<br/>(toilscript)\"]\n S --> W[\"build/server/release.wasm\"]\n S --> R[\"shared/server.ts<br/>(typed client, regenerated)\"]\n R --> C[\"2. Bundle the client<br/>(Vite)\"]\n C --> O[\"build/client/<br/>(HTML, JS, CSS, assets)\"]\n C --> X[\"3. Prerender + SEO<br/>(SSG pages, robots.txt,<br/>sitemap.xml, llms.txt)\"]\n```\n\n1. **The server is built first.** toilscript compiles every decorated server file (not just the entry) into `build/server/release.wasm`, and regenerates `shared/server.ts`. Doing this first means the client always bundles against a current, correct typed server client. A project that also declares `@stream` or `@daemon` surfaces compiles those into their own artifacts (`release-stream.wasm`, `release-cold.wasm`).\n2. **The client is bundled** by Vite into `build/client/` (your HTML, JavaScript, CSS, and optimized assets). The dev toolbar and error overlay are stripped out of the production bundle.\n3. **Static pages and SEO files are generated**: any route that opts into static generation is prerendered to HTML, and if you configured `client.seo`, toiljs writes `robots.txt`, `sitemap.xml`, and `llms.txt`. Routes that opt into server-side rendering get their HTML-with-holes templates baked for the edge.\n\n### build options\n\n| Flag | Meaning |\n| --- | --- |\n| `--server` | Build **only** the server (recompile the wasm and regenerate `shared/server.ts`), and skip the client bundle. Fast when you only touched backend code. This is what `npm run build:server` runs. |\n| `--root <dir>` | Run against a project in another directory. |\n\nA client-only project (no `toilconfig.json`) skips step 1 and just bundles the client.\n\n## `toiljs start`\n\nSelf-hosts the app you just built, on a fast production HTTP server (hyper-express, backed by uWebSockets.js). It serves your static client, runs your `server.wasm` for dynamic requests, does server-side rendering, supports daemons, and exposes a `/_toil` websocket channel. Use it to run your app on your own machine or server instead of deploying to the Dacely edge.\n\n```bash\nnpm run build # start needs a build to serve\nnpx toiljs start\nnpx toiljs start --port 8080 --host 0.0.0.0 --threads 4\n```\n\n`start` fails fast if there is no build yet (it looks for `build/client/index.html`), so run `toiljs build` first.\n\n### start options\n\n| Flag | Meaning |\n| --- | --- |\n| `--port <n>` | Port to listen on. Default `3000` (or `client.port`). |\n| `--host <host>` | Address to bind. Default `127.0.0.1` (loopback only). Pass `0.0.0.0` to accept connections from other machines. |\n| `--threads <n>` | Number of HTTP worker processes. Default is automatic (one per available CPU). Pass `1` to disable the worker pool. `--workers` is an accepted alias. This can also be set as `server.threads` in your config. |\n| `--root <dir>` | Run against a project in another directory. |\n\n## `toiljs configure`\n\nToggles a project's client styling features (the CSS preprocessor and Tailwind) after the fact, on an existing app. It detects your current setup, asks what you want, then rewrites the stylesheets and your app entry's imports, edits `package.json`, and syncs `node_modules` so removed packages are actually uninstalled.\n\n```bash\n# Interactive.\nnpx toiljs configure\n\n# Non-interactive: switch to Sass and turn Tailwind on.\nnpx toiljs configure --style sass --tailwind\n```\n\n### configure options\n\n| Flag | Meaning |\n| --- | --- |\n| `--style <css\\|sass\\|less\\|stylus>` | Switch the CSS preprocessor. |\n| `--tailwind` / `--no-tailwind` | Turn Tailwind on or off. |\n| `--images` / `--no-images` | Turn build-time image optimization on or off (sets `client.images` in your config). |\n| `--no-install` | Edit the files but do not run the package manager. You then run install yourself. |\n| `--root <dir>` | Run against a project in another directory. |\n\nPassing any of `--style`, `--tailwind`, or `--images` makes the command non-interactive (it skips the prompts you did not answer with a flag). See [Styling](../frontend/styling.md) for the full picture.\n\n## `toiljs doctor`\n\nRead-only project diagnostics. It gathers facts from disk (your `package.json`, lockfiles, the resolved config, your app entry, `index.html`, your routes, and the server target), runs a set of checks, and prints a grouped report. It never changes anything unless you pass `--fix`, and it never crashes on a partial or non-toiljs project (missing pieces just become warnings or failures). It exits with a non-zero status when any check **fails** (warnings do not fail), so it is safe to run in CI.\n\n```bash\n# Human-readable report.\nnpx toiljs doctor\n\n# Machine-readable, for CI.\nnpx toiljs doctor --json\n\n# Auto-repair the common wiring.\nnpx toiljs doctor --fix\n```\n\n### What it checks\n\nThe report is grouped:\n\n| Group | Example checks |\n| --- | --- |\n| **Environment** | Node.js version, that `toiljs` and its peer dependencies (React and so on) are installed and new enough, that TypeScript 7 is installed and its declared range stays within 7.x, that Oxlint and tsgolint are configured for type-aware linting, that a lockfile exists, and that your scripts do not wrap `toiljs` in a stray `npx`. |\n| **Project + routing** | The `client/` and `routes/` folders exist, `index.html` has a `<div id=\"root\">`, your app entry calls `mount(...)` with the `slots` argument, at least one route exists, no two routes collide on the same URL, and no asset paths are written in a way that 404s on nested routes. |\n| **Config + assets** | Your `toil.config` loads, the base path is well formed, `client.seo` has a `url` if SEO is configured, and your styling packages are actually installed. |\n| **Server / WASM** | The `toilconfig.json` and its entry files exist, `toilscript` is installed, a compiled `.wasm` exists, the typed-RPC wiring is in place, your `@rest` controllers are actually dispatched, the Prettier and editor plugins are wired, and a `migrations/` folder exists. |\n| **Security** | If your server uses auth, whether `AUTH_SESSION_SECRET` is set (an unset secret means sessions fall back to a published dev key, which is forgeable). |\n\n### What `--fix` repairs\n\n`--fix` migrates unsupported or missing TypeScript declarations to `^7.0.2` and enables the native TypeScript editor in any project. The rest only touch a server project (one with a `toilconfig.json`), repairing the wiring that is easy to get wrong or that older projects predate:\n\n- adds `--rpcModule shared/server.ts` to your server build scripts,\n- adds `shared` and the `shared/*` path alias to `tsconfig.json`,\n- adds `shared/server.ts` to `.gitignore`,\n- lifts the `toilscript` version floor if it is too old,\n- adds the `toiljs/prettier-plugin` to your Prettier config (so Prettier does not choke on server decorators),\n- removes the legacy toilscript JavaScript language-service plugin, which cannot run in the TypeScript 7 native language server; `toiljs build --server` provides the WebAssembly dialect diagnostics,\n- refreshes the editor-only server globals declaration file.\n\nIt is idempotent: it only writes files it actually needs to change, and it tells you which ones changed and which need a manual edit (for example a `tsconfig.json` that contains comments). If it changed `package.json`, run your installer afterward.\n\n### doctor options\n\n| Flag | Meaning |\n| --- | --- |\n| `--json` | Emit machine-readable JSON instead of the human report (the banner is suppressed so stdout stays valid JSON). |\n| `--fix` | Repair the server wiring in place, as above. |\n| `--root <dir>` | Run against a project in another directory. |\n\n## `toiljs update`\n\nA friendly wrapper over `npm-check-updates`. It checks the registry for newer versions of your dependencies, groups them by how big the jump is (major, minor, patch), lets you pick which to apply (or `-y` to apply all), bumps `package.json`, and runs your package manager's install. It also makes sure your `server/migrations/` folder exists (older projects predate it). `npm-check-updates` runs via `npx`, so it never becomes a permanent dependency of your project.\n\nTypeScript updates must stay within supported **7.x** releases. Older and future compiler majors,\nunbounded ranges, and prereleases are withheld. An older project is offered the latest 7.x migration\neven with `--target patch`. Select that migration before applying other updates. The command writes\nthe exact reviewed versions before installing, so a second registry query cannot substitute a\ndifferent compiler major.\n\n```bash\n# Interactive picker.\nnpx toiljs update\n\n# Apply everything, non-interactively.\nnpx toiljs update --yes\n\n# Only patch-level updates.\nnpx toiljs update --target patch\n```\n\n### update options\n\n| Flag | Meaning |\n| --- | --- |\n| `-y, --yes` | Apply all available updates without the picker. |\n| `--target <latest\\|minor\\|patch\\|newest\\|greatest>` | How far to bump. Default `latest`. |\n| `--root <dir>` | Run against a project in another directory. |\n\n## `toiljs db`\n\nManages the local dev database: the on-disk ToilDB store your dev server writes to `.toil/devdata.json`. Use it to inspect data, wipe a corrupt state, save a snapshot to share as a fixture, or restore one. The snapshot is exactly the JSON the dev database uses, so an exported file imports cleanly.\n\n```bash\n# See what is stored.\nnpx toiljs db status\n\n# Wipe all dev data.\nnpx toiljs db reset\n\n# Save a snapshot (to a file, or to stdout if you omit the file).\nnpx toiljs db export fixture.json\n\n# Restore a snapshot.\nnpx toiljs db import fixture.json\n\n# Print the on-disk path (scriptable).\nnpx toiljs db path\n```\n\n### db actions\n\n| Action | What it does |\n| --- | --- |\n| `status` (alias `info`) | Show the database path, its size, and per-family row counts. |\n| `reset` (alias `purge`) | Delete all dev data (removes `devdata.json`). |\n| `export [file]` | Write a formatted snapshot to `file`, or to stdout if you omit it (pipe-friendly). |\n| `import <file>` | Replace the dev database with the snapshot in `file`. It refuses a file that is not a valid snapshot. |\n| `path` | Print the `devdata.json` path and nothing else. |\n\nThe dev database is per-project and lives under `.toil/`, which is gitignored. See [Database setup](../database/setup.md) for what actually populates it.\n\n## The dev, build, and deploy flow\n\nHere is how the commands fit together across your workflow.\n\n```mermaid\nflowchart LR\n subgraph Develop\n D[\"toiljs dev<br/>(hot reload, local edge + DB)\"]\n end\n subgraph Ship\n B[\"toiljs build<br/>(client bundle + server.wasm)\"]\n end\n subgraph Run\n ST[\"toiljs start<br/>(self-host locally)\"]\n DP[\"deploy to the Dacely edge\"]\n end\n D --> B\n B --> ST\n B --> DP\n```\n\n- **Develop** with `toiljs dev`. Everything runs locally and reloads as you type.\n- **Ship** with `toiljs build`. This produces the artifacts: the client bundle in `build/client/` and the server WebAssembly in `build/server/`.\n- **Run** the build two ways: `toiljs start` self-hosts it on your own machine, or you deploy the same build to the Dacely edge to serve it worldwide.\n\n## Gotchas\n\n- **`start` needs a build.** `toiljs start` serves what is in `build/`. Run `toiljs build` first, or it exits with an error.\n- **Run `toiljs`, not `npx toiljs`, inside npm scripts.** Under `npm run`, `node_modules/.bin` is already on your PATH, so an extra `npx` layer is redundant and can leave your terminal in a broken input mode after `Ctrl+C`. `doctor` warns about this; the scaffold gets it right.\n- **`--host` and `--threads` are `start`-only.** `toiljs dev` always binds locally and does not take `--host` or `--threads`.\n- **`--server` builds the backend only.** Use it while iterating on server code, but run a full `toiljs build` before you deploy so the client bundle is current.\n- **The update notice is not an error.** The \"newer toiljs available\" line is informational and prints to stderr. Set `TOILJS_NO_UPDATE_CHECK=1` to hide it.\n\n## Related\n\n- [Installation](../getting-started/installation.md) and [Create a project](../getting-started/create-project.md)\n- [Project structure](../getting-started/project-structure.md)\n- [Configuration reference (`toil.config.ts`)](../concepts/config.md)\n- [Styling](../frontend/styling.md)\n- [Database setup](../database/setup.md) and the [database overview](../database/README.md)\n- [Compute tiers (L1 to L4)](../concepts/tiers.md)\n",
17
17
  "concepts/ai-guide.md": "# Writing toiljs correctly\n\nThis is a fast, high-signal cheat-sheet for writing toiljs and toilscript code that compiles and behaves. It is aimed at AI assistants generating code (and at humans in a hurry): the patterns to follow, the mistakes to avoid, and a link to the deep page whenever you need more. It does not re-teach the framework; each section points you at the authoritative doc.\n\n## Project shape\n\nA toiljs project has three top-level folders, and knowing which one a file belongs to tells you which rules apply:\n\n- **`client/`**: your React app that runs in the browser. Pages live under `client/routes/`, one file per URL, and each route file `export default`s its component. See [Frontend](../frontend/README.md) and [Routing](../frontend/routing.md).\n- **`shared/`**: a generated typed bridge (`shared/server.ts`) that lets the browser call the backend with full types. It is regenerated on every build, so **never hand-edit it**.\n- **`server/`**: your backend, written in TypeScript but compiled by **toilscript** to WebAssembly. It runs on the Dacely edge, not in Node or the browser. See [Backend](../backend/README.md).\n\nMost code runs on the L1 request tier. Long-lived connections (`@stream`) and scheduled work (`@daemon`) are opt-in tiers in their own entry files. See [Compute tiers](./tiers.md).\n\n## Client rules (the browser, React side)\n\n**Call the backend only through the generated `Server` client.** This is the number one rule. Use `Server.REST.<controller>.<route>(args)` for `@rest` HTTP controllers, or `Server.<service>.<method>(args)` / `Server.<remote>(args)` for RPC. Raw `fetch` to your own backend throws away the type safety, the argument and result decoding, and the binary wire codec, and it silently breaks when a route is renamed. `fetch` is only for third-party URLs (an external API, a CDN). See [Fetching data](../frontend/data-fetching.md).\n\n```ts\n// WRONG: raw fetch to your own backend\nawait fetch('/account/session', { method: 'POST' });\n\n// RIGHT: the typed client (renames become compile errors; args + result typed)\nawait Server.REST.account.session();\n```\n\n**Use the ambient `Toil.*` globals with no import.** `Toil` is the `toiljs/client` package exposed as a global and typed via a generated `toil-env.d.ts`, so it autocompletes with no import: `Toil.Link` / `Toil.NavLink` for navigation, `Toil.useParams`, `Toil.useLoaderData`, `Toil.Image`, `Toil.useHead` / `Toil.Head`, `Toil.Form` / `Toil.useAction`, and more. `Server` and `parseError` are also bare globals; `FastMap`, `FastSet`, `DataWriter`, and `DataReader` are bare globals too (write `new DataWriter()`, not `new Toil.DataWriter()`). Full index: [The Toil global](../frontend/toil-global.md).\n\n**Load page data with a `loader`, not a `useEffect` fetch.** A route file exports a `loader`, and the component reads its result with `Toil.useLoaderData`. The loader runs on navigation in parallel with the route chunk, integrates with `loading.tsx`, caching, and SSR hydration. A `useEffect` fetch runs only after mount (slower, invisible to the server).\n\n```tsx\n// client/routes/blog/[id].tsx\nexport const loader = async ({ params }: Toil.LoaderArgs) => {\n return Server.REST.blog.get({ params: { id: params.id } });\n};\n\nexport default function BlogPost() {\n const post = Toil.useLoaderData<typeof loader>();\n return <article><h1>{post.title}</h1></article>;\n}\n```\n\n**Mutate with `Toil.useAction` or `<Toil.Form>`, then revalidate.** Both track pending and error state and refetch the affected loader data on success, so the page updates without a manual refetch. Use `<Toil.Form>` for form submits, `useAction` for anything else (a delete button, a toggle). Reach for `Toil.revalidate()` / `router.revalidate()` after a write that these do not cover.\n\n**Per-route SEO with `export const metadata`** (or `Toil.useHead` / `<Toil.Head>` from inside a component). Opt a route into edge SSR with `export const ssr = true`. See [Metadata and SEO](../frontend/metadata.md) and [Rendering and SSR](../frontend/rendering.md).\n\n**Typed hrefs.** `Toil.Link` `href`, `Toil.navigate`, and `router.push` are all type-checked against your real routes, so a typo is a compile error. Use `Toil.href(str)` only when a URL is assembled from data and TypeScript cannot prove it is a real route. See [Navigation](../frontend/navigation.md).\n\n**`getUser()` on the client is display-only.** It reads a readable session cookie with no network call, so it is instant but **forgeable**. Use it to render \"logged in as ...\", never to gate real access. The authoritative check is a server route guarded with `@auth`. See [Auth usage](../auth/usage.md).\n\n## Server rules (toilscript, compiles to WASM)\n\nThe decorators, each doing one job (full list in [Decorators](./decorators.md)):\n\n- **`@data`** on a class: generates the binary and JSON codec so the value can cross the wire and the database. Every field needs a default; field order **is** the binary layout, so add new fields at the end. See [Data types](../backend/data.md).\n- **`@rest('name')`** on a class mounts an HTTP controller at `/name`; **`@get`/`@post`/`@put`/`@del`/`@patch`** on its methods declare routes (`@del`, not `@delete`, which is reserved). See [REST](../backend/rest.md).\n- **`@service`** + **`@remote`** expose typed RPC callable from your own frontend as `Server.<service>.<method>()`. See [RPC](../backend/rpc.md).\n- **`@user`** declares the authenticated-user shape and enables `AuthService.getUser()`. **Exactly one `@user` per project.** **`@auth`** guards a route or a whole `@rest` class. See [Auth](../auth/README.md).\n- **`@ratelimit(strategy, limit, window)`** caps how often a caller may hit a route, rejected at the edge before your code runs. See [Security](./security.md).\n- **`@database`** + **`@collection`** declare a ToilDB schema (below).\n\n```ts\n@rest('players')\nclass Players {\n @get('/:id')\n public get(ctx: RouteContext): Response {\n const id = ctx.param('id');\n return Response.json(`{\"id\":\"${id}\"}`);\n }\n}\n```\n\n**`@auth` rejects with 401 before your handler runs.** So inside an `@auth`-guarded handler, `AuthService.getUser()!` is guaranteed non-null and safe to assert. Without `@auth`, `getUser()` can be null, so null-check it instead. Either way the server re-verifies the signed session, so it (not the client cookie) is the real authorization boundary.\n\n```ts\n@auth\n@get('/settings')\npublic settings(): Response {\n const user = AuthService.getUser()!; // safe: @auth guarantees a session\n return Response.text('hi ' + user.username);\n}\n```\n\n**Server code is a strict, WASM-targeted dialect (AssemblyScript), not full TypeScript.** The real constraints, verified against the toilscript standard library:\n\n- **Use explicit value types**, never `number`: `i32` / `u32` / `i64` / `u64` / `f64` (and `i8`/`u8`/`i16`/`u16`/`f32`, plus `u128`..`u256`), `bool`, `string`, `Uint8Array`. Plain `number` resolves to `f64`, which is wrong for ids and counts. Integer math **wraps** on overflow (it never throws), and integer `/` truncates. See [The type system](./types.md).\n- **No `any`.** Every value has a concrete type; that is what lets it compile to WASM.\n- **No arbitrary npm.** Only the toilscript standard library plus the toiljs host APIs.\n- **Use `null`, not `undefined`** for \"no value\" (`T | null`, narrowed with `if (x != null)` or a `!` assertion).\n- **No usable built-in `RegExp`** (it is a host stub that throws). Parse strings by hand.\n- **Structured values are `@data` classes, not object literals.** Encode and decode raw binary with `DataWriter` / `DataReader` (imported on the server from the `data` module: `import { DataWriter, DataReader } from 'data';`). For dynamic JSON, use the ambient `JSON` value tree.\n- **Read config with `Environment.get(key)` and secrets with `Environment.getSecure(key)`** (each returns `string | null`; the two buckets are disjoint so a secret can never leak through `get`). See [Environment](../services/environment.md).\n- **Return a runtime `Response`** (`Response.json` / `.text` / `.html` / `.bytes` / `.notFound` / ...), or return a `@data` value and let toiljs serialize it.\n\n**ToilDB has seven collection families; pick the one that matches the job** (do not force everything into one). Declare each as a `static` `@collection` field on a `@database` class, typed by its family, and reach it statically (`AppDb.users.get(...)`). See [The database](../database/README.md) and [Setup](../database/setup.md).\n\n- **`Documents<K,V>`**: the default record store, looked up by id (users, posts, orders).\n- **`Unique<K,V>`**: a globally one-of-a-kind claim (usernames, emails, slugs).\n- **`Counter<K>`**: a running total many callers bump at once (likes, views); `add` a delta only.\n- **`Events<K,V>`**: an append-only log kept in order (feeds, audit trails).\n- **`Capacity<K>`**: a limited quantity handed out without overselling (tickets, seats).\n- **`Membership<K,M>`**: sets of who belongs to what (followers, tags, room members).\n- **`View<K,V>`**: a precomputed read-optimized snapshot (leaderboards, home pages).\n\n**Data access is gated by function kind.** A `@get` route is a read-only **Query**; a `@post` route is an **Action** (may write); a plain `@remote` defaults to read-only Query, so add **`@action`** to let it write. Scans (`events.latest`, `membership.list`) are barred from request handlers: do them in a `@derive` and have the request read the resulting `View`. The compiler enforces this and the edge re-checks it.\n\n## Common mistakes\n\nA short do-not list. Every one of these is a real, avoidable error:\n\n- **Raw `fetch` to your own backend.** Go through `Server.*` instead.\n- **Importing `Toil.*`.** They are ambient globals; importing them is wrong.\n- **Fetching page data in a `useEffect`.** Use a route `loader` + `Toil.useLoaderData`.\n- **A plain `<a>` for in-app links.** Use `Toil.Link` (a bare `<a>` triggers a full reload).\n- **More than one `@user`.** Exactly one per project.\n- **Trusting client `getUser()` for authorization.** It is display-only and forgeable; enforce on the server with `@auth`.\n- **`any`, `number`, or `RegExp` in server code.** Use explicit value types; there is no usable `RegExp`.\n- **Reordering `@data` fields** (or changing a field type) on a stored type. Field order is the layout; add at the end and use `@migrate` to evolve. See [Data types](../backend/data.md).\n- **Writing from a plain `@remote` or `@get`.** Reads are the default; a write needs `@action`.\n- **Calling a scan (`events.latest`, `membership.list`) from a request handler.** Do it in a `@derive`.\n- **Hand-editing `shared/server.ts` or `.toil/`.** They are generated; rebuild instead.\n\n## Where to go deeper\n\n- Concepts: [Decorators](./decorators.md), [Type system](./types.md), [Security](./security.md), [Compute tiers](./tiers.md).\n- Backend: [Overview](../backend/README.md), [REST](../backend/rest.md), [RPC](../backend/rpc.md), [Data types](../backend/data.md).\n- Frontend: [Overview](../frontend/README.md), [Fetching data](../frontend/data-fetching.md), [Routing](../frontend/routing.md), [Navigation](../frontend/navigation.md), [The Toil global](../frontend/toil-global.md).\n- Database: [ToilDB overview](../database/README.md), [Setup](../database/setup.md).\n- Auth: [Overview](../auth/README.md), [Usage](../auth/usage.md).\n- Services: [Environment](../services/environment.md), [Rate limiting](../services/ratelimit.md).\n</content>\n</invoke>\n",
18
18
  "concepts/config.md": "# Configuration (`toil.config.ts`)\n\n`toil.config.ts` is the one file that configures your whole toiljs app: the client (React/Vite) side and the server (WebAssembly) side. Every field is optional and has a sensible default, so most projects keep it tiny.\n\n## The shortest possible config\n\nA scaffolded project ships something like this:\n\n```ts\n// toil.config.ts\nimport { defineConfig } from 'toiljs/compiler';\n\nexport default defineConfig({\n client: {\n // Optimize images at build time (resize and compress imported images).\n images: true,\n },\n});\n```\n\n`defineConfig` does not do anything at runtime: it is an identity helper that gives you full editor autocomplete and type-checking for the config shape. Always wrap your config in it.\n\nAn empty config is valid too. `export default defineConfig({})` gives you a working app with all defaults.\n\n## Where the file lives\n\ntoiljs looks in your project root for the first file that matches, in this order:\n\n```\ntoil.config.ts toil.config.mts toil.config.js toil.config.mjs\ntoiljs.config.ts toiljs.config.mts toiljs.config.js toiljs.config.mjs\n```\n\nUse whichever you like. `toil.config.ts` is the convention.\n\n## Config vs. environment variables\n\nThere are two very different places settings live, and it matters which one you use.\n\n| | `toil.config.ts` (this page) | Environment variables |\n| --- | --- | --- |\n| **When it applies** | Build time and dev time | Runtime, per request |\n| **What it holds** | Framework and build options (routing, styling, SEO, which features to compile) | Values and secrets your running server reads (API keys, feature flags, connection info) |\n| **Committed to git?** | Yes, it is source | No. Local values go in `.env` / `.env.secrets` (both gitignored); production values live on your deploy target |\n| **Read in code with** | Not read from your app code | `Environment.get(...)` / `Environment.getSecure(...)` on the server |\n\nRule of thumb: if it is a **secret** (a password, an API key, a session key), it does **not** go in `toil.config.ts`. It goes in the environment. See [Environment and secrets](../services/environment.md) for the full story.\n\nThere is also a **third** file, `toilconfig.json`, which is a different thing entirely. See [`toil.config.ts` is not `toilconfig.json`](#toilconfigts-is-not-toilconfigjson) at the bottom.\n\n## The top-level shape\n\n```ts\ninterface ToilConfig {\n root?: string; // project root (defaults to the current working directory)\n client?: ClientConfig; // the React / Vite frontend\n server?: ServerConfig; // the toilscript / WebAssembly backend\n}\n```\n\nEverything else lives under `client` or `server`.\n\n## `client` reference\n\nConfigures the frontend: source folders, the dev server, and build-time optimizations.\n\n| Field | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `srcDir` | `string` | `\"client\"` | Your frontend source directory, relative to the project root. |\n| `routesDir` | `string` | `\"routes\"` | Your file-based routes directory, relative to `srcDir`. |\n| `publicDir` | `string` | `\"<srcDir>/public\"` | Static assets directory. Holds `index.html` (which you own) plus files served as-is (favicons, images). |\n| `outDir` | `string` | `\"build/client\"` | Where the production client bundle is written. |\n| `base` | `string` | `\"/\"` | The public base path. Use this if you serve the app under a sub-path (a non-root base should start and end with `/`, like `\"/app/\"`). |\n| `port` | `number` | `3000` | The dev server port. `--port` on the CLI overrides it. |\n| `images` | `boolean` | `true` | Optimize imported images at build time (resize and convert). See [Images](../frontend/images.md). |\n| `fonts` | `boolean` | `true` | Preload bundled fonts (inject `<link rel=\"preload\">` for each `@font-face`) so text paints faster. |\n| `viewTransitions` | `boolean` | `false` | Animate page navigations with the browser View Transitions API (a crossfade by default). Respects `prefers-reduced-motion`. |\n| `transitions` | `boolean` | `false` | Wrap navigations in a React transition, keeping the current page visible while the next route's loader runs (instead of showing its loading state immediately). |\n| `devtools` | `boolean` or object | `true` | The floating dev toolbar (route/build info, errors, live controls). It is dev-only and never ships in production. Set `false` to disable, or pass an object to configure its AI integration. |\n| `seo` | object | (off) | Build-time SEO: bakes site-level metadata into the HTML and generates `robots.txt`, `sitemap.xml`, `llms.txt`. See [`client.seo`](#clientseo) below. |\n| `vite` | Vite `InlineConfig` | `{}` | An escape hatch: raw Vite options, deep-merged over the framework's own Vite setup. toiljs owns the Vite config; use this only to override specific options. |\n\n### Example\n\n```ts\nimport { defineConfig } from 'toiljs/compiler';\n\nexport default defineConfig({\n client: {\n images: true,\n fonts: true,\n viewTransitions: true,\n // Serve the app under https://example.com/app/\n base: '/app/',\n },\n});\n```\n\n### `client.devtools`\n\nThe dev toolbar is on by default. To turn it off, or to give it an AI provider (so its \"explain this error\" helpers can call a model), pass an object:\n\n```ts\nimport { defineConfig, AiProvider } from 'toiljs/compiler';\n\nexport default defineConfig({\n client: {\n devtools: {\n ai: {\n provider: AiProvider.Anthropic, // 'anthropic' or 'openai'\n model: 'claude-sonnet-4-6',\n // The name of the env var holding the API key. It is read\n // server-side by the dev process and never sent to the browser.\n apiKeyEnv: 'ANTHROPIC_API_KEY',\n // Optional: a custom POST endpoint ({ prompt } in, { text } out)\n // that overrides `provider` entirely.\n // endpoint: 'http://localhost:5000/ai',\n },\n },\n },\n});\n```\n\n| `devtools.ai` field | Type | What it does |\n| --- | --- | --- |\n| `provider` | `AiProvider` | Built-in provider: `AiProvider.Anthropic` (`'anthropic'`) or `AiProvider.OpenAI` (`'openai'`). |\n| `model` | `string` | The model id, for example `'claude-sonnet-4-6'` or `'gpt-4o'`. |\n| `apiKeyEnv` | `string` | The **name** of the environment variable that holds the API key. The key stays server-side and never reaches the browser. |\n| `endpoint` | `string` | A custom POST endpoint. When set, it takes precedence over `provider`. |\n\nThe toolbar always offers hand-off links to Claude and ChatGPT even without any AI config; this only enables the in-toolbar helpers.\n\n### `client.seo`\n\n`client.seo` turns on build-time search-engine and social metadata. It bakes tags into your HTML `<head>` (so crawlers and link-preview bots that do not run JavaScript still see real metadata) and generates `robots.txt`, `sitemap.xml`, and `llms.txt`. This is a large section with its own guide; see [Metadata and SEO](../frontend/metadata.md) for the complete field list and examples. The one thing worth knowing here: set `seo.url` to your absolute site URL (like `\"https://example.com\"`), because the sitemap and canonical links need it.\n\n```ts\nexport default defineConfig({\n client: {\n seo: {\n url: 'https://example.com',\n title: 'My App',\n description: 'A toiljs app.',\n },\n },\n});\n```\n\n## `server` reference\n\nConfigures the backend: which platform features to compile in, and how the dev server and self-host behave.\n\n| Field | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `auth` | `boolean` | `false` | Opt into the framework's built-in post-quantum login. See [`server.auth`](#serverauth) below. |\n| `email` | object | (off) | The non-secret email backend config for the dev server and self-host. See [`server.email`](#serveremail) below. |\n| `daemon` | object | (defaults) | Background-job (L4 daemon) settings for dev and self-host. See [`server.daemon`](#serverdaemon) below. |\n| `nodeMode` | string | `\"all\"` | Which compute layer the single dev/self-host process emulates. See [`server.nodeMode`](#servernodemode) below. |\n| `threads` | `number` or `\"auto\"` | `\"auto\"` | HTTP worker count for `toiljs start`. `\"auto\"` uses one per CPU; `1` disables the worker pool. `--threads` on the CLI overrides it. |\n| `srcDir` | `string` | `\"server\"` | Declarative: your server source directory. See the note below. |\n| `outDir` | `string` | `\"build/server\"` | Declarative: the server build output directory. See the note below. |\n\n> **Note on `server.srcDir` / `server.outDir`.** These fields exist in the config type, but the actual location of your server source and its compiled output is driven by `toilconfig.json` (the toilscript compiler config), not by these two fields. Change your server paths in `toilconfig.json`. These fields are currently declarative and left for forward compatibility. See [not `toilconfig.json`](#toilconfigts-is-not-toilconfigjson) below.\n\n### `server.auth`\n\nSet `auth: true` to get a complete post-quantum login system with no boilerplate. The build appends a shipped `@rest('auth')` controller and its `@user` shape to your server, giving you `/auth/register`, `/auth/login`, `/auth/me`, and `/auth/logout` plus sessions.\n\n```ts\nexport default defineConfig({\n server: {\n auth: true,\n },\n});\n```\n\nTwo things to know:\n\n- If you opt in, your app must **not** declare its own `@user` type. The built-in auth owns the single per-program one.\n- There is an escape hatch: adding `import 'toiljs/server/auth'` in `server/main.ts` turns on the same built-in auth surface without this flag.\n\nAuth has its own configuration (session secrets, the OPRF and KEM keys) that lives in the **environment**, not here. See [Auth configuration](../auth/configuration.md).\n\n### `server.email`\n\nThe **non-secret** part of your email setup: which provider, the \"from\" address, and send caps. The dev server and self-host read it. The API key or SMTP password is a **secret** and never goes here; it comes from `.env.secrets` (`TOIL_EMAIL_API_KEY`). Any `TOIL_EMAIL_*` environment variable overrides the matching field here.\n\n```ts\nexport default defineConfig({\n server: {\n email: {\n provider: 'resend',\n from: 'hello@example.com',\n maxPerMin: 60,\n },\n },\n});\n```\n\n| Field | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `provider` | `'resend'` \\| `'gmail'` \\| `'smtp'` | `'resend'` | Which email backend to use. |\n| `from` | `string` | (none) | The \"from\" address. Validated (single address, no line breaks). |\n| `maxPerMin` | `number` | `60` | Per-process send ceiling per minute (rolling). `0` means unlimited. |\n| `maxPerDay` | `number` | `0` | Per-process send ceiling per day (rolling). `0` means unlimited. |\n| `maxPerRecipientPerHour` | `number` | `5` | Per-recipient hourly cap (anti-abuse). |\n| `smtp` | object | (none) | Connection details for the `gmail` / `smtp` providers: `host`, `port` (defaults to 587 STARTTLS; 465 is implicit TLS), and `user` (defaults to `from`). |\n\nSee the full guide at [Email and 2FA](../services/email.md).\n\n### `server.daemon`\n\nSettings for the daemon (L4) background layer, used by the dev process and self-host. In dev, the local process is always the leader, so region fields are informational.\n\n```ts\nexport default defineConfig({\n server: {\n daemon: {\n defaultIntervalMs: 60000,\n maxTasks: 64,\n },\n },\n});\n```\n\n| Field | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `region` | `string` | (none) | Region the daemon is pinned to (informational in dev). |\n| `standbyRegion` | `string` | (none) | Warm standby region (informational in dev). |\n| `defaultIntervalMs` | `number` | `60000` | Default interval for a `@scheduled` task that declares none. Values below `1000` are clamped up to `1000` (a sub-second loop would flood the console). |\n| `tickBudgetMs` | `number` | `30000` | Per-tick wall-clock budget before the dev scheduler logs an overrun. |\n| `gasTick` | `number` | `0` | Per-tick gas cap (a dev stub: charged then ignored). |\n| `maxTasks` | `number` | `64` | Maximum number of `@scheduled` tasks. Clamped to the range 1 to 1024. |\n\nSee [Daemons and scheduled jobs](../background/daemons.md).\n\n### `server.nodeMode`\n\nWhich compute layer the single local process emulates. This is a dev and self-host knob only; in production the Dacely edge decides each server's role. Valid values are `hot`, `regional`, `continental`, `daemon`, and `all`. The default, `all`, runs every surface (requests, streams, and daemons) in one process, which is what you want for a full local run. An invalid value falls back to `all` with a warning rather than failing. See [Compute tiers (L1 to L4)](../concepts/tiers.md) for what each layer means.\n\n```ts\nexport default defineConfig({\n server: {\n nodeMode: 'all', // run everything locally (the default)\n },\n});\n```\n\n## Defaults at a glance\n\nIf you write nothing, this is what you get.\n\n| Setting | Default |\n| --- | --- |\n| `client.srcDir` | `\"client\"` |\n| `client.routesDir` | `\"routes\"` |\n| `client.publicDir` | `\"client/public\"` |\n| `client.outDir` | `\"build/client\"` |\n| `client.base` | `\"/\"` |\n| `client.port` | `3000` |\n| `client.images` | `true` |\n| `client.fonts` | `true` |\n| `client.viewTransitions` | `false` |\n| `client.transitions` | `false` |\n| `client.devtools` | on |\n| `client.seo` | off |\n| `server.auth` | `false` |\n| `server.email` | off |\n| `server.nodeMode` | `\"all\"` |\n| `server.threads` | `\"auto\"` |\n| `server.daemon.defaultIntervalMs` | `60000` |\n| `server.daemon.tickBudgetMs` | `30000` |\n| `server.daemon.maxTasks` | `64` |\n\n## A fuller example\n\n```ts\nimport { defineConfig, AiProvider } from 'toiljs/compiler';\n\nexport default defineConfig({\n client: {\n images: true,\n fonts: true,\n viewTransitions: true,\n seo: {\n url: 'https://example.com',\n title: 'Example',\n description: 'Built with toiljs.',\n },\n devtools: {\n ai: { provider: AiProvider.Anthropic, model: 'claude-sonnet-4-6', apiKeyEnv: 'ANTHROPIC_API_KEY' },\n },\n },\n server: {\n auth: true,\n email: { provider: 'resend', from: 'hello@example.com' },\n threads: 'auto',\n },\n});\n```\n\n## `toil.config.ts` is not `toilconfig.json`\n\nThese two look almost the same and are easy to confuse. They are not the same file.\n\n| File | What it is |\n| --- | --- |\n| `toil.config.ts` | **This page.** The framework config: client and server options, styling, SEO, which features to build. You edit it often. |\n| `toilconfig.json` | The **toilscript compiler** config: which server files are entry points, where the `.wasm` is written, and low-level WebAssembly options (optimization level, memory layout, enabled wasm features). It is scaffolded for you and you rarely touch it. Its presence is also what tells toiljs \"this project has a server.\" |\n\nIf you ever need to change where your server source lives or what the compiled artifact is named, that is `toilconfig.json`, not `toil.config.ts`.\n\n## The `toilconfig.json` reference\n\n`toilconfig.json` is the **toilscript compiler** config. toilscript is the compiler that turns your `server/` TypeScript into a `.wasm` file (WebAssembly, the sandboxed binary your backend ships as). This file tells toilscript which server files to compile, where to write the output, and which low-level WebAssembly options to use.\n\n`toiljs create` scaffolds it for you and most projects never touch it. You only edit it if you want to rename the compiled artifact, move your server entry, or hand-tune the WebAssembly codegen. Its presence at the project root is also the signal toiljs uses to decide \"this project has a server\" (a project with no `toilconfig.json` is a client-only app).\n\nA scaffolded file looks like this:\n\n```json\n{\n \"entries\": [\"server/main.ts\"],\n \"targets\": {\n \"release\": {\n \"outFile\": \"build/server/release.wasm\",\n \"textFile\": \"build/server/release.wat\"\n }\n },\n \"options\": {\n \"sourceMap\": false,\n \"optimizeLevel\": 3,\n \"shrinkLevel\": 1,\n \"converge\": true,\n \"noAssert\": false,\n \"enable\": [\n \"sign-extension\",\n \"mutable-globals\",\n \"nontrapping-f2i\",\n \"bulk-memory\",\n \"simd\",\n \"reference-types\",\n \"multi-value\"\n ],\n \"runtime\": \"stub\",\n \"lib\": [\"node_modules/toiljs/server/globals\"],\n \"memoryBase\": 65536,\n \"initialMemory\": 4,\n \"debug\": false,\n \"trapMode\": \"allow\"\n }\n}\n```\n\n### `entries`\n\nAn array of your server entry files: the toilscript starting points for the compile. The scaffold lists just `server/main.ts`, and `main.ts` imports your other surface modules so they all get pulled in. (Under `toiljs build`, toiljs compiles every decorated server file it finds, not only the entries, so a `@rest` or `@data` file you drop in is picked up even if `main.ts` does not import it.)\n\nA project that also has a streams tier or a daemon tier lists their entry files here too, so each tier can compile into its own artifact:\n\n```json\n\"entries\": [\"server/main.ts\", \"server/main.stream.ts\", \"server/main.daemon.ts\"]\n```\n\n### `targets`\n\nA map of named build targets. Each target names its output files. The scaffold has one target, `release`.\n\n| Field | Type | What it does |\n| --- | --- | --- |\n| `outFile` | `string` | Where the compiled `.wasm` is written. The scaffold uses `build/server/release.wasm`. |\n| `textFile` | `string` | Where the `.wat` (WebAssembly **text** format, the human-readable text form of the same module) is written. Handy for inspecting the output; not needed at runtime. |\n\n### `options`\n\nLow-level WebAssembly codegen options passed straight to toilscript. The defaults are already tuned for production, so change these only if you know you need to.\n\n| Field | Type | Scaffold value | What it controls |\n| --- | --- | --- | --- |\n| `sourceMap` | `boolean` | `false` | Emit a source map alongside the `.wasm` so a debugger can map machine code back to your TypeScript. Off by default (it makes the build bigger). |\n| `optimizeLevel` | `number` | `3` | How hard the optimizer works on **speed**, from `0` (none) to `3` (most). `3` is a production release setting. |\n| `shrinkLevel` | `number` | `1` | How hard the optimizer works on **size**, from `0` to `2`. `1` trades a little speed for a smaller binary. |\n| `converge` | `boolean` | `true` | Re-run the optimizer until the output stops getting better. Squeezes out a bit more at the cost of a slower build. |\n| `noAssert` | `boolean` | `false` | Strip `assert(...)` checks from the output (replace them with just their value, no trap). `false` keeps the safety checks in. |\n| `enable` | `string[]` | (see below) | Which modern WebAssembly features the compiled module is allowed to use. |\n| `runtime` | `string` | `\"stub\"` | The memory-management runtime baked into the module. `\"stub\"` is a minimal runtime that never frees memory, which fits toiljs's model exactly: the edge runs one fresh instance per request and throws its whole memory away when the request ends, so there is nothing to garbage-collect. |\n| `lib` | `string[]` | `[\"node_modules/toiljs/server/globals\"]` | Extra library paths whose top-level exports become **ambient globals** (usable with no `import`). This is what makes toiljs's server globals (like `crypto` and the auth primitives) available everywhere in `server/`. |\n| `memoryBase` | `number` | `65536` | The byte offset where your server's static data starts. toiljs reserves the first 64 KiB (`[0, 65536)`) for the **request envelope** the edge writes at offset 0, so a large request body can never overwrite your program's state. Raise it to accept larger request bodies (it costs a little more initial memory). |\n| `initialMemory` | `number` | `4` | How much linear memory the module starts with, in **pages**. One WebAssembly page is 64 KiB, so `4` is 256 KiB. It grows on demand past this. |\n| `debug` | `boolean` | `false` | Include debug information (names and the like) in the binary. Off for production. |\n| `trapMode` | `string` | `\"allow\"` | What happens on a trapping operation (like a bad float-to-int conversion). `\"allow\"` lets it trap (the default and correct choice); `\"clamp\"` replaces traps with clamping instead. |\n\nThe `enable` array turns on WebAssembly features that are off by default in the compiler. The scaffold enables the modern set the compiled server relies on: `sign-extension`, `mutable-globals`, `nontrapping-f2i` (non-trapping float-to-int conversions), `bulk-memory` (fast `memory.copy` / `memory.fill`), `simd` (vector operations), `reference-types`, and `multi-value` (functions that return more than one value). Leave this list as scaffolded unless you have a specific reason to change it; removing an entry can make the module fail to compile or run.\n\n### The `--rpcModule` build flag\n\nOne toilscript flag is worth knowing even though it lives in your npm scripts, not in `toilconfig.json`: `--rpcModule shared/server.ts`. It tells the compiler to also emit `shared/server.ts`, the fully typed client the browser imports to call your server (the `@data` codec plus the typed `Server` surface). toiljs adds this flag for you on the request build. If your `build:server` script is missing it (older projects predate it), `toiljs doctor --fix` injects it. See [the CLI reference](../cli/README.md#what---fix-repairs).\n\n## Gotchas\n\n- **Wrap the config in `defineConfig`.** Without it you lose autocomplete and type errors on typos.\n- **Secrets never go here.** API keys, session secrets, and passwords belong in the environment, not in `toil.config.ts` (which is committed to git). See [Environment and secrets](../services/environment.md).\n- **`server.srcDir` / `server.outDir` do not move your server.** The server source location is governed by `toilconfig.json` entries. Editing these two config fields has no effect today.\n- **`nodeMode` and `daemon` are dev/self-host only.** In production the edge assigns each server its role; these settings never override that.\n- **An invalid `nodeMode` does not crash the build.** It warns and falls back to `all`.\n\n## Related\n\n- [The CLI](../cli/README.md): the commands that read this config.\n- [Environment and secrets](../services/environment.md): runtime values and how they differ from build-time config.\n- [Auth configuration](../auth/configuration.md): the auth-related environment settings behind `server.auth`.\n- [Email and 2FA](../services/email.md): the full email setup behind `server.email`.\n- [Metadata and SEO](../frontend/metadata.md): the full `client.seo` field list.\n- [Daemons and scheduled jobs](../background/daemons.md): the background layer behind `server.daemon`.\n- [Compute tiers (L1 to L4)](../concepts/tiers.md): what `server.nodeMode` selects.\n- [Images](../frontend/images.md) and [Styling](../frontend/styling.md): the features `client.images` and `toiljs configure` control.\n",
19
19
  "concepts/decorators.md": "# Decorators reference\n\nEvery feature of a toiljs backend, an HTTP route, an RPC method, a database collection, a scheduled job, is switched on by a **decorator**. This page lists them all, grouped by what they do, so you can find the right one at a glance and jump to the page that covers it in depth.\n\n## What a decorator is\n\nA **decorator** is the `@name` you write on the line just above a class, a method, or a field. It attaches meaning to that code without changing what the code itself does. You are labelling it so the compiler knows how to wire it up.\n\n```ts\n@rest('users') // <- a class decorator: \"this class is an HTTP controller\"\nclass Users {\n @get('/:id') // <- a method decorator: \"this method answers GET /users/:id\"\n public byId(): Response { /* ... */ }\n}\n```\n\nThree things to notice, because they decide where each decorator can go:\n\n- Some decorators apply to a **class** (`@rest`, `@service`, `@stream`, `@daemon`, `@database`, `@data`, `@user`).\n- Some apply to a **method** or a free **function** (`@get`, `@remote`, `@scheduled`, `@query`).\n- One applies to a **field** (`@collection`).\n\nSome take arguments, like `@get('/:id')` or `@cache(60)`; the bare ones, like `@rest` or `@daemon`, do not. You never register anything by hand: tagging the code is enough, and the build discovers it.\n\nEach decorator also belongs to a **tier** (where its code runs) or is **shared** (compiled into every tier). If tiers are new to you, read [Compute tiers](./tiers.md) first; the short version is L1 = per-request at the edge, L2 / L3 = long-lived stream connections, L4 = one global daemon.\n\n## Routing and HTTP (L1)\n\nTurn a class into an HTTP controller and its methods into routes. All run on the L1 request tier. Covered in [REST](../backend/rest.md).\n\n| Decorator | Applies to | What it does |\n| --- | --- | --- |\n| `@rest` | class | Marks the class an HTTP controller, mounted at a prefix (`@rest('users')` -> `/users`). |\n| `@route` | method | Declares a route with an explicit method + path: `@route({ method: Methods.GET, path: '/' })`. |\n| `@get` | method | Shorthand for a `GET` route: `@get('/:id')`. |\n| `@post` | method | Shorthand for a `POST` route. |\n| `@put` | method | Shorthand for a `PUT` route. |\n| `@del` | method | Shorthand for a `DELETE` route (named `del` because `delete` is a reserved word). |\n| `@patch` | method | Shorthand for a `PATCH` route. |\n| `@head` | method | Shorthand for a `HEAD` route. |\n| `@options` | method | Shorthand for an `OPTIONS` route. |\n\n## RPC (L1)\n\nExpose server functions that your own frontend calls like typed async functions. Run on L1. Covered in [RPC](../backend/rpc.md).\n\n| Decorator | Applies to | What it does |\n| --- | --- | --- |\n| `@service` | class | Marks an RPC service; its `@remote` methods are namespaced under the generated client as `Server.<service>.<method>()`. |\n| `@remote` | method / function | Marks a method (of a `@service`) or a top-level function as a client-callable RPC endpoint. |\n\n## Guards and policy (L1)\n\nAttach a rule to a route (or a whole controller). These stack above a route method. Run on L1.\n\n| Decorator | Applies to | What it does | Covered in |\n| --- | --- | --- | --- |\n| `@auth` | class / method | Requires a valid session; returns `401` otherwise. On a class it guards every route. | [Auth usage](../auth/usage.md) |\n| `@cache` | method | Caches the response at the edge and browser: `@cache(edgeMinutes, browserSeconds?, privateScope?, allowAuth?)`. | [Caching](../services/caching.md) |\n| `@ratelimit` | method | Rate-limits a route: `@ratelimit(strategy, limit, window)`. | [Rate limiting](../services/ratelimit.md) |\n\n## Realtime streams (L2 / L3)\n\nHandle a long-lived connection, keeping state per connected client. The `@stream` class runs on the L2 / L3 stream tier; its lifecycle-hook methods fire as events arrive. Covered in [Realtime streams](../realtime/streams.md).\n\n| Decorator | Applies to | What it does |\n| --- | --- | --- |\n| `@stream` | class | Marks a stream protocol handler. `@stream('name')` sets the mount name; `@stream({ scope })` picks Regional (L2) or Continental (L3). |\n| `@connect` | method | Lifecycle hook: fires when a client connects (returns a `StreamOutbound` accept/reject). |\n| `@message` | method | Lifecycle hook: fires on each inbound packet. |\n| `@close` | method | Lifecycle hook: fires on a graceful close. |\n| `@disconnect` | method | Lifecycle hook: fires on an abrupt transport loss. |\n\nA server-side broadcast hook, `@channel`, is planned but not live in the current runtime. See [Channels](../realtime/channels.md) for the status and the working client-side `useChannel` hook you can use today.\n\n## Background and daemon (L4)\n\nRecurring, run-once-globally background work. `@daemon` runs on the single L4 leader. Covered in [Daemons](../background/daemons.md).\n\n| Decorator | Applies to | What it does |\n| --- | --- | --- |\n| `@daemon` | class | Marks the single daemon class (at most one per project). May declare a zero-arg `onStart()` run once at boot. |\n| `@scheduled` | method | Runs the method on a cadence: an interval (`'30s'`, `'5m'`, `'1h'`, `'1d'`) or a 5-field cron string (`'15 9 * * 1-5'`). |\n\n## Database structure (shared)\n\nDeclare your database schema. These carry no tier of their own; they are compiled into every artifact so any tier can use them. Covered in the [database section](../database/README.md).\n\n| Decorator | Applies to | What it does |\n| --- | --- | --- |\n| `@database` | class | Marks a class as a ToilDB database; each `@collection` field becomes a typed handle (`App.users.get(...)`). |\n| `@collection` | field | Declares a field as a collection handle (`Documents` / `View` / `Unique` / `Counter` / `Events` / `Membership` / `Capacity`). |\n| `@data` | class | Marks a class serializable: the compiler generates a binary codec so it can cross the wire and the database. See [Data types](../backend/data.md). |\n| `@migrate` | function | Schema migration for a `@data` type: a free function that upgrades a record written under an old layout to the current shape (lives in `migrations/<Type>.migration.ts`). See [Data types](../backend/data.md). |\n\n## Database function kinds (data-access policy)\n\nA **function kind** labels a function or method with *which* database operations it is allowed to issue, and the compiler enforces it (a read-only `@query` that tries to write is a compile error). Covered across the [database section](../database/README.md).\n\n| Decorator | Applies to | What it does | Covered in |\n| --- | --- | --- | --- |\n| `@query` | function / method | Read-only data access. | [Database](../database/README.md) |\n| `@action` | function / method | Read plus bounded writes and claims. | [Database](../database/README.md) |\n| `@derive` | function / method | Publishes materialized views and rollups (a background materializer). | [@derive](../background/derive.md) |\n| `@job` | function / method | Background work. | [Background work](../background/README.md) |\n| `@admin` | function / method | Control-plane only operations. | [Database](../database/README.md) |\n\n## Auth and structure\n\n| Decorator | Applies to | What it does | Covered in |\n| --- | --- | --- | --- |\n| `@user` | class | Declares the authenticated-user shape; enables the typed `AuthService.getUser()`. At most one per project. | [Auth usage](../auth/usage.md) |\n| `@main` | function | Marks a single top-level function as the module entry point (exported as the WASM `main`). Rarely written by hand: toiljs supplies the entry glue. | [Project structure](../getting-started/project-structure.md) |\n\n## A note on low-level decorators\n\nAssemblyScript (the language toilscript is built on) has its own low-level decorators such as `@inline`, `@unsafe`, and `@operator`. They tune how a symbol compiles and are for advanced library authors, not everyday app code. You will not need them to build a normal toiljs app, and they are out of scope here.\n\n## Related\n\n- [Compute tiers](./tiers.md): where each decorator's code runs (L1 to L4).\n- [The type system](./types.md): the types (`u64`, `string`, `@data`) these decorators work with.\n- [REST](../backend/rest.md) and [RPC](../backend/rpc.md): the L1 surfaces.\n- [Realtime streams](../realtime/streams.md): the `@stream` surface.\n- [Daemons](../background/daemons.md) and [@derive](../background/derive.md): L4 and background work.\n- [The database (ToilDB)](../database/README.md): `@database`, `@collection`, and the function kinds.\n- [Auth](../auth/README.md): `@auth` and `@user`.\n",
@@ -33,7 +33,7 @@ export const TOIL_DOCS = {
33
33
  "frontend/data-fetching.md": "# Fetching data\n\nYour React frontend and your toiljs backend live in one project, so toiljs generates a **typed client** that lets the browser call your server with full type safety and no hand-written `fetch` boilerplate. This page covers loading data for a page, calling the backend directly, submitting forms, and reading who is logged in.\n\n## The generated `Server` client\n\nWhen you build the server, toiljs writes a file at `shared/server.ts` that contains your `@data` classes and a typed description of every backend endpoint. Importing anything from `shared/server` attaches the runtime clients to a global called `Server`. From then on you call your backend through `Server`, fully typed, with editor autocomplete.\n\nThere are two surfaces under `Server`:\n\n- **`Server.REST.<controller>.<route>(args)`**: a real, typed `fetch` client for your `@rest` HTTP controllers. This is the working, recommended way to call the backend today.\n- **`Server.<service>.<method>(args)` and `Server.<remote>(args)`**: the typed RPC surface for `@service` / `@remote` functions.\n\nBoth are generated from your server code, so if you rename a route or change an argument type, the call site is a compile error until you fix it.\n\n> **Rule: never call your own backend with raw `fetch`. Always go through `Server`.**\n> Use `Server.REST.<controller>.<route>(args)` for `@rest` HTTP controllers, or\n> `Server.<service>.<method>(args)` / `Server.<remote>(args)` for `@service` / `@remote`\n> RPC. Raw `fetch` throws away the type safety, the argument/return decoding, and the\n> binary wire format that the generated client handles for you, and it silently breaks\n> when a route is renamed. This applies to AI assistants generating code too.\n>\n> ```ts\n> // WRONG: raw fetch to your backend\n> await fetch('/account/session', { method: 'POST', credentials: 'same-origin' });\n>\n> // RIGHT: the typed client (renames become compile errors, args + result are typed)\n> await Server.REST.account.session();\n> ```\n>\n> `fetch` is only for URLs that are NOT your toiljs backend (a third-party API, a CDN).\n\n## Calling a REST endpoint\n\n`Server.REST` mirrors your `@rest` controllers. If your backend has a `players` controller with routes on it, you call them like this:\n\n```tsx\nimport { NewPlayer, ScoreDelta } from 'shared/server';\n\n// POST /players with a typed @data body -> typed Promise<Player>\nconst player = await Server.REST.players.create({ body: new NewPlayer('Ada') });\n\n// POST /players/:id/score with a path param AND a body\nconst updated = await Server.REST.players.addScore({\n params: { id: 1 },\n body: new ScoreDelta(5n),\n});\n\n// GET /leaderboard -> typed Promise<Standings>\nconst board = await Server.REST.leaderboard.top();\n```\n\nThe single argument is an object with up to four optional parts:\n\n| Key | What it is |\n| --- | --- |\n| `params` | Path parameters, e.g. `{ id: 1 }` for a `/players/:id` route. |\n| `body` | The request body, usually a `@data` class instance. |\n| `query` | Query-string values. |\n| `headers` | Extra request headers. |\n\nReturn values are typed and decoded for you. A route that returns a `@data` type hands you the parsed class instance. A route that returns a raw `Response` hands you the raw fetch `Response`, so you can inspect the status and headers yourself:\n\n```tsx\n// This route returns a Response, so you get the raw fetch Response.\nconst res = await Server.REST.players.get({ params: { id: 1 } });\nif (!res.ok) {\n console.log('status', res.status);\n} else {\n const p = await res.json();\n}\n```\n\n`@data` classes are the typed values that cross the wire. You import them from `shared/server` and construct them normally (`new NewPlayer('Ada')`). See [Data types](../backend/data.md) for how they are defined on the server.\n\n### Handling errors\n\nA failed call throws. The global `parseError` helper turns any caught value into a readable message, which is handy in a `catch`:\n\n```tsx\ntry {\n const board = await Server.REST.leaderboard.top();\n} catch (err) {\n console.error(parseError(err));\n}\n```\n\n## Loading data for a page (loaders)\n\nCalling the backend from a button handler (as above) is fine for actions. For the data a page needs to *render*, use a route **loader** instead of a `useEffect`. A loader runs on navigation, in parallel with loading the page's code, and the page suspends (showing its `loading.tsx`) until the data is ready:\n\n```tsx\n// client/routes/blog/[id].tsx\nexport const loader = async ({ params }: Toil.LoaderArgs) => {\n return Server.REST.blog.get({ params: { id: params.id } });\n};\n\nexport default function BlogPost() {\n const post = Toil.useLoaderData<typeof loader>();\n return <article><h1>{post.title}</h1></article>;\n}\n```\n\n`useLoaderData<typeof loader>()` is fully typed from the loader's return. This keeps data fetching declarative and out of effects, and it is what lets server-rendered pages seed the browser with the server's data so hydration stays clean.\n\n### Caching and revalidating loader data\n\nLoader results are cached by URL. You control how long with `export const revalidate`:\n\n| `revalidate` value | Behavior |\n| --- | --- |\n| `0` (default) | Re-run the loader on every navigation to the route. |\n| a number `n` | Reuse cached data for `n` seconds, then refetch. |\n| `false` | Cache until you invalidate it manually. |\n\nAfter a mutation (say you just POSTed a new comment), refresh the current page's data with `revalidate()` or the router:\n\n```tsx\nconst router = Toil.useRouter();\nawait Server.REST.comments.add({ body: new NewComment(text) });\nrouter.revalidate(); // refetch the active route's loader\n// or target another route: router.revalidate('/posts');\n```\n\n`router.refresh()` re-runs the current loader and clears the cache; `router.revalidate(href)` invalidates a specific route.\n\n### Invalidating loader data by hand\n\n`revalidate()` and the `<Toil.Form>` `revalidate` prop are both built on one lower-level primitive: `Toil.invalidateLoaderData(href?)`. It drops cached loader data so the next render refetches it, but it does **not** trigger the re-render itself (that is the difference from `revalidate()`, which does both):\n\n- `invalidateLoaderData()` with no argument clears every route's cached data.\n- `invalidateLoaderData('/posts')` clears just that one route's entry.\n\nYou rarely need it directly; `revalidate()` (which pairs it with a re-render) is what you usually want. Reach for the primitive when you want to mark data stale *now* but let a later navigation do the actual refetch:\n\n```tsx\n// After a background sync finished, mark the dashboard stale so its\n// next visit refetches, without disturbing the page the user is on.\nToil.invalidateLoaderData('/dashboard');\n```\n\nSignature: `invalidateLoaderData(href?: string): void`.\n\n## Forms\n\nFor the common \"submit a form, then refresh the page's data\" loop, use `Toil.Form`. It runs an async action on submit (no page reload), tracks pending and error state, and revalidates the route's loader data on success:\n\n```tsx\nimport { NewMessage } from 'shared/server';\n\nexport default function Guestbook() {\n const entries = Toil.useLoaderData<typeof loader>();\n\n const sign = async (data: FormData) => {\n const author = String(data.get('author'));\n const message = String(data.get('message'));\n await Server.REST.guestbook.sign({ body: new NewMessage(author, message) });\n };\n\n return (\n <Toil.Form action={sign} resetOnSuccess>\n {({ pending }) => (\n <>\n <input name=\"author\" />\n <input name=\"message\" />\n <button disabled={pending}>Sign</button>\n </>\n )}\n </Toil.Form>\n );\n}\n```\n\nKey `Toil.Form` props:\n\n| Prop | Default | What it does |\n| --- | --- | --- |\n| `action` | (required) | Runs on submit, receiving the form's `FormData`. May be async. |\n| `revalidate` | `true` (current route) | Which loader data to refetch after a successful submit. |\n| `resetOnSuccess` | `false` | Clear the form fields after success. |\n| `onSuccess` / `onError` | | Callbacks for the two outcomes. |\n\nPassing a **render function** as `children` gives you live submit state (`pending`, `error`), so you can disable the button while the request is in flight. On success, `Form` revalidates the loader, so the page's data updates automatically without a manual refetch.\n\n## Mutations without a form: `useAction`\n\n`Toil.Form` is convenient sugar over a more general primitive, `Toil.useAction`. Use `useAction` for any write that is not a form submit: a delete button, a \"like\" toggle, a \"mark as read\" action. It runs your async function on demand and tracks the whole lifecycle (`pending`, `error`, `data`), then revalidates the affected loader data on success so the page reflects the change, exactly like `Form` does.\n\nYou call it with the function to run and an optional options object, and it hands back a handle:\n\n```tsx\nexport default function PostRow({ id }: { id: number }) {\n const del = Toil.useAction(\n (postId: number) => Server.REST.posts.remove({ params: { id: postId } }),\n { revalidate: true, onError: (e) => alert(parseError(e)) },\n );\n\n return (\n <button disabled={del.pending} onClick={() => void del.run(id)}>\n {del.pending ? 'Deleting...' : 'Delete'}\n </button>\n );\n}\n```\n\nThe handle it returns:\n\n| Field | What it is |\n| --- | --- |\n| `run(input)` | Runs the action. Resolves to the result, or `undefined` if it threw. The error is captured in `error` rather than rejected, so a fire-and-forget `onClick` cannot leak an unhandled rejection. |\n| `pending` | `true` while a run is in flight. Drive a spinner or a disabled button off this. |\n| `error` | The error from the last failed run, or `undefined`. |\n| `data` | The value the last successful run returned, or `undefined`. |\n| `reset()` | Clear `pending` / `error` / `data` back to idle. |\n\nThe options mirror `Toil.Form`:\n\n| Option | Default | What it does |\n| --- | --- | --- |\n| `revalidate` | `true` (current route) | Which loader data to refetch after a successful run: `true` (the active route), an `href` or array of hrefs (those routes), or `false` (none). |\n| `onSuccess` | | Called with the action's return value after a successful run. |\n| `onError` | | Called with the thrown value when the action fails. |\n\n`Toil.Form` is just this hook wired to a `<form>`'s submit event, with the form's `FormData` passed as the input. Anything a form does, you can do by hand with `useAction`.\n\n## Reading who is logged in\n\nTo render \"logged in as ...\" you need the current user. toiljs generates a `getUser()` helper (from the backend's `@user` surface) that reads the current user from a readable session cookie with no network round-trip. It is instant, which makes it perfect for display, but it is **untrusted**: a value read on the client can be forged, so never gate anything security-sensitive on it. For a trusted check, call a guarded backend route that re-verifies the signed session.\n\nThe full auth flow (post-quantum login, sessions, `getUser()`, and guarding routes) is its own guide:\n\n- [Auth usage](../auth/usage.md): reading the session, `getUser()`, and guarding pages.\n\n## RPC (`@service` / `@remote`)\n\nAlongside REST, toiljs generates a typed RPC surface for `@service` methods and free `@remote` functions. These read like plain function calls with no URL:\n\n```tsx\nconst n = await Server.ping(10); // a free @remote\nconst count = await Server.stats.playerCount(); // a @service method\n```\n\nThe types come straight from your server, so `Server.ping` knows it takes a number and returns a number. Under the hood each call encodes its arguments and POSTs them to a single reserved endpoint (`/__toil_rpc`) with a compact method id, then decodes the typed result. Both the local dev server and the production edge dispatch this endpoint, so RPC works end to end.\n\nIf a call throws an \"unavailable\" error, it means the generated client has not attached yet: build the server (`npm run build:server`) to regenerate `shared/server.ts`, and import from `shared/server` so the client loads (see the gotchas below). See [Typed RPC](../backend/rpc.md) for the backend side and when to choose RPC over REST.\n\n## Gotchas\n\n- **Import from `shared/server` to attach the clients.** `Server.REST` (and the RPC client) attach when you import from `shared/server`. Importing your `@data` classes from there does it naturally; if `Server.REST.foo()` throws an \"unavailable\" error, make sure the server has been built (`npm run build:server`) and that `shared/server` is imported.\n- **The server runs a fresh instance per request.** In the examples, in-memory writes are previews that do not persist. To keep data, write it to the database (see [Database](../database/README.md)). This is a backend property, but it explains why a create returns a value that is not there on the next request.\n- **Fetch page data in a `loader`, not `useEffect`.** A loader runs in parallel with the route chunk and integrates with `loading.tsx`, suspense, caching, and SSR hydration. A `useEffect` fetch runs only after the page mounts (slower, and invisible to the server).\n- **`getUser()` is display-only.** It is fast because it does not verify anything. Do real authorization on the server.\n\n## Related\n\n- [Routing](./routing.md): loaders, `loading.tsx`, and navigation.\n- [Backend HTTP routes](../backend/rest.md): the `@rest` controllers behind `Server.REST`.\n- [Typed RPC](../backend/rpc.md): the `@service` / `@remote` surface behind `Server`.\n- [Data types](../backend/data.md): the `@data` classes that cross the wire.\n- [Auth usage](../auth/usage.md): sessions and `getUser()`.\n",
34
34
  "frontend/images.md": "# Images\n\n`Toil.Image` is a drop-in replacement for `<img>` that stops layout shift, lazy-loads by default, and can fade in from a blurred placeholder. Use it for content images instead of a raw `<img>`.\n\n## Why not just use `<img>`?\n\nTwo problems with a plain `<img>`:\n\n1. **Layout shift.** Before the image loads, the browser does not know how tall it is, so the page has no space reserved. When the image arrives, everything below it jumps down. That jump hurts the user experience and your Core Web Vitals score (specifically CLS, Cumulative Layout Shift).\n2. **Loading cost.** Every image loads eagerly by default, competing for bandwidth with the things the user actually needs first.\n\n`Toil.Image` fixes both: it reserves the right amount of space up front, and it lazy-loads everything except the images you mark as high priority.\n\n## The simplest usage\n\nGive it a `src`, an `alt`, and a `width` + `height`. The width and height are the image's real pixel dimensions, and they are what reserve space so nothing jumps:\n\n```tsx\nexport default function Post() {\n return (\n <Toil.Image\n src=\"/images/diagram.png\"\n alt=\"How the request flows\"\n width={800}\n height={400}\n />\n );\n}\n```\n\n`alt` is required (toiljs enforces it for accessibility). For a purely decorative image, pass `alt=\"\"`.\n\nThere is no server-side resizing here: `Toil.Image` is a client component. Point `src` at an already-optimized image. When you import an image (see below), Vite hashes and optimizes the file for you.\n\n## Automatic blur placeholders with `?toil`\n\nFor a nicer loading experience you can show a tiny blurred preview of the image while the full one downloads. toiljs generates that preview automatically when you import the image with a `?toil` flag:\n\n```tsx\nimport hero from './hero.webp?toil';\n\nexport default function Landing() {\n return <Toil.Image src={hero} alt=\"Product hero\" placeholder=\"blur\" />;\n}\n```\n\nThe `?toil` import does not give you a plain URL string. It gives you an object:\n\n```ts\n{\n src: string; // the optimized, hashed image URL\n width: number; // the image's intrinsic width\n height: number; // the image's intrinsic height\n blurDataURL: string; // a tiny base64 blurred preview, inlined\n}\n```\n\n`Toil.Image` unpacks that object for you. So a `?toil` import auto-fills `width`, `height`, and the blur placeholder with **no extra props**: you do not repeat the dimensions, and `placeholder=\"blur\"` just works. Explicit props still win if you pass them.\n\nBehind the scenes, at import time toiljs uses `sharp` to downscale the image to about 24 pixels on its longest edge, blur it, and encode it as a small WebP data URI (a few hundred bytes) inlined right into your bundle. This runs in dev and in the build. Only raster images (`.png`, `.jpg`, `.webp`, `.avif`, `.gif`, `.tiff`) get a blur; SVGs and animated images are left as-is.\n\n### The skeleton fallback\n\n`placeholder=\"blur\"` is never a silent no-op. If there is no `blurDataURL` (you used a string `src` and did not pass one), `Toil.Image` shows a neutral animated \"skeleton shimmer\" instead of a blur. Either way the placeholder needs a reserved box to paint into, so give it `width` + `height` (or use `fill`, below).\n\nYou can also pass a `blurDataURL` by hand for a plain string `src`:\n\n```tsx\n<Toil.Image\n src=\"/images/photo.jpg\"\n alt=\"A photo\"\n width={1200}\n height={800}\n placeholder=\"blur\"\n blurDataURL=\"data:image/webp;base64,UklGR...\"\n/>\n```\n\n## The real layout-shift fix: aspect ratio\n\nThe important detail: the thing that actually prevents layout shift is not the blur, it is the **reserved aspect ratio**. When you pass both `width` and `height`, `Toil.Image` sets a CSS `aspect-ratio` derived from them. That reservation survives responsive CSS like `width: 100%` (a bare `width`/`height` attribute does not survive that). So the box holds its shape while the image loads, and nothing jumps. The blur is cosmetic; the reserved size is what holds.\n\nThe takeaway: always give `width` + `height` (or `fill`). A `?toil` import supplies them for you.\n\n## `fill`: sizing from the container\n\nSometimes you do not know the image's size, you want it to fill a box you control (a card, a banner). Pass `fill`:\n\n```tsx\n<div style={{ maxWidth: 480 }}>\n <Toil.Image src={photo} alt=\"Cover\" fill />\n</div>\n```\n\nWith `fill`, `Toil.Image` wraps the image in a block-level box and the image fills that box's width, scaling to its natural height. If you size the box yourself (with `width`/`height`, or a `style` like `aspectRatio: '16 / 9'`), the image *covers* that box, cropped according to `objectFit` (default `cover`):\n\n```tsx\n<Toil.Image\n src={photo}\n alt=\"Cover\"\n fill\n style={{ aspectRatio: '16 / 9' }}\n objectFit=\"cover\"\n/>\n```\n\nThe `fill` image flows in the normal document layout (it is never absolutely positioned), so it cannot escape to cover the whole page nor collapse to zero height, two common bugs with hand-rolled fill images. With `fill`, your `className` and `style` apply to the wrapper box, not the raw `<img>`.\n\nUse fixed `width`/`height` when you know the image's size; use `fill` when the layout decides the size.\n\n## Priority images (above the fold)\n\nBy default every `Toil.Image` lazy-loads: the browser fetches it only as it nears the viewport. That is right for most images, but wrong for the one big image at the top of the page (your hero, your LCP element). Mark that one `priority`:\n\n```tsx\n<Toil.Image src={hero} alt=\"Hero\" width={1200} height={630} priority />\n```\n\n`priority` loads the image eagerly and sets `fetchPriority=\"high\"`, telling the browser to fetch it right away. Use it only for the important above-the-fold image; everything else should stay lazy.\n\n## Prop reference\n\n| Prop | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `src` | `string \\| ToilImageSource` | (required) | A URL, or a `?toil` import object. |\n| `alt` | `string` | (required) | Alt text. Use `\"\"` for decorative images. |\n| `width` / `height` | `number` | from `?toil` | Intrinsic size, reserves space. |\n| `fill` | `boolean` | `false` | Fill the container instead of using fixed size. |\n| `objectFit` | CSS `object-fit` | `cover` (with fill) | How the image fits its box. |\n| `priority` | `boolean` | `false` | Eager load + high fetch priority (above-the-fold). |\n| `placeholder` | `'empty' \\| 'blur'` | `'empty'` | Show a blur / skeleton while loading. |\n| `blurDataURL` | `string` | from `?toil` | The tiny preview for `placeholder=\"blur\"`. |\n\nEvery other standard `<img>` attribute (`className`, `style`, `onLoad`, `sizes`, `srcSet`, `id`, `data-*`) passes straight through.\n\n## Gotchas\n\n- **No server-side resizing.** `Toil.Image` does not shrink or convert your image at request time. Ship an appropriately sized `src` (importing it lets Vite optimize and hash it). The `?toil` blur is only a placeholder, not a resized copy.\n- **A placeholder needs a reserved box.** `placeholder=\"blur\"` only shows if there is a size to paint into. Give `width` + `height`, or use `fill` inside a sized parent.\n- **`?toil` is for raster images.** SVGs and animated formats do not get a generated blur (importing them with `?toil` skips the blur step). Import an SVG normally.\n- **`alt` is mandatory.** This is intentional. For decorative images that add no meaning, pass `alt=\"\"` so screen readers skip them.\n- **Do not double up dimensions with a `?toil` import.** The import already carries `width`/`height`; passing them again is redundant (though harmless, explicit props win).\n\n## Related\n\n- [Styling](./styling.md): importing images and CSS in general.\n- [Metadata and SEO](./metadata.md): setting `og:image` for social-share previews.\n- [Rendering and SSR](./rendering.md): why first-paint layout stability matters.\n",
35
35
  "frontend/metadata.md": "# Metadata and SEO\n\nMetadata is the information in a page's `<head>`: its title, its description, and the tags that control how it looks when shared on social media or listed by a search engine. toiljs lets you set all of it per route, and bakes it into real HTML so crawlers see it even without running your JavaScript.\n\n## The quick version\n\nFor most pages, `export const metadata` from the route file. That is it:\n\n```tsx\n// client/routes/features/seo.tsx\nexport const metadata: Toil.Metadata = {\n title: 'useReducer | React Hooks',\n description: 'Manage complex state transitions with a reducer function.',\n keywords: ['react', 'hooks', 'useReducer'],\n canonical: 'https://example.com/features/seo',\n openGraph: {\n title: 'useReducer | React Hooks',\n description: 'Manage complex state transitions with a reducer.',\n type: 'website',\n },\n};\n\nexport default function SeoDemo() {\n return <main><h1>Route metadata</h1></main>;\n}\n```\n\nThe router applies this before the page paints (so the tab title updates with no flicker), and the build bakes it into the page's static HTML so search engines and link-preview bots read it directly.\n\n## The `Metadata` fields\n\n`Toil.Metadata` maps friendly fields onto the right `<meta>` and `<link>` tags for you:\n\n| Field | Becomes |\n| --- | --- |\n| `title` | The document `<title>`. |\n| `description` | `<meta name=\"description\">`. |\n| `keywords` | `<meta name=\"keywords\">` (an array is joined with commas). |\n| `canonical` | `<link rel=\"canonical\">`. |\n| `robots` | `<meta name=\"robots\">`, e.g. `'noindex, nofollow'`. |\n| `themeColor` | `<meta name=\"theme-color\">` (the accent color of some link embeds). |\n| `openGraph` | The `og:*` tags (title, description, type, url, image, siteName). |\n| `meta` | Escape hatch: extra raw `<meta>` tags. |\n| `link` | Escape hatch: extra raw `<link>` tags. |\n\nOpen Graph (the `og:*` tags) is the shared standard that Facebook, Discord, Slack, LinkedIn, and iMessage read to build a link preview card. Set `openGraph.image` (an absolute URL, ideally at least 1200 by 630 pixels) to control the preview picture:\n\n```tsx\nexport const metadata: Toil.Metadata = {\n title: 'Our launch',\n description: 'Read the announcement.',\n openGraph: {\n title: 'Our launch',\n description: 'Read the announcement.',\n type: 'article',\n image: 'https://example.com/og/launch.png',\n },\n};\n```\n\n## Dynamic metadata: `generateMetadata`\n\nWhen the title depends on the URL or on fetched data (a blog post's title, a product name), export `generateMetadata` instead of a static object. It receives the route params, the query, and the route loader's data, and returns a `Metadata`:\n\n```tsx\n// client/routes/blog/[id].tsx\nexport const generateMetadata: Toil.GenerateMetadata = ({ params }) => ({\n title: `Blog post ${params.id}`,\n description: `Reading blog post ${params.id}.`,\n});\n```\n\nNow `/blog/42` sets the tab to \"Blog post 42\". If your route has a `loader`, its data is passed in as `data`, so you can title a page from the content it loaded:\n\n```tsx\nexport const loader = async ({ params }: Toil.LoaderArgs) =>\n Server.REST.blog.get({ params: { id: params.id } });\n\nexport const generateMetadata: Toil.GenerateMetadata = ({ data }) => ({\n title: data.title,\n description: data.excerpt,\n});\n```\n\n## Imperative and stateful head: `useHead`, `useTitle`, `<Head>`\n\nSometimes the head depends on component state, not on the route. For that, set it from inside a component with `Toil.useHead`, `Toil.useTitle`, or the declarative `<Toil.Head>`. These apply for the component's lifetime and revert when it unmounts:\n\n```tsx\nexport default function HeadDemo() {\n const [count, setCount] = useState(0);\n\n // The tab title updates every render as count changes.\n Toil.useTitle(`Clicked ${count} times`);\n\n Toil.useHead({\n meta: [{ name: 'description', content: `Clicked ${count} times.` }],\n });\n\n return <button onClick={() => setCount((c) => c + 1)}>Clicked {count}</button>;\n}\n```\n\nThe declarative form renders nothing and is equivalent:\n\n```tsx\n<Toil.Head\n title=\"Blog\"\n meta={[{ name: 'description', content: 'Latest posts' }]}\n/>\n```\n\nUse `useHead`/`useTitle` when the value is dynamic or lives in component state; use the `metadata` export when it is a static property of the route.\n\n### Applying a whole `Metadata` object from a component: `useMetadata`\n\n`useHead` takes raw `<meta>` and `<link>` tags. When you would rather pass the same friendly `Metadata` shape you use in a route's `metadata` export (with `title`, `openGraph`, `keywords`, and the rest), use `Toil.useMetadata` from inside any component. It applies for that component's lifetime and reverts on unmount, exactly like `useHead`. This is the tool for content that is not itself a route file: a reusable article component, a widget, a search-result view.\n\n```tsx\nexport default function Article({ post }: { post: Post }) {\n Toil.useMetadata({\n title: post.title,\n description: post.excerpt,\n openGraph: { type: 'article', title: post.title, image: post.cover },\n });\n\n return <article>{/* ... */}</article>;\n}\n```\n\nThere is a declarative twin, `<Toil.Metadata title=\"...\" openGraph={...} />`, which renders nothing (the component-level counterpart of a route's `metadata` export). A route's own `metadata` export is still merged last (highest priority), so it wins for the keys it sets, and `useMetadata` fills in for routes that declare none.\n\n> **Advanced.** Under the hood, `Toil.resolveMetadata(metadata)` is the pure function that expands a `Metadata` object into concrete `<meta>`/`<link>` tags (a `HeadSpec`), and `Toil.mergeHead(specs)` is the pure merge that resolves all active head contributions into the final result (the last `title` wins, `meta` dedupes by `name`/`property`, `link` by `rel` + `href`). You rarely call these directly, but they are exported for tooling and tests.\n\n## How the pieces combine\n\nMultiple things can contribute to the head at once: your site-wide defaults, a layout, and the page. They merge by key, with a clear priority. Later, more specific contributions win per key, and anything left unset falls through to a broader default:\n\n```mermaid\nflowchart TB\n A[\"Site-wide defaults<br/>client.seo config\"] --> M{\"merge by key\"}\n B[\"Root layout &lt;Toil.Head&gt;<br/>(fallback title, description)\"] --> M\n C[\"Component useHead / useTitle\"] --> M\n D[\"Route metadata / generateMetadata<br/>(highest priority)\"] --> M\n M --> R[\"The page's final &lt;head&gt;\"]\n```\n\n- A **root layout** is the natural home for site-wide fallbacks (a default title and description for any page that sets none):\n\n ```tsx\n // client/layout.tsx\n <Toil.Head\n title=\"My Site\"\n meta={[{ name: 'description', content: 'Planet-scale apps.' }]}\n />\n ```\n\n- A **route's `metadata`** overrides those defaults for the keys it sets, while the layout still fills in everything the route leaves unset. So a page can set just a `title` and inherit the site description.\n\nThe rule of thumb: put fallbacks in the layout, put the specifics on the route.\n\n## Build-time SEO for the whole site\n\nBeyond per-route metadata, toiljs generates site-level SEO assets at build time from a `client.seo` block in `toil.config.ts`. These are baked into the HTML `<head>` and written as files, so JavaScript-less crawlers and AI bots get correct information:\n\n```ts\n// toil.config.ts\nimport { defineConfig } from 'toiljs/compiler';\n\nexport default defineConfig({\n client: {\n seo: {\n url: 'https://example.com', // required for canonical/OG urls, sitemap\n title: 'My Site',\n description: 'Planet-scale apps from a single repo.',\n openGraph: {\n type: 'website',\n siteName: 'My Site',\n image: 'https://example.com/og.png',\n },\n twitter: { card: 'summary_large_image', site: '@mysite' },\n robots: { ai: 'allow' }, // allow or disallow known AI crawlers\n llms: { instructions: 'Docs live at /docs.' },\n jsonLd: { '@context': 'https://schema.org', '@type': 'WebSite', name: 'My Site' },\n },\n },\n});\n```\n\nFrom this one block, the build:\n\n- bakes the default `<title>`, `description`, Open Graph, Twitter card, and JSON-LD structured data into every page's HTML;\n- overlays each route's own `metadata` on top and points that route's canonical and `og:url` at its own URL;\n- generates `robots.txt` (with directives for AI crawlers like GPTBot and ClaudeBot), `sitemap.xml` (from your static routes), and `llms.txt` (a guidance file for AI crawlers).\n\nYou get correct, per-page SEO in the raw HTML with almost no manual tag writing. Confirm it with \"View source\" on a built page: the real title and tags are right there, not injected later by JavaScript.\n\n### The complete `client.seo` reference\n\nEvery field `client.seo` accepts is below. All of them are optional, and everything you set becomes a baked-in `<head>` tag or a generated file (`robots.txt`, `sitemap.xml`, `llms.txt`).\n\n**Top level:**\n\n| Field | Type | What it does |\n| --- | --- | --- |\n| `url` | `string` | Absolute site base URL, e.g. `'https://example.com'`. Unlocks `sitemap.xml`, the canonical `<link>`, and absolute Open Graph / Twitter URLs. |\n| `title` | `string` | Default document `<title>`. |\n| `description` | `string` | Default `<meta name=\"description\">`. |\n| `robotsMeta` | `string` | Default `<meta name=\"robots\">`, e.g. `'index, follow'`. (This is the meta tag; the `robots` field below is the separate `robots.txt` file.) |\n| `themeColor` | `string` | `<meta name=\"theme-color\">`, also the accent color of some Discord / Slack link embeds. |\n| `openGraph` | object | Open Graph (`og:*`) defaults, see below. |\n| `twitter` | object | Twitter / X card, see below. |\n| `facebook` | `{ appId?: string }` | `appId` renders `<meta property=\"fb:app_id\">`. Open Graph covers the rest of the Facebook card. |\n| `preconnect` | `string[]` | Origins to `<link rel=\"preconnect\">` (early connection hints). |\n| `dnsPrefetch` | `string[]` | Origins to `<link rel=\"dns-prefetch\">`. |\n| `jsonLd` | object or object[] | JSON-LD structured data injected as `<script type=\"application/ld+json\">`. Pass an array to include several nodes (they are serialized into one `<script>` as a JSON array). |\n| `robots` | object or `false` | `robots.txt` generation, see below. `false` skips the file. |\n| `sitemap` | `boolean` | `sitemap.xml` generation. On by default when `url` is set; `false` skips it. |\n| `llms` | object or `boolean` | `llms.txt` (AI-crawler guidance) generation. `false` skips it, `true` or an object configures it. |\n\n**`openGraph`** (the `og:*` tags; `title` and `description` fall back to the top-level values):\n\n| Field | Type | Renders |\n| --- | --- | --- |\n| `title` | `string` | `og:title` |\n| `description` | `string` | `og:description` |\n| `type` | `string` | `og:type`, e.g. `'website'` or `'article'` (defaults to `'website'`). |\n| `siteName` | `string` | `og:site_name` |\n| `locale` | `string` | `og:locale`, e.g. `'en_US'`. |\n| `image` | `string` | `og:image`, the preview picture (absolute URL, ideally at least 1200 by 630 pixels). |\n| `imageAlt` | `string` | `og:image:alt` |\n| `imageWidth` | `number` | `og:image:width` in pixels (lets Facebook / LinkedIn render without a re-fetch). |\n| `imageHeight` | `number` | `og:image:height` in pixels. |\n| `imageType` | `string` | `og:image:type`, e.g. `'image/png'`. |\n\n**`twitter`** (the Twitter / X card; unset fields fall back to the Open Graph or top-level values):\n\n| Field | Type | Renders |\n| --- | --- | --- |\n| `card` | `string` | `twitter:card`: `'summary'` or `'summary_large_image'` (defaults by whether an image is present). |\n| `site` | `string` | `twitter:site`, the site's `@handle`. |\n| `creator` | `string` | `twitter:creator`, the author's `@handle`. |\n| `title` | `string` | `twitter:title` (falls back to `openGraph.title` / `title`). |\n| `description` | `string` | `twitter:description` (falls back to `openGraph.description` / `description`). |\n| `image` | `string` | `twitter:image` (falls back to `openGraph.image`). |\n| `imageAlt` | `string` | `twitter:image:alt` (falls back to `openGraph.imageAlt`). |\n\n**`robots`** (the `robots.txt` file; set `robots: false` to skip it):\n\n| Field | Type | What it does |\n| --- | --- | --- |\n| `rules` | `RobotsRule[]` | Custom `User-agent` groups. Each rule is `{ userAgent?: string \\| string[], allow?: string[], disallow?: string[] }`. Defaults to one group allowing everything (`User-agent: *`, `Allow: /`). |\n| `ai` | `'allow'` or `'disallow'` | How to treat known AI crawlers (GPTBot, ClaudeBot, Google-Extended, and more). Default `'allow'`. |\n| `sitemap` | `string` | Explicit `Sitemap:` line (defaults to `<url>/sitemap.xml` when `url` is set). |\n\n**`llms`** (the `llms.txt` guidance file; set `llms: false` to skip it, `llms: true` for defaults):\n\n| Field | Type | What it does |\n| --- | --- | --- |\n| `title` | `string` | The file's heading (falls back to `seo.title`). |\n| `summary` | `string` | The summary blockquote (falls back to `seo.description`). |\n| `instructions` | `string` | Free-form guidance for AI / LLM crawlers. |\n| `pages` | `LlmsPage[]` | Key pages, each `{ title: string, url: string, description?: string }`. Defaults to the site's static routes. |\n\nA fully worked config touching most of these fields:\n\n```ts\n// toil.config.ts\nimport { defineConfig } from 'toiljs/compiler';\n\nexport default defineConfig({\n client: {\n seo: {\n url: 'https://example.com', // base URL: unlocks sitemap, canonical, absolute OG/Twitter URLs\n title: 'My Site', // default <title>\n description: 'Planet-scale apps.', // default <meta name=\"description\">\n robotsMeta: 'index, follow', // default <meta name=\"robots\">\n themeColor: '#0b0b0f', // <meta name=\"theme-color\">\n\n openGraph: {\n type: 'website', // og:type\n siteName: 'My Site', // og:site_name\n locale: 'en_US', // og:locale\n image: 'https://example.com/og.png', // og:image (absolute, ideally 1200x630)\n imageAlt: 'My Site preview', // og:image:alt\n imageWidth: 1200, // og:image:width\n imageHeight: 630, // og:image:height\n imageType: 'image/png', // og:image:type\n },\n\n twitter: {\n card: 'summary_large_image', // twitter:card\n site: '@mysite', // twitter:site\n creator: '@ada', // twitter:creator\n // title / description / image / imageAlt fall back to openGraph + top level\n },\n\n facebook: { appId: '1234567890' }, // <meta property=\"fb:app_id\">\n\n preconnect: ['https://cdn.example.com'], // <link rel=\"preconnect\">\n dnsPrefetch: ['https://analytics.example.com'], // <link rel=\"dns-prefetch\">\n\n robots: {\n ai: 'allow', // known AI crawlers: 'allow' | 'disallow'\n rules: [ // custom User-agent groups for robots.txt\n { userAgent: '*', allow: ['/'], disallow: ['/admin'] },\n ],\n // sitemap: 'https://example.com/sitemap.xml', // explicit line (auto from url otherwise)\n },\n sitemap: true, // generate sitemap.xml (on by default when url is set)\n\n llms: { // llms.txt (AI-crawler guidance)\n title: 'My Site',\n summary: 'Planet-scale apps from a single repo.',\n instructions: 'Docs live at /docs.',\n // pages: [{ title: 'Docs', url: 'https://example.com/docs', description: 'Guides' }],\n },\n\n jsonLd: [ // array = several nodes in one <script>\n { '@context': 'https://schema.org', '@type': 'WebSite', name: 'My Site' },\n { '@context': 'https://schema.org', '@type': 'Organization', name: 'My Site' },\n ],\n },\n },\n});\n```\n\n## Per-request titles on server-rendered pages\n\nFor a page that is server-rendered (`export const ssr = true`), the backend can set a fresh `<title>` for each individual request (for example, a search page titled with the query the visitor typed). The edge splices that per-request title into the document before sending it, so the correct title is in the very first byte of HTML. This is a server-side API used in your `render` function; the frontend `metadata` above covers everything else. See [Rendering and SSR](./rendering.md) for how SSR pages are assembled.\n\n## Gotchas\n\n- **`generateMetadata` needs the data available.** It runs with the route loader's data, so it can only use what the loader returns. Fetch what the title needs in the loader.\n- **Open Graph images must be absolute URLs.** A relative `/og.png` will not resolve for an external crawler. Use the full `https://...` URL (set `seo.url` and it can build them for you).\n- **`seo.url` unlocks the site-level assets.** `sitemap.xml`, canonical links, and absolute OG urls all need the site's base `url`. Set it once in the config.\n- **Static export vs runtime.** The `metadata` export is baked into HTML at build (great for crawlers) *and* applied at runtime on navigation. `useHead` runs only at runtime; a crawler that does not execute JS will not see it. Prefer the `metadata` export for anything that matters for SEO.\n\n## Related\n\n- [Rendering and SSR](./rendering.md): how the baked head and SSR title reach the browser.\n- [Images](./images.md): producing the `og:image` for share previews.\n- [Routing](./routing.md): where `metadata` and `generateMetadata` live in a route file.\n- [Configuration](../concepts/config.md): the full `toil.config.ts` reference.\n",
36
- "frontend/navigation.md": "# Navigation\n\nOnce your files are turned into URLs (see [Routing](./routing.md)), you need a way to move between them. This page is about that half: the links, hooks, and functions that navigate a user from one page to the next without a full reload, keep the current section highlighted, prefetch what is coming, and restore scroll. Everything here lives on the global `Toil` object, so route files need no imports for the common cases.\n\nRouting is \"file to URL\"; navigation is \"get me to that URL\". If you are looking for how a file becomes a page, or for dynamic params and layouts, that is the [Routing](./routing.md) page.\n\n## `Toil.Link`\n\n`Toil.Link` is the client-side replacement for a plain `<a>`. It navigates in place (no full page reload), and it prefetches the target route's chunk on hover or focus, so the click feels instant:\n\n```tsx\n<Toil.Link href=\"/about\">About</Toil.Link>\n```\n\n`Link` accepts every standard anchor attribute (`className`, `style`, `target`, `rel`, `download`, `referrerPolicy`, `ref`, `data-*`, `aria-*`, event handlers, and so on), plus a few toiljs controls:\n\n| Prop | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `href` | `Href` | (required) | Destination, typed to your project's real routes (a typo is a compile error). |\n| `replace` | `boolean` | `false` | Replace the current history entry instead of pushing a new one. |\n| `scroll` | `boolean` | `true` | Scroll to the top after navigating. `false` keeps the current position. |\n| `prefetch` | `boolean` | `true` | Prefetch the route chunk on hover/focus. `false` opts this link out. |\n\n### When `Link` does not intercept\n\n`Link` is deliberate about when to hand a click back to the browser. It only intercepts a plain, same-origin, left-click. All of these fall through to native browser behavior instead:\n\n- **External URLs** (a different origin), and opaque targets like `mailto:` or `tel:`.\n- **`target` other than `_self`** (for example `target=\"_blank\"`, a new tab).\n- **`download`** links.\n- **In-page `#hash`-only** links.\n- **Modified clicks**: middle-click or any click with Ctrl, Cmd (Meta), Shift, or Alt held (so \"open in new tab\" keeps working).\n\nBecause of this, you can point a `Link` at an external site or a download and it behaves exactly like an `<a>` would, no special-casing on your part. For genuinely external links a plain `<a>` is still fine; reach for `Link` when the target is one of your own routes.\n\n## `Toil.NavLink` and active state\n\n`Toil.NavLink` is a `Link` that knows whether it points at the current page. When active it adds a class (`active` by default) and sets `aria-current=\"page\"`, which is exactly what a navigation bar wants for highlighting the current section. It inherits Link's full anchor API and its prefetching.\n\nOn top of `LinkProps` it adds:\n\n| Prop | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `end` | `boolean` | `false` | Require an exact match. Without it, a parent link is active for its sub-paths. |\n| `activeClassName` | `string` | `'active'` | The class added when active (used with a string `className`). |\n| `className` | `string \\| (state) => string \\| undefined` | (none) | A string, or a function of `{ isActive }`. |\n| `style` | `CSSProperties \\| (state) => CSSProperties \\| undefined` | (none) | A style object, or a function of `{ isActive }`. |\n| `children` | `ReactNode \\| (state) => ReactNode` | (none) | Content, or a function of `{ isActive }`. |\n\nA simple nav bar with string classes:\n\n```tsx\n// client/components/Nav.tsx\nexport default function Nav() {\n return (\n <nav>\n <Toil.NavLink href=\"/\" end>Home</Toil.NavLink>\n <Toil.NavLink href=\"/blog\">Blog</Toil.NavLink>\n <Toil.NavLink href=\"/about\" activeClassName=\"is-current\">About</Toil.NavLink>\n </nav>\n );\n}\n```\n\nBy default a parent link stays active for its sub-paths, so `/blog` is active on `/blog`, `/blog/42`, and `/blog/42/edit`. Pass `end` to require an exact match. This matters most for the home link: `/` would otherwise be active on every page, so `Home` almost always wants `end`.\n\nThe function forms let the active state drive `className`, `style`, or `children` directly. Each receives a `{ isActive }` object:\n\n```tsx\n<Toil.NavLink href=\"/blog\" className={({ isActive }) => (isActive ? 'tab on' : 'tab')}>\n {({ isActive }) => <span>{isActive ? '• ' : ''}Blog</span>}\n</Toil.NavLink>\n```\n\n## Navigating in code\n\nNot every navigation is a click. After a form submit, a login, or a `fetch`, you often want to move the user yourself. There are three ways, from smallest to fullest.\n\n### `Toil.useNavigate()`\n\nThe hook returns the bare `navigate(href, options)` function:\n\n```tsx\nexport default function Login() {\n const navigate = Toil.useNavigate();\n return (\n <button onClick={() => navigate('/dashboard', { replace: true })}>\n Continue\n </button>\n );\n}\n```\n\nThe options are `NavigateOptions`, the same two controls `Link` exposes:\n\n| Option | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `replace` | `boolean` | `false` | Replace the current history entry instead of pushing. |\n| `scroll` | `boolean` | `true` | Scroll to top after navigating. `false` keeps the position. |\n\n### `Toil.navigate` (outside React)\n\nThe exact same function is available free of any hook as `Toil.navigate(href, options)`. Use it in code that is not a React component: a plain event handler, a utility module, or right after a `fetch` resolves:\n\n```tsx\nasync function saveDraft(draft: Draft) {\n await Server.REST.posts.create({ body: draft });\n Toil.navigate('/posts');\n}\n```\n\n`Toil.back()`, `Toil.forward()`, and `Toil.refresh()` are the history counterparts, also callable from anywhere: `back` and `forward` step through history, and `refresh` re-renders the current route (re-running its loader for the current URL).\n\n### `Toil.useRouter()`\n\nFor a full imperative handle, `useRouter()` returns a `RouterInstance`:\n\n```tsx\nexport default function PostActions() {\n const router = Toil.useRouter();\n return (\n <>\n <button onClick={() => router.push('/blog/new')}>New post</button>\n <button onClick={() => router.back()}>Back</button>\n <button onClick={() => router.revalidate()}>Refresh data</button>\n </>\n );\n}\n```\n\n| Method | What it does |\n| --- | --- |\n| `push(href, options?)` | Navigate to `href`, pushing a new history entry (or replacing with `{ replace: true }`). |\n| `replace(href)` | Navigate to `href`, replacing the current history entry. |\n| `back()` | Go back one history entry. |\n| `forward()` | Go forward one history entry. |\n| `refresh()` | Re-render the current route and re-run its loader (clears all cached loader data). |\n| `revalidate(href?)` | Invalidate cached loader data and re-render so it refetches. No argument targets the active route; pass an `href` for a specific route. Use after a mutation. |\n| `prefetch(href)` | Warm a route's chunk ahead of navigation. |\n\nReach for `revalidate()` after a write (you changed some data and want the current page's loader to refetch), and `refresh()` when you want a clean re-run of everything. `push`/`replace` are the imperative twins of a `Link` click.\n\n## Typed hrefs and the `href()` escape hatch\n\nEvery `href` you pass to `Toil.Link`, `NavLink`, `navigate`, or `router.push` is type-checked against your project's real routes. The compiler scans `client/routes/` and generates a `toil-routes.d.ts` that narrows the `Href` type to the union of your actual paths, so a typo is a compile error before you run the app:\n\n```tsx\n<Toil.Link href=\"/abuot\">About</Toil.Link>\n// ^ Type error: \"/abuot\" is not assignable to type 'Href'. (No such route.)\n```\n\nDynamic routes appear in that union as template-literal types, so a file at `client/routes/blog/[id].tsx` contributes `` `/blog/${string}` `` and a single interpolation still checks:\n\n```tsx\n<Toil.Link href={`/blog/${post.id}`}>Read</Toil.Link> // fine\n```\n\nWhen a URL is assembled from several data pieces (or from values TypeScript cannot prove the shape of), it is typed as a plain `string`, and `string` is not assignable to `Href`. That is when you reach for `Toil.href()`, the escape hatch that asserts a runtime string is a valid href:\n\n```tsx\nconst path = `/${product.category}/${product.slug}`; // string\nToil.navigate(Toil.href(path)); // asserted valid\n<Toil.Link href={Toil.href(path)}>Open</Toil.Link>;\n```\n\n`href()` is a pure type assertion (it returns the string unchanged), so use it only when the type-check is genuinely in your way, not to paper over a real typo. Before the routes are generated (a fresh project, the first build), `Href` is just `string`, so nothing complains until `toil-routes.d.ts` exists.\n\n## Reading the current location\n\nA handful of hooks let a component read where it is. Each re-reads on every navigation, so a component using one re-renders when the location changes:\n\n| Hook | Returns |\n| --- | --- |\n| `Toil.usePathname()` | The current pathname, e.g. `\"/blog/42\"`. |\n| `Toil.useLocation()` | The current pathname (an alias of `usePathname()`). |\n| `Toil.useParams<T>()` | The dynamic route params, e.g. `{ id }` for `/blog/[id]`. |\n| `Toil.useSearchParams()` | The query string as a `URLSearchParams`. |\n| `Toil.useNavigationPending()` | `true` while a navigation is in flight (started but not committed). |\n\n`useNavigationPending()` is what you wire a top loading bar to. It flips to `true` when a navigation begins and back to `false` once the new route commits:\n\n```tsx\n// client/components/ProgressBar.tsx\nexport default function ProgressBar() {\n const pending = Toil.useNavigationPending();\n return <div className=\"progress\" data-active={pending} />;\n}\n```\n\n`useSearchParams()` gives you a live `URLSearchParams`, so a filtered list reads its state from the URL:\n\n```tsx\nexport default function Results() {\n const params = Toil.useSearchParams();\n const q = params.get('q') ?? '';\n return <p>Results for {q}</p>;\n}\n```\n\n## Prefetching\n\ntoiljs prefetches routes before you navigate to them, so a click resolves with nothing left to download. There are two intents, both automatic:\n\n- **Hover / focus intent.** When you hover or focus a link that points at a known internal route, toiljs warms both its route chunk and its loader data, so the actual click can commit right away.\n- **Viewport intent.** As a link scrolls into view (or within about 200px of it), its route chunk is warmed. Links added later by client navigation are picked up automatically.\n\nPrefetching is best-effort and cheap: each route loads at most once, a failed prefetch is forgotten (so the real navigation can retry and surface the error), and new-tab / download / opted-out links are skipped. It is also **skipped entirely when the browser signals data-saver** (or reports a 2g-class connection), so you never spend a metered user's bandwidth on speculation.\n\nOpt a single link out with `prefetch={false}`, which emits a `data-no-prefetch` attribute the prefetcher respects:\n\n```tsx\n<Toil.Link href=\"/huge-report\" prefetch={false}>Annual report</Toil.Link>\n```\n\nYou can also warm a route yourself, for example right before an imperative `navigate`, with the standalone `Toil.prefetch(href)`:\n\n```tsx\n<button\n onPointerEnter={() => Toil.prefetch('/dashboard')}\n onClick={() => Toil.navigate('/dashboard')}>\n Open dashboard\n</button>\n```\n\n## Scroll restoration\n\ntoiljs manages scroll for you (it switches off the browser's automatic restoration and does the intuitive thing per navigation type):\n\n- **A push navigation** (a `Link` click, `navigate`, `router.push`) **scrolls to the top** of the new page.\n- **Back and forward restore** the scroll position you had on that entry, so returning to a long list lands you where you were.\n- **A `#hash` target** scrolls that element into view instead of jumping to the top.\n\nTo keep the current scroll on a push navigation, set `scroll={false}` on the `Link` (or `{ scroll: false }` in `NavigateOptions`). This is handy for tab bars or filters that change the URL but should not yank the viewport:\n\n```tsx\n<Toil.Link href=\"/settings/billing\" scroll={false}>Billing</Toil.Link>\n```\n\n```tsx\nnavigate('/settings/billing', { scroll: false });\n```\n\n## Animated transitions\n\nTwo optional effects can animate navigations. Both are **off by default** and are normally enabled from `toil.config.ts`, not in code:\n\n```ts\n// toil.config.ts\nexport default {\n client: {\n viewTransitions: true, // browser View Transitions API (a crossfade between pages)\n transitions: true, // React transition: keep the old page visible while the next loads\n },\n};\n```\n\n`viewTransitions` uses the browser's View Transitions API to crossfade the old and new page (and it respects `prefers-reduced-motion`, animating nothing for users who ask for less motion). `transitions` wraps each navigation in a React transition, keeping the current page on screen while the next route's loader runs, instead of showing its `loading.tsx` right away (smoother, but you trade away the immediate loading state).\n\nConfig is the normal way to turn these on. For a manual override at runtime, the setters are `Toil.setViewTransitions(enabled)` and `Toil.setTransitions(enabled)`:\n\n```tsx\nToil.setViewTransitions(true);\nToil.setTransitions(false);\n```\n\nYou rarely need the setters; prefer the config keys unless you are toggling an effect dynamically.\n\n## Types\n\nAlmost everything on this page is a value on the global `Toil` object, so you call it with no import (`Toil.Link`, `Toil.navigate`, `Toil.useRouter`, and so on). A few of the **types**, though, are not namespaced under `Toil`, so if you want to annotate a prop or a variable you import them from `toiljs/client`:\n\n```tsx\nimport type {\n LinkProps,\n NavLinkProps,\n NavLinkState,\n NavigateOptions,\n RouterInstance,\n} from 'toiljs/client';\n```\n\nFor example, a component that forwards `Link` props, or a helper typed against the router handle:\n\n```tsx\nimport type { RouterInstance } from 'toiljs/client';\n\nfunction logoutThenGo(router: RouterInstance) {\n router.replace('/login');\n}\n```\n\n## Related\n\n- [Routing](./routing.md): how files become URLs, dynamic params, layouts, and templates.\n- [Fetching data](./data-fetching.md): loaders, the typed backend clients, forms, and revalidation.\n- [Rendering and SSR](./rendering.md): what renders on the server versus the browser, and how hydration fits with client navigation.\n",
36
+ "frontend/navigation.md": "# Navigation\n\nOnce your files are turned into URLs (see [Routing](./routing.md)), you need a way to move between them. This page is about that half: the links, hooks, and functions that navigate a user from one page to the next without a full reload, keep the current section highlighted, prefetch what is coming, and restore scroll. Everything here lives on the global `Toil` object, so route files need no imports for the common cases.\n\nRouting is \"file to URL\"; navigation is \"get me to that URL\". If you are looking for how a file becomes a page, or for dynamic params and layouts, that is the [Routing](./routing.md) page.\n\n## `Toil.Link`\n\n`Toil.Link` is the client-side replacement for a plain `<a>`. It navigates in place (no full page reload), and it prefetches the target route's chunk on hover or focus, so the click feels instant:\n\n```tsx\n<Toil.Link href=\"/about\">About</Toil.Link>\n```\n\n`Link` accepts every standard anchor attribute (`className`, `style`, `target`, `rel`, `download`, `referrerPolicy`, `ref`, `data-*`, `aria-*`, event handlers, and so on), plus a few toiljs controls:\n\n| Prop | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `href` | `Href` | (required) | Destination, typed to your project's real routes (a typo is a compile error). |\n| `replace` | `boolean` | `false` | Replace the current history entry instead of pushing a new one. |\n| `scroll` | `boolean` | `true` | Scroll to the top after navigating. `false` keeps the current position. |\n| `prefetch` | `boolean` | `true` | Prefetch the route chunk on hover/focus. `false` opts this link out. |\n\n### When `Link` does not intercept\n\n`Link` is deliberate about when to hand a click back to the browser. It only intercepts a plain, same-origin, left-click. All of these fall through to native browser behavior instead:\n\n- **External URLs** (a different origin), and opaque targets like `mailto:` or `tel:`.\n- **`target` other than `_self`** (for example `target=\"_blank\"`, a new tab).\n- **`download`** links.\n- **In-page `#hash`-only** links.\n- **Modified clicks**: middle-click or any click with Ctrl, Cmd (Meta), Shift, or Alt held (so \"open in new tab\" keeps working).\n\nBecause of this, you can point a `Link` at an external site or a download and it behaves exactly like an `<a>` would, no special-casing on your part. For genuinely external links a plain `<a>` is still fine; reach for `Link` when the target is one of your own routes.\n\n## `Toil.NavLink` and active state\n\n`Toil.NavLink` is a `Link` that knows whether it points at the current page. When active it adds a class (`active` by default) and sets `aria-current=\"page\"`, which is exactly what a navigation bar wants for highlighting the current section. It inherits Link's full anchor API and its prefetching.\n\nOn top of `LinkProps` it adds:\n\n| Prop | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `end` | `boolean` | `false` | Require an exact match. Without it, a parent link is active for its sub-paths. |\n| `activeClassName` | `string` | `'active'` | The class added when active (used with a string `className`). |\n| `className` | `string \\| (state) => string \\| undefined` | (none) | A string, or a function of `{ isActive }`. |\n| `style` | `CSSProperties \\| (state) => CSSProperties \\| undefined` | (none) | A style object, or a function of `{ isActive }`. |\n| `children` | `ReactNode \\| (state) => ReactNode` | (none) | Content, or a function of `{ isActive }`. |\n\nA simple nav bar with string classes:\n\n```tsx\n// client/components/Nav.tsx\nexport default function Nav() {\n return (\n <nav>\n <Toil.NavLink href=\"/\" end>Home</Toil.NavLink>\n <Toil.NavLink href=\"/blog\">Blog</Toil.NavLink>\n <Toil.NavLink href=\"/about\" activeClassName=\"is-current\">About</Toil.NavLink>\n </nav>\n );\n}\n```\n\nBy default a parent link stays active for its sub-paths, so `/blog` is active on `/blog`, `/blog/42`, and `/blog/42/edit`. Pass `end` to require an exact match. This matters most for the home link: `/` would otherwise be active on every page, so `Home` almost always wants `end`.\n\nThe function forms let the active state drive `className`, `style`, or `children` directly. Each receives a `{ isActive }` object:\n\n```tsx\n<Toil.NavLink href=\"/blog\" className={({ isActive }) => (isActive ? 'tab on' : 'tab')}>\n {({ isActive }) => <span>{isActive ? '• ' : ''}Blog</span>}\n</Toil.NavLink>\n```\n\n## Navigating in code\n\nNot every navigation is a click. After a form submit, a login, or a `fetch`, you often want to move the user yourself. There are three ways, from smallest to fullest.\n\n### `Toil.useNavigate()`\n\nThe hook returns the bare `navigate(href, options)` function:\n\n```tsx\nexport default function Login() {\n const navigate = Toil.useNavigate();\n return (\n <button onClick={() => navigate('/dashboard', { replace: true })}>\n Continue\n </button>\n );\n}\n```\n\nThe options are `NavigateOptions`, the same two controls `Link` exposes:\n\n| Option | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `replace` | `boolean` | `false` | Replace the current history entry instead of pushing. |\n| `scroll` | `boolean` | `true` | Scroll to top after navigating. `false` keeps the position. |\n\n### `Toil.navigate` (outside React)\n\nThe exact same function is available free of any hook as `Toil.navigate(href, options)`. Use it in code that is not a React component: a plain event handler, a utility module, or right after a `fetch` resolves:\n\n```tsx\nasync function saveDraft(draft: Draft) {\n await Server.REST.posts.create({ body: draft });\n Toil.navigate('/posts');\n}\n```\n\n`Toil.back()`, `Toil.forward()`, and `Toil.refresh()` are the history counterparts, also callable from anywhere: `back` and `forward` step through history, and `refresh` re-renders the current route (re-running its loader for the current URL).\n\n### `Toil.useRouter()`\n\nFor a full imperative handle, `useRouter()` returns a `RouterInstance`:\n\n```tsx\nexport default function PostActions() {\n const router = Toil.useRouter();\n return (\n <>\n <button onClick={() => router.push('/blog/new')}>New post</button>\n <button onClick={() => router.back()}>Back</button>\n <button onClick={() => router.revalidate()}>Refresh data</button>\n </>\n );\n}\n```\n\n| Method | What it does |\n| --- | --- |\n| `push(href, options?)` | Navigate to `href`, pushing a new history entry (or replacing with `{ replace: true }`). |\n| `replace(href)` | Navigate to `href`, replacing the current history entry. |\n| `back()` | Go back one history entry. |\n| `forward()` | Go forward one history entry. |\n| `refresh()` | Re-render the current route and re-run its loader (clears all cached loader data). |\n| `revalidate(href?)` | Invalidate cached loader data and re-render so it refetches. No argument targets the active route; pass an `href` for a specific route. Use after a mutation. |\n| `prefetch(href)` | Warm a route's chunk ahead of navigation. |\n\nReach for `revalidate()` after a write (you changed some data and want the current page's loader to refetch), and `refresh()` when you want a clean re-run of everything. `push`/`replace` are the imperative twins of a `Link` click.\n\n## Typed hrefs and the `href()` escape hatch\n\nEvery `href` you pass to `Toil.Link`, `NavLink`, `navigate`, or `router.push` is type-checked against your project's real routes. The compiler scans `client/routes/` and generates a `toil-routes.d.ts` that narrows the `Href` type to the union of your actual paths, so a typo is a compile error before you run the app:\n\n```tsx\n<Toil.Link href=\"/abuot\">About</Toil.Link>\n// ^ Type error: \"/abuot\" is not assignable to type 'Href'. (No such route.)\n```\n\nDynamic routes appear in that union as template-literal types, so a file at `client/routes/blog/[id].tsx` contributes `` `/blog/${string}` `` and a single interpolation still checks:\n\n```tsx\n<Toil.Link href={`/blog/${post.id}`}>Read</Toil.Link> // fine\n```\n\nWhen a URL is assembled from several data pieces (or from values TypeScript cannot prove the shape of), it is typed as a plain `string`, and `string` is not assignable to `Href`. That is when you reach for `Toil.href()`, the escape hatch that asserts a runtime string is a valid href:\n\n```tsx\nconst path = `/${product.category}/${product.slug}`; // string\nToil.navigate(Toil.href(path)); // asserted valid\n<Toil.Link href={Toil.href(path)}>Open</Toil.Link>;\n```\n\n`href()` is a pure type assertion (it returns the string unchanged), so use it only when the type-check is genuinely in your way, not to paper over a real typo. Before the routes are generated (a fresh project, the first build), `Href` is just `string`, so nothing complains until `toil-routes.d.ts` exists.\n\n## Reading the current location\n\nA handful of hooks let a component read where it is. Each re-reads on every navigation, so a component using one re-renders when the location changes:\n\n| Hook | Returns |\n| --- | --- |\n| `Toil.usePathname()` | The current pathname, e.g. `\"/blog/42\"`. |\n| `Toil.useLocation()` | The current pathname (an alias of `usePathname()`). |\n| `Toil.useParams<T>()` | The dynamic route params, e.g. `{ id }` for `/blog/[id]`. |\n| `Toil.useSearchParams()` | The query string as a `URLSearchParams`. |\n| `Toil.useNavigationPending()` | `true` while a navigation is in flight (started but not committed). |\n\n`useNavigationPending()` is what you wire a top loading bar to. It flips to `true` when a navigation begins and back to `false` once the new route commits:\n\n```tsx\n// client/components/ProgressBar.tsx\nexport default function ProgressBar() {\n const pending = Toil.useNavigationPending();\n return <div className=\"progress\" data-active={pending} />;\n}\n```\n\n`useSearchParams()` gives you a live `URLSearchParams`, so a filtered list reads its state from the URL:\n\n```tsx\nexport default function Results() {\n const params = Toil.useSearchParams();\n const q = params.get('q') ?? '';\n return <p>Results for {q}</p>;\n}\n```\n\n## Prefetching\n\ntoiljs prefetches routes before you navigate to them, so a click resolves with nothing left to download. There are two intents, both automatic:\n\n- **Hover / focus intent.** When you hover or focus a link that points at a known internal route, toiljs warms both its route chunk and its loader data, so the actual click can commit right away.\n- **Viewport intent.** As a link scrolls into view (or within about 200px of it), its route chunk is warmed. Links added later by client navigation are picked up automatically.\n\nPrefetching is best-effort and cheap: each route loads at most once, a failed prefetch is forgotten (so the real navigation can retry and surface the error), and new-tab / download / opted-out links are skipped. It is also **skipped entirely when the browser signals data-saver** (or reports a 2g-class connection), so you never spend a metered user's bandwidth on speculation.\n\nOpt a single link out with `prefetch={false}`, which emits a `data-no-prefetch` attribute the prefetcher respects:\n\n```tsx\n<Toil.Link href=\"/huge-report\" prefetch={false}>Annual report</Toil.Link>\n```\n\nYou can also warm a route yourself, for example right before an imperative `navigate`, with the standalone `Toil.prefetch(href)`:\n\n```tsx\n<button\n onPointerEnter={() => Toil.prefetch('/dashboard')}\n onClick={() => Toil.navigate('/dashboard')}>\n Open dashboard\n</button>\n```\n\n## Scroll restoration\n\ntoiljs manages scroll for you (it switches off the browser's automatic restoration and does the intuitive thing per navigation type):\n\n- **A push navigation** (a `Link` click, `navigate`, `router.push`) **scrolls to the top** of the new page.\n- **Back and forward restore** the scroll position you had on that entry, so returning to a long list lands you where you were.\n- **A `#hash` target** scrolls that element into view instead of jumping to the top.\n\nTo keep the current scroll on a push navigation, set `scroll={false}` on the `Link` (or `{ scroll: false }` in `NavigateOptions`). This is handy for tab bars or filters that change the URL but should not yank the viewport:\n\n```tsx\n<Toil.Link href=\"/settings/billing\" scroll={false}>Billing</Toil.Link>\n```\n\n```tsx\nnavigate('/settings/billing', { scroll: false });\n```\n\n## Animated transitions\n\nTwo optional effects can animate navigations. Both are **off by default** and are normally enabled from `toil.config.ts`, not in code:\n\n```ts\n// toil.config.ts\nexport default {\n client: {\n viewTransitions: true, // browser View Transitions API (a crossfade between pages)\n transitions: true, // React transition: keep the old page visible while the next loads\n },\n};\n```\n\n`viewTransitions` uses the browser's View Transitions API to crossfade the old and new page (and it respects `prefers-reduced-motion`, animating nothing for users who ask for less motion). `transitions` wraps each navigation in a React transition, keeping the current page on screen while the next route's loader runs, instead of showing its `loading.tsx` right away (smoother, but you trade away the immediate loading state).\n\nConfig is the normal way to turn these on. For a manual override at runtime, the setters are `Toil.setViewTransitions(enabled)` and `Toil.setTransitions(enabled)`:\n\n```tsx\nToil.setViewTransitions(true);\nToil.setTransitions(false);\n```\n\nYou rarely need the setters; prefer the config keys unless you are toggling an effect dynamically.\n\n## Types\n\nAlmost everything on this page is a value on the global `Toil` object, so you call it with no import (`Toil.Link`, `Toil.navigate`, `Toil.useRouter`, and so on). A few of the **types**, though, are not namespaced under `Toil`, so if you want to annotate a prop or a variable you import them from `toiljs/client`:\n\n```tsx\nimport type {\n LinkProps,\n NavLinkProps,\n NavLinkState,\n NavigateOptions,\n RouterInstance,\n} from 'toiljs/client';\n```\n\nFor example, a component that forwards `Link` props, or a helper typed against the router handle:\n\n```tsx\nimport type { RouterInstance } from 'toiljs/client';\n\nfunction logoutThenGo(router: RouterInstance) {\n router.replace('/login');\n}\n```\n\n## Related\n\n- [Routing](./routing.md): how files become URLs, dynamic params, layouts, and templates.\n- [Fetching data](./data-fetching.md): loaders, the typed backend clients, forms, and revalidation.\n- [Rendering and SSR](./rendering.md): what renders on the server versus the browser, and how hydration fits with client navigation.\n\n### Guarding unsaved changes\n\nListen for the cancelable `window` event `toil-before-navigation` and call\n`event.preventDefault()` to keep the current route mounted. Its detail contains\n`href` and `kind` (`navigate` or `history`). Guards run before application navigation;\na cancelled Back/Forward navigation restores the current indexed history entry.\nOnly router-managed history entries can be restored this way. Use a separate\n`beforeunload` listener for document reloads, tab closure, and external navigation.\n",
37
37
  "frontend/README.md": "# Frontend\n\nYour toiljs frontend is a React app with file-based routing that runs in the browser, and can also be rendered ahead of time on the server for a fast first paint and good SEO.\n\nIf you have written React before, everything here is familiar React: components, hooks, JSX. toiljs adds the parts a plain React app makes you wire up yourself: a router, data loading, `<head>` and SEO management, an image component, and a typed client for calling your backend. You get them for free and you do not import most of them, they live on a global called `Toil`.\n\n## What \"frontend\" means here\n\nA toiljs project has three top-level folders. The frontend is the first two:\n\n- **`client/`** is your React app: pages, components, and styles. This is what runs in the user's browser.\n- **`shared/`** is a typed bridge that toiljs generates for you. It lets the browser call your backend with full type safety (see [Fetching data](./data-fetching.md)).\n- **`server/`** is your backend. It compiles to WebAssembly and runs on the edge. That is a separate section (see [Backend](../backend/README.md)).\n\nInside `client/`, the important pieces are:\n\n| Path | What it is |\n| --- | --- |\n| `client/toil.tsx` | The entry file. It imports your routes and global styles and mounts the app. |\n| `client/routes/` | Your pages. One file per URL (file-based routing). See [Routing](./routing.md). |\n| `client/layout.tsx` | The root layout that wraps every page (a header, a footer, and so on). |\n| `client/components/` | Your own reusable React components. |\n| `client/styles/` | Your CSS. See [Styling](./styling.md). |\n| `client/public/` | Static files served as-is (`favicon.ico`, images, `robots.txt`). |\n\nThe entry file is tiny, and you rarely touch it:\n\n```tsx\n// client/toil.tsx\nimport { routes, layout, notFound, globalError, slots } from 'toiljs/routes';\nimport './styles/main.css';\n\nToil.mount(routes, layout, notFound, globalError, slots);\n```\n\n`toiljs/routes` is a virtual module: the compiler scans `client/routes/` and generates the route table for you, so you never hand-maintain a list of pages. Adding a file under `client/routes/` adds a page.\n\n## SPA, but with a server head start\n\nBy default a toiljs app is a single-page application (SPA). The word \"SPA\" means the browser loads one HTML shell, then JavaScript builds every page and swaps between them without a full reload. That makes navigation instant, but it has a classic weakness: the very first load shows a blank page until the JavaScript runs, and simple crawlers that do not run JavaScript see nothing useful.\n\ntoiljs closes that gap in two ways, both optional and both explained in [Rendering and SSR](./rendering.md):\n\n1. **Build-time prerendering.** At build time toiljs bakes each route's `<head>` (title, description, Open Graph, and so on) into real HTML, so crawlers and link-preview bots see correct metadata even without running your code.\n2. **Edge server-side rendering (SSR).** For routes you opt in with `export const ssr = true`, the edge fills in real first-paint HTML for the page body, then the browser \"hydrates\" it (attaches React to the already-drawn markup instead of redrawing it).\n\nHere is the full life of one request, from a cold link click to a warm client-side app:\n\n```mermaid\nsequenceDiagram\n participant U as User's browser\n participant E as Dacely edge\n U->>E: GET /some-page\n E-->>U: HTML (baked head + first-paint body if ssr=true)\n Note over U: First paint appears immediately\n U->>E: fetch the JS bundle + route chunk\n Note over U: React \"hydrates\": attaches to the existing HTML\n Note over U: Page is now interactive\n U->>U: Click a link -> client-side navigation (no reload)\n Note over U: Only the new route's data + chunk are fetched\n```\n\nThe key idea: the server gets you a correct first paint fast, and from then on the app runs entirely in the browser, fetching only small route chunks and data as you navigate.\n\n## The `Toil` global\n\nMost of the client API lives on a global object called `Toil`, so route files need no imports for the common things. A few examples you will meet across these pages:\n\n```tsx\nToil.Link // a client-side navigation link\nToil.NavLink // a Link that knows when it is \"active\"\nToil.useParams() // read dynamic URL params, e.g. { id } for /blog/[id]\nToil.useLoaderData // read data your route's loader fetched\nToil.Image // an <img> that avoids layout shift and lazy-loads\nToil.useHead // set the <title> and <meta> tags\nServer.REST.* // the typed fetch client for your backend\n```\n\n`Server` is also global (it is the typed backend client, see [Fetching data](./data-fetching.md)). Everything on `Toil` is fully typed: your editor autocompletes it, because toiljs generates a `toil-env.d.ts` that maps `Toil` onto the `toiljs/client` package.\n\nThe same fast data utilities your backend uses are available in client code too, as bare globals with no import: `FastMap` and `FastSet` (high-performance map and set collections), and `DataWriter` / `DataReader` (a compact binary codec for encoding and decoding buffers). They are handed to you the same way `Toil` and `Server` are, so you can write `new DataWriter()` straight in a component. See [Data types](../backend/data.md) for the codec and when to reach for it.\n\n## The frontend pages\n\nRead them in roughly this order:\n\n- **[Routing](./routing.md)**: turn files into URLs. Index, nested, and dynamic pages; layouts and templates.\n- **[Navigation](./navigation.md)**: move between pages. Links and active state, navigating in code, typed hrefs, prefetching, scroll restoration, and animated transitions.\n- **[Components](./components.md)**: use your own React components, plus the toiljs primitives (`Image`, `Script`, `Form`, `Slot`, `Head`, and the SSR markers).\n- **[Rendering and SSR](./rendering.md)**: what renders on the server versus in the browser, how hydration works, and the current SSR limitations.\n- **[Styling](./styling.md)**: plain CSS, preprocessors (Sass / Less / Stylus), and Tailwind.\n- **[Images](./images.md)**: the `Toil.Image` component, automatic blur placeholders, and how it stops layout shift.\n- **[Metadata and SEO](./metadata.md)**: set the page title, description, and social-share tags per route.\n- **[Fetching data](./data-fetching.md)**: call your backend with the generated typed clients, submit forms, and read who is logged in.\n- **[Scripts](./scripts.md)**: load external or inline `<script>` tags with a loading strategy, using `Toil.Script`.\n- **[Search](./search.md)**: the built-in, statically-baked page search and command palette (`usePageSearch`).\n- **[The Toil global (reference)](./toil-global.md)**: a complete, grouped list of everything on the `Toil` object.\n\n## Related\n\n- [Getting started](../getting-started/README.md): install toiljs and create a project.\n- [Project structure](../getting-started/project-structure.md): the full folder layout.\n- [Backend overview](../backend/README.md): the `server/` side your frontend talks to.\n- [The CLI](../cli/README.md): `toiljs dev`, `toiljs build`, and every flag.\n",
38
38
  "frontend/rendering.md": "# Rendering and SSR\n\nThis page explains where your pages are built: in the browser, ahead of time at build, or on the server for each request. Getting this right is what makes a page paint fast and rank well.\n\n## The three ways a page can render\n\nA toiljs page can reach the user in three ways. You mostly get all three for free; the only one you opt into per page is edge SSR.\n\n| Mode | Who builds the HTML | When | Good for |\n| --- | --- | --- | --- |\n| **Client rendering** | The browser, from JavaScript | On every visit | Interactive, per-user pages (a dashboard). The default. |\n| **Build-time prerender** | The build, once | When you run `toiljs build` | Baking each route's `<head>` (SEO) into real HTML. Automatic. |\n| **Edge SSR** | The edge server, per request | When you set `ssr = true` | A real first-paint page body plus SEO, for landing and content pages. |\n\nLet us define the two words that trip people up:\n\n- **Rendering** means turning your React components into HTML.\n- **Hydration** means React attaching to HTML that already exists on the page (from the server) instead of throwing it away and redrawing it. Hydration is what makes a server-rendered page interactive without a flash.\n\n## Client rendering (the default)\n\nBy default, toiljs ships a small HTML shell with an empty `<div id=\"root\">`, plus your JavaScript. The browser downloads the JS, React runs, and it builds the page into `#root`. From then on, navigating between pages is pure JavaScript: only the next route's small code chunk and its data are fetched, and the page swaps in place with no reload.\n\nThis is fast to navigate and simple to reason about. Its one weakness is the *first* paint: until the JavaScript runs, `#root` is empty. For an app behind a login (a dashboard) that is fine, nobody is trying to index it. For a public landing page, you usually want one of the two server-assisted modes below.\n\n## Build-time prerender (automatic SEO)\n\nEvery time you build, toiljs renders each static route once and bakes its resolved `<head>` (title, description, canonical link, Open Graph tags, and so on) into that route's HTML file. This happens for all routes with no extra work from you, and it is driven by the `metadata` you export from a route plus the site-wide `seo` config (see [Metadata and SEO](./metadata.md)).\n\nThe payoff: a crawler or a link-preview bot (Slack, Discord, iMessage) that fetches your page sees correct tags immediately, even though it does not run your JavaScript. \"View source\" on a built page shows the real title and meta tags, not an empty shell.\n\nIn production, `toiljs build` writes one prerendered HTML file per route (for example `about/index.html`), and the production static server (`npm start`) serves each route its own prerendered file rather than a single shared shell. That is how each page gets its own metadata in the raw HTML.\n\nBuild-time prerender covers the `<head>`. It does not, by itself, fill in the page *body*: for a client-rendered route the body is still built by React in the browser. To get real first-paint body HTML, opt the route into edge SSR.\n\n### Prerendering dynamic routes with `generateStaticParams`\n\nBuild-time prerender bakes a `<head>` for every **static** route on its own. A **dynamic** route (`client/routes/blog/[id].tsx`, one file that serves `/blog/1`, `/blog/2`, and so on) has no single URL to prerender, so by default it gets none of that per-URL HTML. If you know the concrete URLs ahead of time (a fixed set of blog posts, product pages, or docs), you can opt the route into **static site generation (SSG)**, which means the build renders one HTML file per known URL: it enumerates each URL and writes a real `<url>/index.html` with that page's resolved metadata, plus a `sitemap.xml` entry. This is the toiljs analog of Next.js `generateStaticParams`.\n\nYou opt in by exporting `generateStaticParams` from the dynamic route. It returns one object per URL, keyed by the route's param names:\n\n```tsx\n// client/routes/blog/[id].tsx\n\n// One entry per URL to prerender. `id` matches the [id] segment.\nexport const generateStaticParams: Toil.GenerateStaticParams = () => {\n return [{ id: '1' }, { id: '2' }, { id: '3' }];\n};\n\n// The build runs this once per URL, so each baked page gets its own <head>.\nexport const generateMetadata: Toil.GenerateMetadata = ({ params }) => ({\n title: `Blog post ${params.id}`,\n description: `Reading blog post ${params.id}.`,\n});\n\nexport default function BlogPost() {\n const { id } = Toil.useParams();\n return <h1>Blog post {id}</h1>;\n}\n```\n\nAt build, that writes `blog/1/index.html`, `blog/2/index.html`, and `blog/3/index.html`, each with its own title and description in the raw HTML (so a crawler that does not run JavaScript sees them), and all three land in `sitemap.xml`. The param values also feed the route's `loader`, so per-URL metadata can depend on real data.\n\n`generateStaticParams` is async-friendly (return a promise if you fetch the id list first) and completely opt-in: a dynamic route without it is untouched, and the whole pass is skipped when your project has no `seo` config. A catch-all segment (`[...slug]`) takes an array value: `{ slug: ['2024', 'hello'] }` fills the URL as `2024/hello`.\n\n> Two things this is **not**: it is build-time prerender of the `<head>`, not edge SSR of the body. To also serve real body HTML for these URLs on first paint, add `export const ssr = true` as well (see below). And to make a dynamic route show up in the on-site [search index](./search.md) even though its title is dynamic, export `searchHints` (covered there), which is separate from `generateStaticParams`.\n\n## Edge SSR (`ssr = true`)\n\nFor a route where you want the body content visible on first paint (a marketing page, an article), add one line:\n\n```tsx\n// client/routes/index.tsx\nexport const ssr = true;\n\nexport default function Home() {\n return (\n <section className=\"hero\">\n <h1>Welcome</h1>\n </section>\n );\n}\n```\n\nNow the Dacely edge sends a real, filled-in first paint for that page, and the browser hydrates it. The user sees content immediately, and React takes over without redrawing anything.\n\n### How it works, in brief\n\ntoiljs does something clever to keep SSR cheap. It does not re-run React on the server for every request. Instead, at build time it renders the page once into a **template**: the static HTML with the dynamic bits punched out into named holes. Then, per request, your compiled backend fills only the hole values (a small list of \"slot 3 = this text\"), and the edge splices those values into the pre-baked template. The result is real first-paint HTML produced about as fast as serving a static file.\n\n```mermaid\nsequenceDiagram\n participant B as Build\n participant Edge as Dacely edge\n participant U as Browser\n B->>Edge: prebuilt template (HTML with holes) plus a coherence hash\n U->>Edge: GET / (ssr route)\n Edge->>Edge: run the wasm render to get a small hole-values list\n Edge->>Edge: splice values into the template\n Edge-->>U: real first-paint HTML\n Note over U: Content is visible immediately\n U->>Edge: fetch JS plus route chunk\n Note over U: React hydrates and attaches to the existing HTML\n Note over U: Page is interactive with no redraw or flash\n```\n\nFor SSR to hydrate cleanly, the HTML the server produced and the HTML the browser would produce must match byte-for-byte. toiljs guarantees this by escaping hole values exactly as React does and by carrying a hash that ties the running backend to the exact template it was built against. Authoring the server side of an SSR route (the hole markers in the page and the matching `render` function in `server/`) is a deeper topic that lives with the [backend](../backend/README.md). For most pages you only need `export const ssr = true` and to keep the page \"SSR-safe\" (below).\n\n### Authoring an SSR route\n\n`export const ssr = true` is all you need for a page whose body is fully static. The moment the body has a **dynamic bit** (a value that changes per request: a name from the URL, a list from a loader, a chunk of user HTML), you have to tell the build *where* that dynamic bit lives, so it can be punched out into a hole. You do that by wrapping the dynamic value in a **hole marker**.\n\nWhy is this necessary? The build renders your page once into a template. It cannot guess which `{expression}` in your JSX is a per-request value and which is a constant, so it does not try: any dynamic content you leave unwrapped gets frozen into the template as whatever value it happened to have during that one build render, and it will never update per request. The markers are the explicit \"fill this in later\" signal.\n\nThe markers live on the `Toil` global (so you do not import them), and they are **transparent in the browser**: `<Toil.Hole>` just renders its children, `<Toil.Repeat>` just maps its rows. They only behave differently during the build render, so your client-side app runs exactly as written.\n\n| Marker | Use it for | Shape |\n| --- | --- | --- |\n| `Toil.Hole` | A single dynamic **text** value. | JSX element with `id` + children |\n| `Toil.Repeat` | A **list**: a row template repeated over an `each` array. | JSX element with `id` + `each` + a render function |\n| `Toil.RawHtml` | A block of **pre-rendered HTML** you trust (Markdown you rendered, say). | JSX element with `id` + `html` (+ optional `as`) |\n| `Toil.attr` | A dynamic value in **attribute position** (an `href`, a `class`). | a **function** you call inside the attribute |\n| `Toil.Island` | Content that must render **only in the browser** (the escape hatch). | JSX element with children |\n\n`Toil.attr` is a function rather than an element because an attribute is not a child node, so it cannot be a JSX element. You call it right where the value goes.\n\nHere is a full SSR route: a blog post whose title, body HTML, tag list, and author link all come from the route's `loader`.\n\n```tsx\n// client/routes/blog/[id].tsx\nexport const ssr = true;\n\n// Runs on the server for the first paint, then again on the client to reproduce\n// the same data so hydration matches (see \"Keeping a route SSR-safe\" below).\nexport const loader = async ({ params }: Toil.LoaderArgs) => {\n // Illustrative shape: { title, bodyHtml, tags, authorUrl }.\n return Server.REST.blog.get({ params: { id: params.id } });\n};\n\nexport default function BlogPost() {\n const post = Toil.useLoaderData<typeof loader>();\n return (\n <article>\n <h1>\n <Toil.Hole id=\"title\">{post.title}</Toil.Hole>\n </h1>\n\n {/* A dynamic attribute: call attr() in attribute position. */}\n <a href={Toil.attr('authorUrl', post.authorUrl)}>By the author</a>\n\n {/* A block of trusted, pre-rendered HTML. */}\n <Toil.RawHtml id=\"body\" html={post.bodyHtml} />\n\n {/* A list: one row template, stamped once per item on the server. */}\n <ul>\n <Toil.Repeat id=\"tags\" each={post.tags}>\n {(tag) => <li key={tag}>{tag}</li>}\n </Toil.Repeat>\n </ul>\n </article>\n );\n}\n```\n\nA few rules that keep the template valid:\n\n- **Every marker needs a stable `id`**: a short name unique within the page. The build maps each id to a numbered slot, so keep the ids constant across builds.\n- **`Toil.Repeat` needs at least one row at build time.** It captures that first row as the sub-template for every row, so the build render must see sample data with one or more items (an empty `each` gives it nothing to capture).\n- **`Toil.RawHtml` renders inside a wrapper element** (a `<div>` by default; pass `as=\"section\"` to change the tag), and you own sanitising that HTML, exactly like React's `dangerouslySetInnerHTML`.\n\nAnything that genuinely cannot run on the server (it reads `window`, calls `Date.now()`, or depends on the live URL) goes inside a `Toil.Island`, which renders nothing on the server and reveals its children only after hydration:\n\n```tsx\n<Toil.Island>\n <LiveClock />\n</Toil.Island>\n```\n\nThis is the flow from your marked-up JSX to the first-paint HTML:\n\n```mermaid\nflowchart TD\n A[\"Your SSR route JSX<br/>dynamic bits wrapped in Toil.Hole / Repeat / RawHtml / attr\"] --> B[\"Build render (once, in sentinel mode)\"]\n B --> C[\".tmpl: static HTML with numbered holes\"]\n B --> D[\".slots: which hole is text / raw / attr / repeat\"]\n B --> E[\"Slot enum + coherence HASH\"]\n C --> F[\"Deployed to the Dacely edge\"]\n E --> G[\"Compiled backend render(req)\"]\n G -->|\"per request: fill only the hole values\"| F\n D --> G\n F -->|\"splice values into the template\"| H[\"First-paint HTML to the browser\"]\n```\n\n#### Advanced: hand-writing the server `render`\n\nYou almost never do this. The compiler generates the server `render(req)` for an SSR route from the JSX above, so the hole ids line up automatically. But the server side is backed by a plain, hand-writable API for the rare case you need full control (an unusual template, or a value the compiler cannot derive). Your `render` returns a `SlotValues` object, filled with:\n\n- `setText(slot, value)`: a text hole (React-escaped for you).\n- `setRaw(slot, html)`: a raw-HTML hole (you own sanitising).\n- `setAttr(slot, value)`: an attribute hole.\n- `setRepeat(slot, rows)`: a repeat region, with the rows assembled through an `HtmlBuilder` (chain `.raw(...)`, `.text(...)`, `.attr(...)`).\n- `setHeader(name, value)`, `setTitle(title)`, and `setStatus(code)`: response headers, a per-request `<title>`, and the status code.\n\n`SlotValues` and `HtmlBuilder` live in `server/runtime/ssr/slots.ts`. The `setTitle` helper is the supported way to give an SSR page a data-driven `<title>` (a blog post's real title from its loader), overriding the one baked into the template. This is server (backend) code, so it belongs with the [backend](../backend/README.md).\n\n### Keeping a route SSR-safe\n\nServer rendering happens where there is no browser: no `window`, no `document`, no mouse. So an SSR route (and every layout above it) must render without touching browser-only APIs during that first render. Anything that must run only in the browser (reading `window`, using `Date.now()`, or router hooks that need the live URL) goes inside an **island**, a marker that renders nothing on the server and appears only after hydration.\n\nIf a route or one of its layouts throws while rendering on the server, toiljs does not ship a broken page. It **skips SSR for that route at build with a warning** and falls back to plain client rendering. So adding `ssr = true` is always safe: worst case you get client rendering plus a build warning telling you what to move into an island.\n\n### Suspense markers and self-healing hydration\n\nUnder the hood the client wraps each route and layout in React `Suspense` boundaries that line up with what the server emitted, so hydration matches. If hydration ever does mismatch (the server HTML and the client's idea of the page disagree), React does the safe thing: it discards the server markup for that part and re-renders it on the client. You get a correct page either way. The cost of a mismatch is a small flash and some wasted work, not a broken page, which is why the guidance above (keep it SSR-safe, put browser-only bits in islands) is about smoothness, not correctness.\n\n## Known SSR limitations\n\nBe aware of these honest gaps as of today:\n\n- **`template.tsx` is not server-rendered.** A `template.tsx` wrapper (the re-mounting cousin of a layout) is not part of the SSR output. A route under one still works: hydration self-heals to client rendering for that part.\n- **Parallel slots (`@slot`) are not server-rendered.** Slot content (including intercepted modals) renders on the client after hydration, not in the first paint. Since slots are typically modals and overlays that appear on interaction, this is rarely a problem.\n- **Islands have no first paint or SEO.** That is by design: an island is your \"client only\" escape hatch, so anything inside it is intentionally absent from the server HTML and from what crawlers see.\n- **The client loader must reproduce the server's data.** For a hole whose value comes from the request (a query param), the route's client `loader` has to derive the same value the server used, or hydration will re-render that part. If the client cannot reproduce a value, put that content in an island.\n\n## Which mode should I use?\n\n- **Interactive, per-user page** (dashboard, settings): client rendering. Do nothing.\n- **Public page that needs correct link previews and titles**: you already have build-time prerender. Do nothing extra.\n- **Public page that should also show its content instantly on first load** (landing page, blog post, docs): add `export const ssr = true` and keep it SSR-safe.\n\n## Related\n\n- [Backend overview](../backend/README.md): where the server-side `render` for an SSR route lives.\n- [Metadata and SEO](./metadata.md): what gets baked into the `<head>`.\n- [Routing](./routing.md): layouts, templates, and slots.\n- [Fetching data](./data-fetching.md): loaders and how their data seeds hydration.\n",
39
39
  "frontend/routing.md": "# Routing\n\ntoiljs uses file-based routing: every file under `client/routes/` becomes a page, and its path on disk becomes its URL. There is no route config to maintain, you create a file and the route exists.\n\nThis page is about **frontend** (browser) routing: the pages a user visits and how they navigate between them. Your **backend** HTTP endpoints use a different, decorator-based system (see [HTTP routes](../backend/rest.md)).\n\n## The basic idea\n\nDrop a React component at `client/routes/<something>.tsx`, `export default` it, and it is a page. The default export is the component that renders at that URL.\n\n```tsx\n// client/routes/about.tsx -> /about\nexport default function About() {\n return (\n <main>\n <h1>About us</h1>\n </main>\n );\n}\n```\n\nRun `toiljs dev`, open `/about`, and it is there. Nothing else to register.\n\n## How a file path becomes a URL\n\nThe compiler scans `client/routes/` and turns each file into a URL pattern with a small set of rules. Here is the whole mapping in one table:\n\n| File under `client/routes/` | URL | What it is |\n| --- | --- | --- |\n| `index.tsx` | `/` | The home page. |\n| `about.tsx` | `/about` | A static page. |\n| `blog/index.tsx` | `/blog` | An index inside a folder. |\n| `blog/[id].tsx` | `/blog/:id` | A **dynamic** page (one param). |\n| `docs/[...slug].tsx` | `/docs/*` | A **catch-all** (one or more segments). |\n| `files/[[...slug]].tsx` | `/files` and `/files/*` | An **optional** catch-all (zero or more). |\n| `(marketing)/pricing.tsx` | `/pricing` | A **route group**: parens add no URL segment. |\n| `@modal/photo/[id].tsx` | (a parallel slot, see below) | A named **slot**. |\n\nThe rules, spelled out:\n\n- **`index`** means \"the folder itself\", so `blog/index.tsx` is `/blog`, and the top `index.tsx` is `/`.\n- **Square brackets** mark a dynamic segment. `[id]` captures one path segment into a param named `id`. You read it with `Toil.useParams()`.\n- **`[...name]`** is a catch-all: it captures the rest of the path (one segment or more) as a single `/`-joined string.\n- **`[[...name]]`** is an *optional* catch-all: like `[...name]`, but it also matches the bare parent URL with nothing after it (the param is then absent).\n- **Parentheses** like `(marketing)` create a **route group**: a folder that organizes files without adding anything to the URL. Handy for grouping pages that share a layout.\n\n```mermaid\nflowchart LR\n A[\"client/routes/blog/[id].tsx\"] -->|scan| B[\"/blog/:id\"]\n B -->|visit /blog/42| C[\"params = { id: '42' }\"]\n C -->|Toil.useParams| D[\"render Blog post 42\"]\n```\n\n### Dynamic pages: reading the param\n\nA file named with brackets gets its captured values from `Toil.useParams()`:\n\n```tsx\n// client/routes/blog/[id].tsx -> /blog/:id\nexport default function BlogPost() {\n const { id } = Toil.useParams();\n return (\n <main>\n <h1>Blog post {id}</h1>\n </main>\n );\n}\n```\n\nVisiting `/blog/42` renders \"Blog post 42\". Param values are URL-decoded for you.\n\nFor a catch-all, the param is the whole tail joined with slashes:\n\n```tsx\n// client/routes/docs/[...slug].tsx -> /docs/*slug\nexport default function Docs() {\n const { slug } = Toil.useParams();\n // /docs/getting-started/install -> slug === \"getting-started/install\"\n return <main>{slug}</main>;\n}\n```\n\nAn optional catch-all (`[[...slug]]`) matches the bare parent too, so `slug` can be empty:\n\n```tsx\n// client/routes/files/[[...slug]].tsx -> matches /files AND /files/a/b\nexport default function Files() {\n const { slug } = Toil.useParams();\n // \"/files\" -> slug is undefined; \"/files/a/b\" -> slug === \"a/b\"\n return <main>{slug ?? '(the base /files page)'}</main>;\n}\n```\n\n### When two routes could match\n\nIf a URL could match more than one pattern, toiljs picks the most specific one. Static segments win over dynamic (`:id`) segments, which win over catch-alls (`*slug`), and deeper routes win over shallower ones. So `/blog/new` prefers a literal `blog/new.tsx` over `blog/[id].tsx` if both exist. You do not configure this, it just does the intuitive thing.\n\n## Special files\n\nSome filenames are not pages, they are helpers that wrap or replace pages. They live alongside your routes and are never matched as a URL:\n\n| Filename | Role |\n| --- | --- |\n| `layout.tsx` | Wraps the pages in its folder (and below). **Persists** across navigation. |\n| `template.tsx` | Like a layout, but **re-mounts** on every navigation within it. |\n| `loading.tsx` | Shown while the page (and its data) is loading. |\n| `error.tsx` | Shown when the page throws (an error boundary). |\n| `global-error.tsx` | The last-resort error boundary, wraps even the root layout. |\n| `404.tsx` (or `not-found.tsx`) | Shown when no route matches. |\n\n### Layouts\n\nA `layout.tsx` receives the page (or nested layout) as `children` and renders around it. The root `client/layout.tsx` wraps every page. It is the natural home for your header, footer, and site-wide `<head>` defaults:\n\n```tsx\n// client/layout.tsx\nimport type { ReactNode } from 'react';\nimport Header from './components/Header';\nimport Footer from './components/Footer';\n\nexport default function Layout({ children }: { children?: ReactNode }) {\n return (\n <div className=\"app\">\n <Header />\n <main className=\"content\">{children}</main>\n <Footer />\n </div>\n );\n}\n```\n\nLayouts nest by folder. A `client/routes/dashboard/layout.tsx` wraps every page under `/dashboard`, inside the root layout. Because a layout **persists** across navigations, state inside it (a sidebar's open/closed flag, a scroll position) survives when you move between its child pages.\n\n### Templates vs layouts\n\nA `template.tsx` looks like a layout but does the opposite on navigation: it **re-mounts** every time you move to a new page within it. Use a layout when state should persist (a nav sidebar), and a template when it should reset (an enter animation that should replay, or a counter that should start fresh per page).\n\n### Loading and error states\n\nPut a `loading.tsx` next to a page (or in a folder) and it shows automatically while that page's chunk and its `loader` data are still resolving:\n\n```tsx\n// client/routes/dashboard/loading.tsx\nexport default function Loading() {\n return <p>Loading dashboard...</p>;\n}\n```\n\nPut an `error.tsx` there and it catches any error the page throws, showing a fallback instead of a blank screen. `global-error.tsx` sits outside the root layout, so it catches errors thrown by the layout itself.\n\n## Route groups: shared layout, no URL change\n\nWrap a folder name in parentheses to group files without changing their URLs. This is the trick for \"these three pages share one layout, but I do not want a `/legal` prefix\":\n\n```\nclient/routes/\n (legal)/\n layout.tsx -> wraps both pages below\n privacy.tsx -> /privacy (NOT /legal/privacy)\n terms.tsx -> /terms\n```\n\n## Parallel slots (`@slot`) and intercepting routes\n\nThis is an advanced feature; skip it until you need a modal that keeps the page behind it alive.\n\nA folder starting with `@`, like `@modal`, is a **named slot**. It is a whole second route tree that matches the current URL *independently* of the main page, and renders wherever you place a `<Toil.Slot>`:\n\n```tsx\n// client/routes/gallery/layout.tsx\nimport type { ReactNode } from 'react';\n\nexport default function GalleryLayout({ children }: { children?: ReactNode }) {\n return (\n <div>\n {children}\n <Toil.Slot name=\"modal\" /> {/* renders the @modal slot for this URL */}\n </div>\n );\n}\n```\n\nThe `@` folder adds nothing to the URL; it just says \"these routes fill the slot named `modal`\". A slot with no match renders nothing (or a `fallback` you pass).\n\nAn **intercepting route** is a slot route whose folder name starts with `(.)`, `(..)`, or `(...)`. It hijacks a *soft* (in-app) navigation to another URL and renders that URL's content inside the slot instead, while the main page stays mounted behind it. That is exactly how a \"click a photo, see it in a modal over the gallery\" pattern works, where reloading the page (a hard load) shows the full photo page instead:\n\n```\nclient/routes/gallery/\n index.tsx -> /gallery\n photo/[id].tsx -> /gallery/photo/:id (full page, on hard load)\n @modal/(.)photo/[id].tsx -> fills @modal on a soft click to that URL\n```\n\nThe `(.)` markers mean: `(.)` same level, `(..)` up one level, `(...)` from the routes root. This tells the interceptor which real URL it is standing in for.\n\n## Links and navigation\n\nThis is a quick tour. For the full treatment, programmatic navigation, typed hrefs, prefetching, scroll restoration, and animated transitions, see [Navigation](./navigation.md).\n\n### `Toil.Link`\n\nUse `Toil.Link` instead of a plain `<a>` for in-app links. It navigates client-side (no full page reload), and prefetches the target route's code on hover or focus so the click feels instant:\n\n```tsx\n<Toil.Link href=\"/about\">About</Toil.Link>\n```\n\n`Link` accepts every normal anchor attribute (`className`, `target`, `rel`, `download`, and so on), plus a few toiljs controls:\n\n| Prop | Default | What it does |\n| --- | --- | --- |\n| `replace` | `false` | Replace the current history entry instead of pushing a new one. |\n| `scroll` | `true` | Scroll to the top after navigating. |\n| `prefetch` | `true` | Prefetch the route on hover/focus. Set `false` to opt out. |\n\n`Link` is smart about when *not* to intercept: external URLs, `target=\"_blank\"`, `download`, `#hash` links, and modified clicks (Ctrl/Cmd/middle-click) all fall through to normal browser behavior. The `href` is typed to your project's real routes, so a typo is a compile error.\n\n### `Toil.NavLink` and active state\n\n`NavLink` is a `Link` that knows whether it points at the current page. When active it adds the class `active` (configurable) and `aria-current=\"page\"`. This is what you want for a navigation bar that highlights the current section:\n\n```tsx\n<Toil.NavLink href=\"/blog\" activeClassName=\"is-current\">\n Blog\n</Toil.NavLink>\n```\n\nYou can also drive `className`, `style`, or `children` from the active state with a function:\n\n```tsx\n<Toil.NavLink href=\"/blog\" className={({ isActive }) => (isActive ? 'on' : 'off')}>\n Blog\n</Toil.NavLink>\n```\n\nBy default a parent link is active for its sub-paths too (`/blog` is active on `/blog/42`). Pass `end` to require an exact match:\n\n```tsx\n<Toil.NavLink href=\"/\" end>Home</Toil.NavLink>\n```\n\n### Navigating in code\n\nFor navigation that is not a link (after a form submit, a redirect), use the router hook or the free `navigate` function:\n\n```tsx\nexport default function Login() {\n const router = Toil.useRouter();\n const onDone = () => router.push('/dashboard');\n // router.replace(href), router.back(), router.forward(), router.refresh()\n return <button onClick={onDone}>Continue</button>;\n}\n```\n\n`useRouter()` returns a handle with `push`, `replace`, `back`, `forward`, `refresh` (re-run the current page's data loader), `revalidate` (refetch data), and `prefetch`.\n\nIf you just need to jump to a URL and nothing else, `Toil.useNavigate()` returns the bare `navigate(href, options)` function:\n\n```tsx\nexport default function Login() {\n const navigate = Toil.useNavigate();\n return (\n <button onClick={() => navigate('/dashboard', { replace: true })}>\n Continue\n </button>\n );\n}\n```\n\nThe options are `{ replace?: boolean; scroll?: boolean }`, the same two controls `Toil.Link` exposes above. The same function is also available free of any hook as `Toil.navigate(href, options)`, which is handy in code that is not a React component (a plain event handler, a utility module).\n\n### Typed hrefs and the `href()` escape hatch\n\nEvery href you pass to `Toil.Link`, `navigate`, or `router.push` is type-checked against your project's real routes. The compiler scans `client/routes/` and generates a file called `toil-routes.d.ts` that registers the union of your route paths (it fills in the `Href` type). So a typo is a compile error before you ever run the app:\n\n```tsx\n<Toil.Link href=\"/abuot\">About</Toil.Link>\n// ^ Type error: \"/abuot\" is not assignable to type 'Href'. (No such route.)\n```\n\nDynamic routes show up in that union as template-literal types. A file at `client/routes/blog/[id].tsx` contributes the type `` `/blog/${string}` ``, so a link built with one interpolation usually still checks:\n\n```tsx\n<Toil.Link href={`/blog/${post.id}`}>Read</Toil.Link> // fine: matches `/blog/${string}`\n```\n\nThe catch is a URL assembled from several data pieces (or one TypeScript cannot prove the shape of): it is typed as a plain `string`, and `string` is not assignable to `Href`, so you get a type error:\n\n```tsx\nconst cat = product.category; // string\nconst slug = product.slug; // string\nnavigate(`/${cat}/${slug}`);\n// ^ Argument of type 'string' is not assignable to parameter of type 'Href'.\n```\n\nThe escape hatch is `Toil.href()`. It takes a `string` and asserts it is a valid href, returning the `Href` type the navigation APIs expect. Use it right at the call site once you are sure the path is a real route:\n\n```tsx\nnavigate(Toil.href(`/${cat}/${slug}`)); // asserted valid\n<Toil.Link href={Toil.href(`/${cat}/${slug}`)}>Open</Toil.Link>;\n```\n\n`href()` is a pure type assertion (it returns the string unchanged), so reach for it only when the type-check is in your way, not to paper over a genuine typo. Before the routes are generated (a fresh project, the first build), `Href` is just `string`, so nothing complains until `toil-routes.d.ts` exists.\n\n### Reading the current location\n\nA handful of hooks let a component read where it is:\n\n| Hook | Returns |\n| --- | --- |\n| `Toil.useParams()` | The dynamic route params, e.g. `{ id }`. |\n| `Toil.usePathname()` | The current path, e.g. `\"/blog/42\"`. |\n| `Toil.useLocation()` | The current path, e.g. `\"/blog/42\"` (an alias of `usePathname()`). |\n| `Toil.useSearchParams()` | The query string as a `URLSearchParams`. |\n| `Toil.useNavigationPending()` | `true` while a navigation is in flight (for a loading bar). |\n\n## Loading data for a route\n\nA route file can `export const loader` alongside its component. The loader runs on navigation, in parallel with loading the page's code, and the page reads the result with `Toil.useLoaderData`. This keeps data fetching out of `useEffect` and lets `loading.tsx` show while it runs. Loaders are covered in depth in [Fetching data](./data-fetching.md):\n\n```tsx\nexport const loader = async ({ params }: Toil.LoaderArgs) => {\n const post = await Server.REST.blog.get({ params: { id: params.id } });\n return post;\n};\n\nexport default function BlogPost() {\n const post = Toil.useLoaderData<typeof loader>();\n return <article><h1>{post.title}</h1></article>;\n}\n```\n\n## Gotchas\n\n- **`export default` is required.** A route file without a default-exported component is not a page. The special files (`layout`, `loading`, and so on) also use the default export.\n- **Reserved filenames are not pages.** `layout.tsx`, `template.tsx`, `loading.tsx`, `error.tsx`, `global-error.tsx`, `404.tsx`, and `not-found.tsx` are helpers, not routes. Do not name a real page one of these.\n- **Use `Toil.Link`, not `<a>`, for in-app links.** A plain `<a href=\"/about\">` triggers a full page reload, throwing away the SPA speed. Reserve `<a>` for external links.\n- **Params are always strings.** `useParams()` gives you `{ id: \"42\" }`, not a number. Convert if you need a number.\n- **Route groups and slots are invisible in the URL.** `(group)` and `@slot` folders never appear in the address bar. If a URL looks wrong, check for a stray bracket or paren in the file path.\n\n## Related\n\n- [Rendering and SSR](./rendering.md): what renders on the server versus the browser.\n- [Fetching data](./data-fetching.md): loaders, the typed backend clients, and forms.\n- [Metadata and SEO](./metadata.md): set the title and tags per route.\n- [Backend HTTP routes](../backend/rest.md): the separate, decorator-based server routing.\n",
@@ -41,13 +41,14 @@ export const TOIL_DOCS = {
41
41
  "frontend/search.md": "# Page search\n\ntoiljs ships a built-in search over your own pages: at build time it indexes every route's metadata (title, description, keywords, Open Graph), and at runtime you query that index to build a search box or a command palette that jumps straight to a page. It is entirely static (no server, no search service), and there is no equivalent in Next.js, so this page explains it from scratch.\n\n## What it is\n\nEvery route in your app can export `metadata` (its title, description, and so on, see [Metadata and SEO](./metadata.md)). The toiljs compiler reads that metadata from all your routes at build time and bakes a small **page index** into your bundle: a list of `{ path, title, description, keywords, ... }` for every page. When your app starts, that index is registered in the browser. You can then search it instantly, offline, with zero network calls, and turn a match into a navigation.\n\nThis is what powers a \"jump to any page\" box or a `Cmd+K` command palette without you maintaining a list of pages or standing up a search backend.\n\n```mermaid\nflowchart TD\n A[\"Your routes<br/>each with export const metadata\"] -->|build: scan + extract| B[\"Static page index<br/>{ path, dynamic, metadata } per page\"]\n B -->|baked into the bundle| C[\"App startup: Toil.registerPages(index)\"]\n C --> D[\"Browser: usePageSearch / searchPages query the index\"]\n D --> E[\"Ranked results -> goTo() navigates\"]\n```\n\nOnly **statically-known** metadata is indexed. A route whose `<head>` comes from a dynamic `generateMetadata` (a per-request title) has nothing to index by default; to include it anyway, export `searchHints` (see below).\n\n## The `usePageSearch` hook\n\n`usePageSearch` is the React way in. Give it the current query string, and it hands back ranked results plus a helper to navigate:\n\n```tsx\nconst { results, pages, goTo } = Toil.usePageSearch(query);\n```\n\nIt returns an object with three fields:\n\n| Field | Type | What it is |\n| --- | --- | --- |\n| `results` | `readonly PageSearchResult[]` | The ranked matches for `query`, best first. Empty when the query is blank. |\n| `pages` | `readonly PageMeta[]` | The full page index, handy for showing an \"all pages\" listing. |\n| `goTo` | `(target, options?) => void` | Navigates to a result, a page, or a raw path string. A stable reference (safe to pass to a child or destructure). |\n\nEach item in `results` is a `PageSearchResult`:\n\n- `page`: the matched `PageMeta`, which is `{ path, dynamic, metadata }`. `path` is the route URL (`'/about'`), `dynamic` says whether it has `:param` segments, and `metadata` is the indexed title/description/etc.\n- `score`: a relevance number (higher is better; always above zero for a returned result).\n- `matches`: which fields matched, for example `['title', 'keywords']`.\n\nThe results are memoised, so they recompute only when the query (or options) change, not on every render.\n\n### A full search box\n\nHere is a complete search page: an input, a ranked result list, and a click that navigates to the chosen page.\n\n```tsx\n// client/routes/search.tsx\nimport { useState } from 'react';\n\nexport const metadata: Toil.Metadata = {\n title: 'Search',\n description: 'Find any page and jump straight to it.',\n keywords: ['search', 'find', 'pages'],\n};\n\nexport default function SearchPage() {\n const [query, setQuery] = useState('');\n const { results, pages, goTo } = Toil.usePageSearch(query);\n\n return (\n <main>\n <h1>Search</h1>\n <input\n type=\"search\"\n value={query}\n onChange={(e) => {\n setQuery(e.target.value);\n }}\n placeholder={`Search ${pages.length} pages...`}\n aria-label=\"Search pages\"\n autoFocus\n />\n\n {query.trim() !== '' && (\n <ul>\n {results.length === 0 && <li>No pages match \"{query}\".</li>}\n {results.map((r) => (\n <li key={r.page.path}>\n <button\n type=\"button\"\n onClick={() => {\n goTo(r);\n }}\n >\n <strong>{r.page.metadata.title ?? r.page.path}</strong>{' '}\n <code>{r.page.path}</code>\n {r.page.metadata.description !== undefined && (\n <p>{r.page.metadata.description}</p>\n )}\n </button>\n </li>\n ))}\n </ul>\n )}\n </main>\n );\n}\n```\n\n`goTo` accepts a result (as above), a `PageMeta`, or a plain path string, and takes the same options as `navigate` (see [Routing](./routing.md)). Passing a result is the common case.\n\n### Options\n\n`usePageSearch` takes a second argument to tune the search:\n\n```tsx\nconst { results } = Toil.usePageSearch(query, {\n limit: 8, // cap the number of results (after ranking)\n includeDynamic: true, // include :param routes (see below); default false\n fields: ['title', 'keywords'], // only match these fields; default all fields\n});\n```\n\n| Option | Type | Default | Effect |\n| --- | --- | --- | --- |\n| `limit` | `number` | no cap | Keep only the top N results after ranking. |\n| `includeDynamic` | `boolean` | `false` | Include dynamic (`:param` / `*catch-all`) routes. Off by default because you cannot navigate to them without filling in the params. |\n| `fields` | array of field names | all fields | Restrict matching to a subset of `'title'`, `'description'`, `'keywords'`, `'path'`, `'openGraph'`. |\n\n## Making dynamic routes searchable with `searchHints`\n\nA dynamic route like `client/routes/blog/[id].tsx` usually produces its `<head>` with a per-post `generateMetadata`, so there is nothing static to index, and the route is missing from search. To surface it anyway, export `searchHints`: a small static object the compiler merges into the index for that route (winning ties against any static `metadata`).\n\n```tsx\n// client/routes/blog/[id].tsx\n\n// The per-post <title> is dynamic, so it cannot be indexed. These static hints\n// put the blog into the search index so it shows up when someone searches \"blog\".\nexport const searchHints: Toil.SearchHints = {\n title: 'Blog',\n description: 'Articles and updates.',\n keywords: ['blog', 'posts', 'articles'],\n};\n```\n\n`SearchHints` has three optional fields: `title`, `description`, and `keywords` (a string or an array of strings).\n\nTwo things to keep in mind. First, `searchHints` only affects the index; the route is still dynamic, so it appears in results only when you pass `includeDynamic: true`. Second, you cannot `goTo` a dynamic page directly (it needs concrete params), so `goTo` is a no-op for one unless you hand it a real, filled-in path string. In practice you use `searchHints` to make a *section* discoverable (typing \"blog\" surfaces the blog), and point the user at a concrete landing URL.\n\n## The lower-level API\n\nThe hook is a thin wrapper over a small, framework-agnostic core you can use directly (outside React, in tests, or to build your own UI). All of these are on the `Toil` global:\n\n- `Toil.searchPages(query, options?)`: the pure ranking function behind the hook. Returns `PageSearchResult[]`. Same options as the hook.\n- `Toil.getPages()`: the full registered index (`readonly PageMeta[]`), including dynamic routes. Empty before registration.\n- `Toil.pagePath(target)`: normalises a result, a page, or a raw string down to its path string.\n- `Toil.registerPages(pages)`: replaces the live index. toiljs calls this for you at startup with the compiler-built index, so you rarely call it yourself. It exists for tests and advanced setups that build a custom index.\n\n```tsx\nimport { searchPages } from 'toiljs/client';\n\n// Outside a component: the top 5 title/keyword matches for \"billing\".\nconst hits = searchPages('billing', { limit: 5, fields: ['title', 'keywords'] });\n```\n\n## How ranking works\n\nThe matcher is simple and predictable:\n\n- The query is lower-cased and split on whitespace into terms. **Every** term must match somewhere (AND semantics), or the page is dropped. A blank query returns nothing.\n- Matching is case-insensitive and substring-based, with bonuses: an exact field match ranks highest, then a match at the start of the field, then a match at the start of a word inside the field, then a plain mid-word substring.\n- Each field carries a weight, so a hit in the title counts for much more than a hit in the description:\n\n| Field | Weight |\n| --- | --- |\n| `title` | 10 |\n| `path` | 6 |\n| `keywords` | 5 |\n| `description` | 3 |\n| `openGraph` | 2 |\n\n- The `path` field is made word-searchable: `/get-started` matches \"get\" or \"started\".\n- Results are sorted by score (highest first), ties broken alphabetically by path for a stable order, then cut to `limit` if you set one.\n\n## Gotchas\n\n- **Only static metadata is indexed.** A route whose title comes from `generateMetadata` is invisible to search until you add `searchHints`.\n- **Dynamic routes are excluded by default.** They need `includeDynamic: true` to appear, and even then `goTo` cannot navigate to one without concrete params.\n- **The index is built, not live.** It reflects the metadata at build time. Add a route or change a title and you must rebuild for search to see it.\n- **A blank query returns no results**, not every page. If you want an \"all pages\" listing for an empty box, render `pages` yourself.\n- **The type names** `PageSearchResult`, `PageSearchOptions`, and `SearchField` are importable from `'toiljs/client'` if you need to annotate them; `PageMeta` and `SearchHints` are also available as `Toil.PageMeta` / `Toil.SearchHints`.\n\n## Related\n\n- [Metadata and SEO](./metadata.md): the `metadata` the search index is built from.\n- [Routing](./routing.md): route paths, dynamic segments, and `navigate` (what `goTo` calls).\n- [Rendering and SSR](./rendering.md): where `searchHints` fits alongside `generateStaticParams` for dynamic routes.\n- [Frontend overview](./README.md): the `Toil` global and the rest of the client API.\n",
42
42
  "frontend/styling.md": "# Styling\n\ntoiljs does not force a styling system on you. It builds with Vite, so you style your app the way you would any Vite + React project: plain CSS, CSS Modules, a preprocessor like Sass, or Tailwind. This page shows the practical options.\n\n## The one import that matters\n\nYour app pulls in global styles from the entry file, `client/toil.tsx`:\n\n```tsx\n// client/toil.tsx\nimport { routes, layout, notFound, globalError, slots } from 'toiljs/routes';\nimport './styles/main.css'; // <- your global stylesheet\n\nToil.mount(routes, layout, notFound, globalError, slots);\n```\n\nThat one import is where your global CSS (resets, CSS variables, base element styles) lives. A typical `client/styles/main.css` sets up theme variables and base styles:\n\n```css\n/* client/styles/main.css */\n:root {\n --accent: #2563ff;\n --bg: #080d11;\n --text: #f5f6fa;\n}\n\n*, *::before, *::after {\n box-sizing: border-box;\n}\n\nbody {\n margin: 0;\n background: var(--bg);\n color: var(--text);\n font-family: system-ui, -apple-system, sans-serif;\n line-height: 1.6;\n}\n```\n\n## Importing CSS anywhere\n\nYou are not limited to the one global sheet. Any component can import its own CSS, and Vite bundles it in:\n\n```tsx\n// client/components/Card.tsx\nimport './card.css';\n\nexport default function Card() {\n return <div className=\"card\">...</div>;\n}\n```\n\nThe class names in a plain `.css` file are global (they apply everywhere), so name them carefully to avoid clashes, or reach for CSS Modules.\n\n## CSS Modules (scoped class names)\n\nIf you name a file `*.module.css`, Vite treats it as a **CSS Module**: the class names are locally scoped to the component that imports them, so two components can both use `.title` without colliding. You import the generated names as an object:\n\n```tsx\n// client/components/Card.tsx\nimport styles from './card.module.css';\n\nexport default function Card() {\n return <div className={styles.card}>...</div>;\n}\n```\n\n```css\n/* client/components/card.module.css */\n.card {\n padding: 1rem;\n border: 1px solid var(--border);\n}\n```\n\nThis is the simplest way to get component-scoped styles with no extra tooling.\n\n## Preprocessors: Sass, Less, Stylus\n\nWhen you create a project (`toiljs create`) you can pick a CSS preprocessor, or add one later to an existing project with the `configure` command:\n\n```sh\ntoiljs configure # interactive: choose preprocessor + Tailwind\ntoiljs configure --style sass # switch the preprocessor to Sass\n```\n\n`configure` installs the right packages and rewrites your style imports for you. After that, `.scss` / `.sass` / `.less` / `.styl` files import exactly like `.css` files, and Vite compiles them.\n\n## Tailwind\n\nTo use Tailwind (a utility-class CSS framework), turn it on at create time or add it later:\n\n```sh\ntoiljs configure --tailwind\n```\n\nTailwind lives in its own stylesheet, `client/styles/tailwind.css`, which is just:\n\n```css\n@import \"tailwindcss\";\n```\n\n`configure` wires that import in for you. From then on you use Tailwind's utility classes directly in your JSX:\n\n```tsx\nexport default function Cta() {\n return (\n <button className=\"rounded-lg bg-blue-600 px-4 py-2 font-semibold text-white\">\n Get started\n </button>\n );\n}\n```\n\n## Inline styles and the `style` prop\n\nStandard React inline styles work as always, and are handy for one-off, dynamic values:\n\n```tsx\n<div style={{ padding: 24, background: 'var(--surface)' }}>...</div>\n```\n\nPrefer classes for anything reused; keep inline styles for the truly per-instance case (a computed width, a dynamic color).\n\n## How types work for style and asset imports\n\ntoiljs generates a `toil-env.d.ts` in your project that declares the import types, so TypeScript is happy importing stylesheets and images. It covers `.css`, `.scss`, `.sass`, `.less`, `.styl` (and friends), plus image formats (`.svg`, `.png`, `.jpg`, `.webp`, and so on), which import as a URL string:\n\n```tsx\nimport logoUrl from './logo.svg'; // logoUrl is a string (a hashed URL)\n\n<img src={logoUrl} alt=\"Logo\" />\n```\n\nFor images specifically, prefer the `Toil.Image` component and its `?toil` import, which also handle layout shift and blur placeholders (see [Images](./images.md)).\n\n## Gotchas\n\n- **Global CSS is global.** A plain `.css` import puts its class names in one shared namespace. If two files both define `.button`, the last one loaded wins. Use `*.module.css` (CSS Modules) or unique prefixes to scope styles.\n- **Do not hand-edit `toil-env.d.ts`.** It is generated. If a new file type is not recognized, re-run the build so it regenerates, rather than editing it.\n- **Preprocessor packages are managed by `configure`.** Add or switch preprocessors through `toiljs configure` so the packages and imports stay in sync; do not install them by hand.\n\n## Related\n\n- [Images](./images.md): the `Toil.Image` component and image imports.\n- [The CLI](../cli/README.md): `toiljs configure` and every flag.\n- [Frontend overview](./README.md): where styles fit in the `client/` folder.\n",
43
43
  "frontend/toil-global.md": "# The Toil global (reference)\n\nAlmost the entire toiljs client API hangs off one global object, `Toil`, so your route files need no imports for the everyday things. This page is the full index: every `Toil.*` member, its TypeScript signature, and a one-line description, grouped by area. Reach for it when you know roughly what you want and just need the exact name or shape; the deeper guides (linked per section) explain the concepts.\n\n`Toil` is not a hand-written object. It is the module namespace of the `toiljs/client` package, exposed as a global. The compiler writes a `toil-env.d.ts` at your project root containing `declare const Toil: typeof import('toiljs/client')`, so `Toil` is typed as the whole package and every member autocompletes with no import. Put another way: anything you could `import { X } from 'toiljs/client'` is reachable as `Toil.X`.\n\nA few members are also bare globals, handed to you the same way. `Server` (the typed backend client) and `parseError` are global on their own and additionally live under `Toil` (so `Server` and `Toil.Server` are the same value, likewise `parseError`). `FastMap`, `FastSet`, `DataWriter`, and `DataReader` (the fast collections and compact binary codec from `toiljs/io`, see [Data types](../backend/data.md)) are bare globals only: they are not under `Toil`, so write `new DataWriter()`, never `new Toil.DataWriter()`.\n\n## Components (JSX)\n\nDrop-in components you render in JSX. See [Images](./images.md), [Scripts](./scripts.md), [Fetching data](./data-fetching.md), and [Metadata and SEO](./metadata.md) for the detail.\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `Toil.Image` | `Image(props: ImageProps): ReactNode` | A drop-in `<img>` that reserves space (no layout shift), lazy-loads, and can fade in from a blur placeholder. See [Images](./images.md). |\n| `Toil.Script` | `Script(props: ScriptProps): ReactNode` | Loads an external or inline `<script>` with a load `strategy`, deduplicated so it runs at most once. Renders nothing. See [Scripts](./scripts.md). |\n| `Toil.Form` | `Form(props: FormProps): ReactNode` | A `<form>` that runs an action on submit (no reload) and revalidates loader data on success. See [Fetching data](./data-fetching.md). |\n| `Toil.Slot` | `Slot(props: SlotProps): ReactNode` | Renders the parallel-route slot named `props.name` (`{ name: string; fallback?: ReactNode }`) for the current URL. See [Routing](./routing.md). |\n| `Toil.Head` | `Head(props: HeadSpec): null` | Declarative form of `useHead`: `<Toil.Head title=\"...\" meta={[...]} />`. Renders nothing. See [Metadata and SEO](./metadata.md). |\n| `Toil.Metadata` | `Metadata(props: Metadata): null` | Declarative form of `useMetadata`: `<Toil.Metadata title=\"...\" openGraph={...} />`. Renders nothing. See [Metadata and SEO](./metadata.md). |\n| `Toil.Router` | `Router(props: { routes; layout?; notFound?; globalError?; slots? }): ReactNode` | The app router element. `Toil.mount` renders this for you, so you rarely use it directly. |\n\n## SSR marker primitives\n\nThese mark a route's dynamic bits so the edge can server-render it (the compiler's template extractor finds them deterministically). They are transparent at runtime: in the browser they render exactly the normal tree. See [Components](./components.md) and [Rendering and SSR](./rendering.md).\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `Toil.Hole` | `Hole(props: HoleProps): ReactNode` | A scalar text hole (`{ id: string; children?: ReactNode }`); renders `children` in the browser, a text insertion point under the SSR extractor. |\n| `Toil.Repeat` | `Repeat<T>(props: RepeatProps<T>): ReactNode` | A repeat region (`{ id; each: readonly T[]; children: (item: T, index: number) => ReactNode }`); `each.map(children)` in the browser. |\n| `Toil.RawHtml` | `RawHtml(props: RawHtmlProps): ReactNode` | A raw-HTML block hole (`{ id; html; as? }`); wraps `dangerouslySetInnerHTML` in a host element (default `div`). |\n| `Toil.attr` | `attr(id: string, value: string): string` | An attribute-value hole, used in attribute position (`href={Toil.attr('u', d.url)}`); returns `value` unchanged in the browser. |\n| `Toil.Island` | `Island(props: IslandProps): ReactNode` | A client-only escape hatch (`{ children? }`); renders nothing on the server and first paint, then reveals `children` after mount (so no SSR / SEO, by design). |\n\n## Navigation\n\nClient-side links and imperative navigation. Detailed guide: [Navigation](./navigation.md).\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `Toil.Link` | `Link(props: LinkProps): ReactNode` | A client-side navigation `<a>`: no full reload, prefetches on hover/focus, falls through to the browser for external / `target` / `download` / `#hash` links. |\n| `Toil.NavLink` | `NavLink(props: NavLinkProps): ReactNode` | A `Link` that adds an active class (default `\"active\"`) and `aria-current=\"page\"` when it points at the current page. |\n| `Toil.matchActive` | `matchActive(linkPath: string, currentPath: string, end: boolean): boolean` | Whether a link to `linkPath` is active for `currentPath` (the pure rule behind `NavLink`). |\n| `Toil.navigate` | `navigate(href: Href, options?: NavigateOptions): void` | Navigate in code (no hook needed), pushing history or replacing with `{ replace: true }`. |\n| `Toil.back` | `back(): void` | Go back one history entry. |\n| `Toil.forward` | `forward(): void` | Go forward one history entry. |\n| `Toil.refresh` | `refresh(): void` | Re-render the current route and re-run its loader (its `loading.tsx` shows while it re-fetches). |\n| `Toil.href` | `href(path: string): Href` | Assert a runtime-built string is a valid `Href`. Escape hatch for a path assembled from data. |\n| `Toil.prefetch` | `prefetch(href: string): void` | Warm a route's chunk ahead of navigation; a no-op for external, unknown, or already-warmed targets. |\n| `Toil.setViewTransitions` | `setViewTransitions(enabled: boolean): void` | Enable animated View Transitions for navigation (normally set once from `client.viewTransitions`). |\n| `Toil.setTransitions` | `setTransitions(enabled: boolean): void` | Keep the current page visible until the next route is ready (normally set once from `client.transitions`). |\n\n## Routing and location hooks\n\nHooks a component uses to read where it is and to get a router handle.\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `Toil.useParams` | `useParams<T extends RouteParams = RouteParams>(): T` | Read the dynamic route params, e.g. `{ id }` for `/blog/[id]`. Values are always strings. |\n| `Toil.useNavigate` | `useNavigate(): (href: Href, options?: NavigateOptions) => void` | Returns the bare `navigate` function. |\n| `Toil.useRouter` | `useRouter(): RouterInstance` | Returns the router handle (`push` / `replace` / `back` / `forward` / `refresh` / `revalidate` / `prefetch`). |\n| `Toil.useLocation` | `useLocation(): string` | The current pathname, re-read on each navigation (an alias of `usePathname`). |\n| `Toil.usePathname` | `usePathname(): string` | The current pathname, e.g. `\"/blog/42\"`. |\n| `Toil.useSearchParams` | `useSearchParams(): URLSearchParams` | The query string as a `URLSearchParams`, re-read on each navigation. |\n| `Toil.useNavigationPending` | `useNavigationPending(): boolean` | `true` while a navigation is in flight (drives a top loading bar). |\n| `Toil.matchRoute` | `matchRoute(pattern: string, pathname: string): RouteParams \\| null` | Pure route matcher: extract params, or `null` if the pattern does not match. |\n\n## Route data and mutations\n\nLoaders read data on navigation, actions write it. See [Fetching data](./data-fetching.md).\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `Toil.useLoaderData` | `useLoaderData<L extends LoaderFunction>(loader: L): Awaited<ReturnType<L>>` | Read the data the current route's `loader` returned. Passing `loader` infers the type; the no-arg `useLoaderData<T>()` returns `unknown` unless you supply `T`. |\n| `Toil.revalidate` | `revalidate(href?: string): void` | Invalidate loader data and re-render so the active route (or a given href) re-fetches. Call after a mutation; usable outside React. |\n| `Toil.invalidateLoaderData` | `invalidateLoaderData(href?: string): void` | Drop cached loader data (all routes, or one href) without re-rendering. |\n| `Toil.useAction` | `useAction<TInput = void, TData = unknown>(fn: (input: TInput) => TData \\| Promise<TData>, options?: UseActionOptions<TData>): ActionHandle<TInput, TData>` | Run a mutation with pending / error / result tracking; revalidates loader data on success. |\n\n## Head and metadata\n\nSet the document `<head>` from a component or resolve a route's metadata. See [Metadata and SEO](./metadata.md).\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `Toil.useHead` | `useHead(spec: HeadSpec): void` | Apply a title / `<meta>` / `<link>` contribution for the component's lifetime (reverts on unmount, composes across the tree). |\n| `Toil.useTitle` | `useTitle(title: string): void` | Set `document.title` for the component's lifetime. |\n| `Toil.mergeHead` | `mergeHead(specs: readonly HeadSpec[]): ResolvedHead` | Merge head specs in order: the last `title` wins, `meta` dedupes by name/property, `link` by rel+href. |\n| `Toil.useMetadata` | `useMetadata(metadata: Metadata): void` | Apply a route-style `Metadata` object from inside any component (the runtime counterpart of a route's `metadata` export). |\n| `Toil.resolveMetadata` | `resolveMetadata(metadata: Metadata): HeadSpec` | Expand a `Metadata` object into a title plus concrete `<meta>` / `<link>` tags. |\n\n## Page search\n\nQuery the statically-baked index of your pages' metadata. See [Search](./search.md).\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `Toil.searchPages` | `searchPages(query: string, options?: PageSearchOptions): PageSearchResult[]` | Rank the registered page index against a query (pure, framework-agnostic; AND semantics across terms). |\n| `Toil.usePageSearch` | `usePageSearch(query: string, options?: PageSearchOptions): PageSearch` | React binding for `searchPages`, memoized, with a `goTo` helper that navigates to a match. |\n| `Toil.registerPages` | `registerPages(pages: readonly PageMeta[]): void` | Replace the live page index. Called once at startup by the generated bundle; rarely called by user code. |\n| `Toil.getPages` | `getPages(): readonly PageMeta[]` | The registered page index (every page, including dynamic ones). Empty before registration. |\n| `Toil.pagePath` | `pagePath(target: string \\| PageMeta \\| PageSearchResult): string` | Normalize a result / page / raw path to its route path string. |\n\n## Realtime\n\nOpen a channel or a typed stream to the backend. See [Channels](../realtime/channels.md) and [Streams](../realtime/streams.md).\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `Toil.connectChannel` | `connectChannel(onMessage: (data: ChannelData) => void, options?: ChannelOptions): Channel` | Open a WebSocket channel to the backend, invoking `onMessage` per frame; returns `send` / `close`. |\n| `Toil.useChannel` | `useChannel(options?: ChannelOptions): ChannelHook` | React hook over `connectChannel`: connects on mount, tracks `connected` + `messages`, exposes `send`. |\n| `Toil.resolveChannelUrl` | `resolveChannelUrl(path?: string, location?: { protocol: string; host: string }): string` | Derive the channel's `ws(s)://` URL from the current page location. |\n| `Toil.makeStreamClient` | `makeStreamClient(routes: Record<string, string>, origin?: string, encoders?: Record<string, (msg: never) => Uint8Array>): StreamClient` | Build the `Server.Stream` client from the generated route map. Used by generated code; rarely called directly. |\n\n## Backend client, errors, bootstrap\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `Toil.Server` | `Server.REST.<controller>.<route>(args)` / `Server.Stream.<class>.connect(path?)` / `Server.<service>.<method>(args)` | The typed backend surface. Its shape is generated into `shared/server.ts`; this global is the runtime behind it. See [Fetching data](./data-fetching.md). |\n| `Toil.parseError` | `parseError(err: unknown): string` | Extract a human-readable message from an unknown thrown value: `Error.message`, else `String(err)`. |\n| `Toil.mount` | `mount(routes: RouteDef[], layout?: LayoutLoader, notFound?: NotFoundLoader, globalError?: ErrorComponentLoader, slots?: Record<string, RouteDef[]>): void` | Boot the app into `#root` and start idle link prefetching. Called by the generated entry file; you rarely call it yourself. |\n\n## Auth\n\nThe whole password-auth client lives under `Toil.Auth`, and its error family is exposed on `Toil.*` (and mirrored under `Toil.Auth.*`) so you can branch on it. The password never leaves the browser. See [Auth](../auth/README.md).\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `Toil.Auth.register` | `register(username: string, password: string, email: string, opts?: AuthOptions): Promise<void>` | Create an account (only a derived public key is ever sent). Throws `UsernameTakenError` / `EmailInUseError` on conflict. |\n| `Toil.Auth.login` | `login(username: string, password: string, opts?: AuthOptions): Promise<Uint8Array>` | Log in with mutual authentication; resolves to the opaque session token. |\n| `Toil.Auth.confirmEmail` | `confirmEmail(token: string, opts?: AuthOptions): Promise<void>` | Confirm an account from the one-time token in the emailed link. |\n| `Toil.Auth.resendConfirmation` | `resendConfirmation(email: string, opts?: AuthOptions): Promise<void>` | Ask the server to re-send the confirmation email (never reveals whether the address exists). |\n| `Toil.Auth.requestPasswordReset` | `requestPasswordReset(email: string, opts?: AuthOptions): Promise<void>` | Begin a password reset by emailing a link (anti-enumeration: always resolves). |\n| `Toil.Auth.resetPassword` | `resetPassword(token: string, newPassword: string, opts?: AuthOptions): Promise<void>` | Complete a reset from the token plus a new password. |\n| `Toil.Auth.verifyTwoFactor` | `verifyTwoFactor(twoFaId: string, code: string, opts?: AuthOptions): Promise<Uint8Array>` | Finish a 2FA login by submitting the code for `twoFaId`; resolves to the session token. |\n| `Toil.Auth.setupTwoFactor` | `setupTwoFactor(method: number, opts?: AuthOptions): Promise<void>` | Begin enabling or disabling 2FA for the current session user (pass a `TwoFactorMethod` value). |\n| `Toil.Auth.confirmTwoFactorSetup` | `confirmTwoFactorSetup(code: string, opts?: AuthOptions): Promise<void>` | Confirm a pending `setupTwoFactor` with the delivered code. |\n| `Toil.Auth.twoFactorStatus` | `twoFactorStatus(opts?: AuthOptions): Promise<number>` | The current user's 2FA method (a `TwoFactorMethod` value; `0` means off). |\n| `Toil.Auth.TwoFactorMethod` | `{ None: 0, Email: 1 }` | The 2FA method values, mirroring the server enum. |\n\nThe typed error surface (each error carries a stable `code`, and every subclass maps 1:1 to an `AuthErrorCode`):\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `Toil.AuthError` | `class AuthError extends Error { readonly code: AuthErrorCode }` | Base class for every auth error. `err instanceof Toil.AuthError` narrows the whole family; `err.code` discriminates within it. |\n| `Toil.AuthErrorCode` | `enum AuthErrorCode` (string values) | The stable, machine-readable discriminant carried as `err.code`. Branch on this, never on `err.message` or `err.name`. |\n| `Toil.UsernameTakenError` | `class UsernameTakenError extends AuthError` | register: the username is already registered (`AuthErrorCode.UsernameTaken`). |\n| `Toil.EmailInUseError` | `class EmailInUseError extends AuthError` | register: the email is already in use (`AuthErrorCode.EmailInUse`). |\n| `Toil.InvalidCredentialsError` | `class InvalidCredentialsError extends AuthError` | login: wrong username or password (`AuthErrorCode.InvalidCredentials`). |\n| `Toil.EmailNotConfirmedError` | `class EmailNotConfirmedError extends AuthError` | login: valid credential, but the email is not confirmed (`AuthErrorCode.EmailNotConfirmed`). |\n| `Toil.TwoFactorRequiredError` | `class TwoFactorRequiredError extends AuthError { readonly twoFaId: string }` | login: a second factor is required; echo `err.twoFaId` to `verifyTwoFactor` (`AuthErrorCode.TwoFactorRequired`). |\n| `Toil.ServerAuthFailedError` | `class ServerAuthFailedError extends AuthError` | login/2fa: the server failed to prove its identity, possible MITM (`AuthErrorCode.ServerAuthFailed`). |\n| `Toil.TwoFactorCodeError` | `class TwoFactorCodeError extends AuthError` | 2fa: the code was wrong, expired, or already used (`AuthErrorCode.TwoFactorCodeInvalid`). |\n| `Toil.ConfirmationInvalidError` | `class ConfirmationInvalidError extends AuthError` | confirm: the confirmation link was invalid or expired (`AuthErrorCode.ConfirmationInvalid`). |\n| `Toil.PasswordResetInvalidError` | `class PasswordResetInvalidError extends AuthError` | reset: the reset link was invalid or expired (`AuthErrorCode.PasswordResetInvalid`). |\n\nThe `AuthErrorCode` string values:\n\n```ts\nenum AuthErrorCode {\n RequestFailed = 'request_failed',\n ProtocolError = 'protocol_error',\n UsernameTaken = 'username_taken',\n EmailInUse = 'email_in_use',\n RegistrationRejected = 'registration_rejected',\n InvalidCredentials = 'invalid_credentials',\n EmailNotConfirmed = 'email_not_confirmed',\n TwoFactorRequired = 'two_factor_required',\n ServerAuthFailed = 'server_auth_failed',\n TwoFactorCodeInvalid = 'two_factor_code_invalid',\n TwoFactorSetupFailed = 'two_factor_setup_failed',\n ConfirmationInvalid = 'confirmation_invalid',\n PasswordResetInvalid = 'password_reset_invalid',\n}\n```\n\nCatch by class for the common branches, or on `err.code` for a precise check:\n\n```tsx\ntry {\n const token = await Toil.Auth.login(username, password);\n // ... persist the session token and redirect\n} catch (err) {\n if (err instanceof Toil.EmailNotConfirmedError) return promptEmailConfirmation();\n if (err instanceof Toil.TwoFactorRequiredError) return promptTwoFactorCode(err.twoFaId);\n if (err instanceof Toil.AuthError && err.code === Toil.AuthErrorCode.InvalidCredentials) {\n return setError('Incorrect username or password.');\n }\n throw err;\n}\n```\n\n## A note on types\n\nEvery member above is a value, and values on `Toil.*` need no import: `Toil.Link`, `Toil.useParams`, `Toil.Auth.login`. Types are different. In type position, only a small set are namespaced as `Toil.<Type>`, because the generated `toil-env.d.ts` aliases exactly these under a `declare namespace Toil` block:\n\n`LoaderArgs`, `LoaderFunction`, `Revalidate`, `Metadata`, `GenerateMetadata`, `GenerateStaticParams`, `StaticParams`, `RouteErrorProps`, `Href`, `RoutePath`, `PageMeta`, `SearchHints`.\n\nSo a loader can annotate its argument with no import:\n\n```tsx\nexport const loader = async ({ params }: Toil.LoaderArgs) => { /* ... */ };\n```\n\nEvery other exported type (for example `LinkProps`, `NavLinkState`, `NavigateOptions`, `RouterInstance`, `ImageProps`, `ScriptProps`, `FormProps`, `ActionState`) is not in that namespace, so it must be imported from the package:\n\n```tsx\nimport type { ImageProps } from 'toiljs/client';\n```\n\n## Related\n\n- [Navigation](./navigation.md): links, active state, and navigating in code.\n- [Components](./components.md): the built-in components and the SSR marker primitives.\n- [Fetching data](./data-fetching.md): the `Server` client, loaders, actions, and forms.\n- [Metadata and SEO](./metadata.md): titles, descriptions, and social-share tags per route.\n- [Auth](../auth/README.md): the full password-auth client and its error handling.\n",
44
- "getting-started/create-project.md": "# Create a project\n\nScaffold a brand-new toiljs app with one command. The CLI asks a few questions, writes the files, and (optionally) installs dependencies for you.\n\n## Why and when\n\n`toiljs create` is how every project starts. It wires up the enforced toiljs presets (TypeScript, ESLint, and Prettier config), the file-based routing, and a working client and server, so you get a project that builds and runs on the first try. Use it for a new app. To bring an **existing** React app into toiljs instead, see [Migrating](./migrating.md).\n\n## The command\n\n```sh\ntoiljs create my-app\n```\n\nReplace `my-app` with your project name (it becomes the folder name). If you leave the name off, the wizard asks for one.\n\nBy default this runs an interactive wizard. To skip every question and accept the defaults (handy for scripts and CI), add `--yes`:\n\n```sh\ntoiljs create my-app --yes\n```\n\n## What the wizard asks\n\nRunning `toiljs create` walks you through these prompts. Each one has a flag you can pass instead, so you can answer some or all of them up front.\n\n| Prompt | What it decides | Flag | Default |\n| --- | --- | --- | --- |\n| Project name | The folder and package name | (the first argument) | `my-toil-app` |\n| Which template? | How much starter code you get | `-t, --template <app\\|minimal>` | `app` |\n| Styling | The CSS flavor for `client/` | `--style <css\\|sass\\|less\\|stylus>` | `css` |\n| Add Tailwind CSS? | Adds Tailwind on top of the styling | `--tailwind` / `--no-tailwind` | off |\n| AI assistant files | Editor hint files for Claude, Cursor, Codex, Copilot | `--no-ai` (to skip) | all |\n| Optimize images at build time? | Resize and compress imported images | `--images` / `--no-images` | on |\n| Initialize a git repository? | Runs `git init` and stages the files | `--git` / `--no-git` | on |\n| Install dependencies now? | Runs your package manager's install | `--install` / `--no-install` | on |\n\nTwo more flags do not have a prompt:\n\n- `--pm <npm\\|pnpm\\|yarn\\|bun>` picks the package manager to install with (default `npm`).\n- `-y, --yes` accepts every default and runs without any prompts.\n\n### The two templates\n\n- **`app`** (default) is the full starter: a landing page, a shared layout, styles, and a set of demo routes that show off HTTP routes, typed RPC, cookies, auth, and a ToilDB-backed guestbook. Great for learning by reading real, working code.\n- **`minimal`** is the bare minimum: a layout, a single home page, and a tiny server handler with one example endpoint. Great when you want a clean slate.\n\nA fully non-interactive example:\n\n```sh\ntoiljs create my-app --yes --template minimal --style css --no-tailwind --pm pnpm\n```\n\n## What gets scaffolded\n\nHere is the shape of a new **`app`** project. The exact set of demo routes may grow over time, so this is trimmed for readability.\n\n```text\nmy-app/\n package.json scripts + dependencies (toiljs, react, toilscript, ...)\n toil.config.ts client/build config (SEO, images, page transitions)\n toilconfig.json server (wasm) build config for toilscript\n tsconfig.json TypeScript config for the client\n eslint.config.js linting preset\n .prettierrc formatting preset\n .gitignore ignores build output, generated files, and .env files\n toil-env.d.ts generated editor types for client globals (Toil.*)\n toil-routes.d.ts generated typed-route names (filled in on first build)\n README.md\n CLAUDE.md / AGENTS.md AI assistant hint files (if you kept them)\n\n client/ your React app (runs in the browser)\n toil.tsx the client entry: mounts routes + layout\n layout.tsx the root layout that wraps every page\n 404.tsx the not-found page\n global-error.tsx the top-level error page\n routes/ file-based pages (index.tsx = \"/\", about.tsx = \"/about\", ...)\n components/ shared React components\n styles/main.css global styles\n public/ static files served as-is (favicon, robots.txt, images)\n\n server/ your backend (compiled to wasm, runs on the edge)\n main.ts the entry: wires the handler + imports your surface modules\n tsconfig.json server-only TS config (loads the toilscript editor plugin)\n toil-server-env.d.ts generated editor types for server globals (Cookie, crypto, ...)\n core/ your top-level request handler and shared logic\n models/ @data classes (the typed wire types)\n routes/ @rest controllers (HTTP endpoints)\n services/ @service classes and @remote functions (typed RPC)\n migrations/ ToilDB schema migrations (README explains the convention)\n scheduled/ reserved for scheduled tasks\n\n shared/ (created by the build)\n server.ts GENERATED typed client: the Server proxy + @data codecs\n```\n\nThe **`minimal`** template is the same layout with far fewer files: `client/` has just `layout.tsx`, `routes/index.tsx`, and `styles/main.css`; `server/` has `main.ts` and `core/AppHandler.ts` with a single example endpoint.\n\nA few files are worth calling out now, and the next page ([Project structure](./project-structure.md)) walks through all of them:\n\n- **`shared/server.ts` does not exist yet** in a fresh project. It is generated the first time you run `toiljs dev` or `toiljs build`. That is normal and expected.\n- **`toil-routes.d.ts`** starts as a stub and gets filled in with your real route names on the first build, which is what makes `Toil.Link` route names type-check.\n- **`.env` and `.env.secrets` are not created** for you. You add them yourself when you need local environment variables or secrets. They are already listed in `.gitignore` so you never commit them. See [Environment and secrets](../services/environment.md).\n\n## Run it\n\nOnce scaffolding (and install) finishes, the CLI prints your next steps:\n\n```sh\ncd my-app\nnpm run dev\n```\n\n`npm run dev` runs `toiljs dev`, which builds your server to wasm, generates `shared/server.ts`, and starts the dev server with hot reload. Open the printed URL (by default `http://localhost:3000`) and you have a live app.\n\nIf you told the wizard **not** to install dependencies, run `npm install` first.\n\n## Gotchas and notes\n\n- **Scaffolding into a non-empty folder** asks for confirmation in interactive mode, and fails in `--yes` mode. Create into a fresh, empty directory.\n- **The project name must be a valid package name** and must stay inside the current directory (no `..`, no absolute paths).\n- **Git init is best-effort.** If `git` is not installed, the CLI skips that step and keeps going.\n- **You do not run `toilscript` yourself.** It is added as a dependency and driven by `toiljs dev` / `toiljs build`.\n\n## Related\n\n- [Project structure](./project-structure.md)\n- [Your first app](./first-app.md)\n- [The CLI reference](../cli/README.md)\n- [Configuration](../concepts/config.md)\n- [Styling](../frontend/styling.md)\n",
44
+ "getting-started/create-project.md": "# Create a project\n\nScaffold a brand-new toiljs app with one command. The CLI asks a few questions, writes the files, and (optionally) installs dependencies for you.\n\n## Why and when\n\n`toiljs create` is how every project starts. It wires up the enforced toiljs presets (TypeScript 7, Oxlint + tsgolint, and Prettier config), the file-based routing, and a working client and server, so you get a project that builds and runs on the first try. Use it for a new app. To bring an **existing** React app into toiljs instead, see [Migrating](./migrating.md).\n\n## The command\n\n```sh\ntoiljs create my-app\n```\n\nReplace `my-app` with your project name (it becomes the folder name). If you leave the name off, the wizard asks for one.\n\nBy default this runs an interactive wizard. To skip every question and accept the defaults (handy for scripts and CI), add `--yes`:\n\n```sh\ntoiljs create my-app --yes\n```\n\n## What the wizard asks\n\nRunning `toiljs create` walks you through these prompts. Each one has a flag you can pass instead, so you can answer some or all of them up front.\n\n| Prompt | What it decides | Flag | Default |\n| --- | --- | --- | --- |\n| Project name | The folder and package name | (the first argument) | `my-toil-app` |\n| Which template? | How much starter code you get | `-t, --template <app\\|minimal>` | `app` |\n| Styling | The CSS flavor for `client/` | `--style <css\\|sass\\|less\\|stylus>` | `css` |\n| Add Tailwind CSS? | Adds Tailwind on top of the styling | `--tailwind` / `--no-tailwind` | off |\n| AI assistant files | Editor hint files for Claude, Cursor, Codex, Copilot | `--no-ai` (to skip) | all |\n| Optimize images at build time? | Resize and compress imported images | `--images` / `--no-images` | on |\n| Initialize a git repository? | Runs `git init` and stages the files | `--git` / `--no-git` | on |\n| Install dependencies now? | Runs your package manager's install | `--install` / `--no-install` | on |\n\nTwo more flags do not have a prompt:\n\n- `--pm <npm\\|pnpm\\|yarn\\|bun>` picks the package manager to install with (default `npm`).\n- `-y, --yes` accepts every default and runs without any prompts.\n\n### The two templates\n\n- **`app`** (default) is the full starter: a landing page, a shared layout, styles, and a set of demo routes that show off HTTP routes, typed RPC, cookies, auth, and a ToilDB-backed guestbook. Great for learning by reading real, working code.\n- **`minimal`** is the bare minimum: a layout, a single home page, and a tiny server handler with one example endpoint. Great when you want a clean slate.\n\nA fully non-interactive example:\n\n```sh\ntoiljs create my-app --yes --template minimal --style css --no-tailwind --pm pnpm\n```\n\n## What gets scaffolded\n\nHere is the shape of a new **`app`** project. The exact set of demo routes may grow over time, so this is trimmed for readability.\n\n```text\nmy-app/\n package.json scripts + dependencies (toiljs, react, toilscript, ...)\n toil.config.ts client/build config (SEO, images, page transitions)\n toilconfig.json server (wasm) build config for toilscript\n tsconfig.json TypeScript config for the client\n oxlint.config.ts linting preset\n .prettierrc formatting preset\n .gitignore ignores build output, generated files, and .env files\n toil-env.d.ts generated editor types for client globals (Toil.*)\n toil-routes.d.ts generated typed-route names (filled in on first build)\n README.md\n CLAUDE.md / AGENTS.md AI assistant hint files (if you kept them)\n\n client/ your React app (runs in the browser)\n toil.tsx the client entry: mounts routes + layout\n layout.tsx the root layout that wraps every page\n 404.tsx the not-found page\n global-error.tsx the top-level error page\n routes/ file-based pages (index.tsx = \"/\", about.tsx = \"/about\", ...)\n components/ shared React components\n styles/main.css global styles\n public/ static files served as-is (favicon, robots.txt, images)\n\n server/ your backend (compiled to wasm, runs on the edge)\n main.ts the entry: wires the handler + imports your surface modules\n tsconfig.json server-only TS config (dialect checked by toilscript builds)\n toil-server-env.d.ts generated editor types for server globals (Cookie, crypto, ...)\n core/ your top-level request handler and shared logic\n models/ @data classes (the typed wire types)\n routes/ @rest controllers (HTTP endpoints)\n services/ @service classes and @remote functions (typed RPC)\n migrations/ ToilDB schema migrations (README explains the convention)\n scheduled/ reserved for scheduled tasks\n\n shared/ (created by the build)\n server.ts GENERATED typed client: the Server proxy + @data codecs\n```\n\nThe **`minimal`** template is the same layout with far fewer files: `client/` has just `layout.tsx`, `routes/index.tsx`, and `styles/main.css`; `server/` has `main.ts` and `core/AppHandler.ts` with a single example endpoint.\n\nA few files are worth calling out now, and the next page ([Project structure](./project-structure.md)) walks through all of them:\n\n- **`shared/server.ts` does not exist yet** in a fresh project. It is generated the first time you run `toiljs dev` or `toiljs build`. That is normal and expected.\n- **`toil-routes.d.ts`** starts as a stub and gets filled in with your real route names on the first build, which is what makes `Toil.Link` route names type-check.\n- **`.env` and `.env.secrets` are not created** for you. You add them yourself when you need local environment variables or secrets. They are already listed in `.gitignore` so you never commit them. See [Environment and secrets](../services/environment.md).\n\n## Run it\n\nOnce scaffolding (and install) finishes, the CLI prints your next steps:\n\n```sh\ncd my-app\nnpm run dev\n```\n\n`npm run dev` runs `toiljs dev`, which builds your server to wasm, generates `shared/server.ts`, and starts the dev server with hot reload. Open the printed URL (by default `http://localhost:3000`) and you have a live app.\n\nIf you told the wizard **not** to install dependencies, run `npm install` first.\n\nInstall the **Oxc** plugin for VS Code or WebStorm using the [editor setup instructions](./installation.md#editor-setup).\nThe generated README includes the steps. WebStorm requires **Enable type aware rules** to be\nchecked under **Settings → Tools → Oxlint**, even though the project preset enables them.\n\n## Gotchas and notes\n\n- **Scaffolding into a non-empty folder** asks for confirmation in interactive mode, and fails in `--yes` mode. Create into a fresh, empty directory.\n- **The project name must be a valid package name** and must stay inside the current directory (no `..`, no absolute paths).\n- **Git init is best-effort.** If `git` is not installed, the CLI skips that step and keeps going.\n- **You do not run `toilscript` yourself.** It is added as a dependency and driven by `toiljs dev` / `toiljs build`.\n\n## Related\n\n- [Project structure](./project-structure.md)\n- [Your first app](./first-app.md)\n- [The CLI reference](../cli/README.md)\n- [Configuration](../concepts/config.md)\n- [Styling](../frontend/styling.md)\n",
45
45
  "getting-started/deploy.md": "# Deploying a toiljs app\n\nThis page is an honest look at how you get a toiljs app in front of real users. There are two paths, and it helps to be clear about which one exists today.\n\n- **Self-host it yourself.** You build the app and run it on a machine you control, with `toiljs build` then `toiljs start`. This works right now, on your laptop, a VPS, or any server that can run Node.js.\n- **Run it on the managed Dacely edge.** This is the platform toiljs is built for: a worldwide fleet of servers that runs your compiled backend close to every user. It is the target the whole framework is designed around.\n\nOne thing up front, so you do not go looking for it: **there is no `toiljs deploy` command.** The CLI can build and self-host; pushing a build onto the managed edge is a platform step, not a CLI subcommand. The rest of this page covers what you can do today (self-hosting) and what the managed edge gives you.\n\n## The one build powers both\n\nBoth paths serve the exact same artifacts. `toiljs build` produces them once:\n\n```mermaid\nflowchart LR\n A[\"Your project\"] -->|toiljs build| B[\"build/client/<br/>(HTML, JS, CSS, assets)\"]\n A -->|toiljs build| C[\"build/server/release.wasm<br/>(your backend)\"]\n B --> H[\"toiljs start<br/>(self-host)\"]\n C --> H\n B --> E[\"managed Dacely edge<br/>(worldwide)\"]\n C --> E\n```\n\nSo you never build differently for the two targets. You build once, then either run it yourself or hand the same output to the edge.\n\n## Self-hosting with `toiljs start`\n\n`toiljs start` runs your built app on a fast production HTTP server. It serves your static client, dispatches dynamic requests into your `release.wasm`, does server-side rendering, runs daemons, and exposes a `/_toil` websocket channel. Use it to put your app on your own machine or server instead of the managed edge.\n\n### Step 1: build\n\n`start` serves whatever is in `build/`, so you must build first. If there is no build, `start` exits with an error (it looks for `build/client/index.html`).\n\n```bash\nnpm run build\n```\n\n### Step 2: start\n\n```bash\n# Serve on http://127.0.0.1:3000 (loopback only, one worker per CPU).\nnpx toiljs start\n\n# Accept outside connections, on port 8080, with 4 worker processes.\nnpx toiljs start --host 0.0.0.0 --port 8080 --threads 4\n```\n\n### start flags\n\n| Flag | Meaning |\n| --- | --- |\n| `--port <n>` | Port to listen on. Default `3000` (or `client.port` from your config). |\n| `--host <host>` | Address to bind. Default `127.0.0.1`, which is **loopback only** (reachable just from the same machine). Pass `0.0.0.0` to accept connections from other machines. |\n| `--threads <n>` | Number of HTTP worker processes. Default is automatic (one per available CPU). Pass `1` to disable the worker pool. `--workers` is an accepted alias, and you can also set `server.threads` in your config or the `TOILJS_THREADS` environment variable. |\n| `--root <dir>` | Run against a project in another directory instead of the current one. |\n\nThese flags are verified against the CLI. Note that `--host` and `--threads` are `start`-only: `toiljs dev` always binds locally and does not take them. For the full command reference, see [the CLI](../cli/README.md#toiljs-start).\n\n### A realistic self-host\n\nOn a small server (say a VPS), a minimal run looks like this:\n\n```bash\n# Install and build.\nnpm ci\nnpm run build\n\n# Bind on all interfaces so a reverse proxy or the public can reach it.\nnpx toiljs start --host 0.0.0.0 --port 8080 --threads 4\n```\n\nThen put a reverse proxy (nginx, Caddy) in front for TLS and a real hostname, pointing it at `http://127.0.0.1:8080`. Keep the process alive with your usual tool (systemd, pm2, a container restart policy).\n\n### Self-host gotchas\n\n- **`start` needs a fresh build.** It serves `build/`, not your source. After any code change, run `toiljs build` again before restarting.\n- **The default bind is loopback.** With no `--host`, only the same machine can reach it. Set `--host 0.0.0.0` (usually behind a reverse proxy) to serve real traffic.\n- **Self-host is one place, not the whole world.** You get one server (or however many you run yourself). The latency and worldwide reach of the managed edge is exactly the thing self-hosting does not give you.\n- **The database is different.** Self-hosting runs the built server, but ToilDB's worldwide data layer is an edge feature. Treat self-host as a way to run and test your build, not as a substitute for the managed edge's global database.\n\n## The managed Dacely edge\n\nThe managed **Dacely edge** is the platform toiljs targets: a fleet of servers in many cities that runs your compiled backend as close to each user as possible, backed by the worldwide **ToilDB** database. You write one project, and the build already decides which part of your code belongs to which layer of the edge.\n\nYour backend runs across four **compute tiers**, from a per-request handler at the very edge up to a single worldwide coordinator:\n\n| Tier | Name | Where it runs | Typical code |\n| --- | --- | --- | --- |\n| **L1** | Hot / edge | The node nearest the user, fresh per request | `@rest`, `@service` / `@remote` |\n| **L2** | Regional | One box per connection, per region | `@stream` (Regional) |\n| **L3** | Continental | One box per connection, per continent | `@stream` (Continental) |\n| **L4** | Global / daemon | Exactly one leader worldwide | `@daemon`, `@scheduled` |\n\nBecause the same `toiljs build` output feeds the edge, everything you built and tested with `toiljs dev` and `toiljs start` is what runs there. The edge decides each server's role for you; you do not configure tiers by hand. For the full picture of what each tier means and how to write for it, see [Compute tiers (L1 to L4)](../concepts/tiers.md).\n\n## Related\n\n- [The CLI](../cli/README.md): the full reference for `build` and `start` and every flag.\n- [Configuration (`toil.config.ts`)](../concepts/config.md): `server.threads` and the other self-host knobs.\n- [Compute tiers (L1 to L4)](../concepts/tiers.md): how your code maps onto the managed edge.\n- [Getting started](./README.md): the path from install to a running feature.\n",
46
46
  "getting-started/first-app.md": "# Your first app\n\nBuild a tiny feature end to end: a page with a \"Like\" button that reads and writes a real number in ToilDB, so the count survives page reloads and restarts. Along the way you meet the typed client, an HTTP route, and the database, the three pieces you use in almost every toiljs app.\n\n## What you will build\n\nA `/likes` page that shows a running like count and a button to add one. The count lives in ToilDB, so it is shared by everyone and it persists.\n\n```mermaid\nsequenceDiagram\n participant B as Browser (/likes page)\n participant SH as shared/server.ts (typed client)\n participant W as server.wasm (@rest route)\n participant DB as ToilDB (counter)\n B->>SH: Server.REST.likes.add()\n SH->>W: POST /likes\n W->>DB: counter.add(key, 1)\n W-->>SH: { count: 43 }\n SH-->>B: typed LikeCount\n B->>B: show \"43 likes\"\n```\n\n## Before you start\n\nYou need a project. If you do not have one yet, create one and start the dev server:\n\n```sh\ntoiljs create my-app\ncd my-app\nnpm run dev\n```\n\nLeave `npm run dev` running in a terminal. It rebuilds your server, regenerates the typed client, and hot-reloads the browser every time you save. The app is at `http://localhost:3000`.\n\n## Why the database (and not a variable)\n\nYour first instinct might be to keep the count in a normal variable on the server. That will not work, and it is worth understanding why now.\n\n**The server runs a fresh copy of your `.wasm` for every request, and wipes its memory when the request ends.** So a variable you set while handling one request is gone by the next request. Anything that must outlive a single request, a counter, a user, a post, has to go into a store. toiljs ships one: **ToilDB**, a database built into the edge.\n\nToilDB offers a few specialized shapes called **families**. For a running total, the right one is a **counter**: a value you can atomically add to and read back. (Others include documents, events, and views. See [Database overview](../database/README.md).)\n\n## Step 1: add a data type for the response\n\nYour server and client talk in typed messages called `@data` classes. A `@data` class can cross the wire and be parsed into a real typed object on the other side. Create one to hold the count.\n\nCreate `server/models/LikeCount.ts`:\n\n```ts\n// The response our route returns: just the current like count.\n@data\nexport class LikeCount {\n count: i64 = 0;\n constructor(count: i64 = 0) {\n this.count = count;\n }\n}\n```\n\nTwo things to notice:\n\n- `@data` is a decorator that marks this class as a wire type. You use it with no import (it is a compiler built-in). See [Data types](../backend/data.md).\n- The field type is `i64`, a 64-bit integer. The server uses precise integer types like `i64` and `u64`. On the client side these arrive as JavaScript `bigint`. More on this in [Types](../concepts/types.md).\n\n## Step 2: add the HTTP route\n\nNow the backend endpoint. Create `server/routes/Likes.ts`:\n\n```ts\nimport { RouteContext } from 'toiljs/server/runtime';\n\nimport { LikeCount } from '../models/LikeCount';\n\n// The KEY that names one counter. A counter is a map from a key to a number,\n// so a single fixed key (\"home\") gives us one global tally.\n@data\nclass LikeKey {\n page: string = 'home';\n constructor(page: string = 'home') {\n this.page = page;\n }\n}\n\n// A @database declares your ToilDB collections. Each @collection is one named\n// store. Here we declare a single counter keyed by LikeKey.\n@database\nclass LikesDb {\n @collection static likes: Counter<LikeKey>;\n}\n\n// A @rest controller exposes HTTP endpoints. 'likes' is the URL prefix, so the\n// routes below live under /likes.\n@rest('likes')\nclass Likes {\n // GET /likes -> read the current count.\n // A @get handler is a \"query\": it may read but not write. Reading one\n // counter by its key is a point read, which queries are allowed to do.\n @get('/')\n public show(): LikeCount {\n const key = new LikeKey('home');\n return new LikeCount(LikesDb.likes.get(key));\n }\n\n // POST /likes -> add one, then return the new count.\n // A @post handler is an \"action\": it may write. `add` bumps the counter.\n // (A body-less POST takes the RouteContext; we do not need it here.)\n @post('/')\n public add(_ctx: RouteContext): LikeCount {\n const key = new LikeKey('home');\n LikesDb.likes.add(key, 1);\n return new LikeCount(LikesDb.likes.get(key));\n }\n}\n```\n\nWhat each decorator does:\n\n- **`@rest('likes')`** turns the class into an HTTP controller mounted at `/likes`.\n- **`@get('/')`** and **`@post('/')`** map a method to a verb and path. A `@get` is a read-only **query**; a `@post` is a write-capable **action**. This split is how toiljs keeps expensive or unsafe operations out of read paths.\n- **`@database`** and **`@collection`** declare your ToilDB stores. `Counter<LikeKey>` is a counter you look up by a `LikeKey`.\n\nThe counter gives you two operations: `add(key, delta)` to change it and `get(key)` to read it. See [Counters](../database/counters.md) and [HTTP routes](../backend/rest.md) for the full API.\n\n## Step 3: register the route\n\nOpen `server/main.ts` and add an import for your new route, next to the others:\n\n```ts\nimport './routes/Likes';\n```\n\nThe build actually discovers decorated files under `server/` on its own, but importing them from `main.ts` is the convention: it keeps a direct `toilscript` build finding the same code. (Every `app` project already imports its demo routes this way.)\n\nSave. Watch the terminal: `toiljs dev` recompiles the server to wasm and regenerates `shared/server.ts`. A moment later your new endpoint exists, fully typed.\n\n## Step 4: add the client page\n\nNow the frontend. Create `client/routes/likes.tsx`. The file name is the URL, so this page is at `/likes`.\n\n```tsx\nimport { useEffect, useState } from 'react';\n\nexport default function LikesPage() {\n // The count is a bigint because the server field is i64.\n const [count, setCount] = useState(0n);\n\n // Read the current count once, when the page first loads.\n useEffect(() => {\n Server.REST.likes.show().then((res) => setCount(res.count));\n }, []);\n\n // Add a like, then update the UI with the fresh count the server returns.\n const like = async () => {\n const res = await Server.REST.likes.add();\n setCount(res.count);\n };\n\n return (\n <main>\n <h1>{String(count)} likes</h1>\n <button onClick={like}>Like</button>\n </main>\n );\n}\n```\n\nThe magic here is `Server.REST.likes`. You never wrote it. toiljs generated it into `shared/server.ts` from your `@rest` controller, so:\n\n- `Server.REST.likes.show()` returns a `Promise<LikeCount>`, and `res.count` is typed for you.\n- `Server.REST.likes.add()` sends the POST and returns the updated `LikeCount`.\n- `Server` is a global in client code, so you do not import it. Its types come from the generated file.\n\nIf you rename the route or change its return type on the server, this client code stops type-checking until you fix it. That is the whole point: the browser and the backend cannot silently disagree.\n\n## Step 5: try it\n\nOpen `http://localhost:3000/likes`. You should see \"0 likes\" and a button. Click **Like** a few times and the number climbs.\n\nNow the important test: **reload the page.** The count is still there. Stop the dev server, start it again, reload: still there. That is ToilDB persisting your counter, exactly as it would on the real edge. The same code that ran against the local dev database runs against the worldwide database in production, with no connection string to configure.\n\n## What just happened\n\n```mermaid\nflowchart LR\n M[\"server/models/LikeCount.ts<br/>@data\"] --> GEN\n R[\"server/routes/Likes.ts<br/>@rest + @database\"] --> GEN\n GEN[\"toiljs dev<br/>compiles + generates\"] --> WASM[\"release.wasm\"]\n GEN --> SS[\"shared/server.ts<br/>(typed Server.REST.likes)\"]\n SS --> P[\"client/routes/likes.tsx<br/>calls Server.REST.likes\"]\n```\n\nYou wrote three small files. toiljs compiled the server to WebAssembly, generated a typed client from your route, and your React page called it with full type safety. The like count lives in ToilDB, so it persists.\n\n## Gotchas and notes\n\n- **`res.count` is a `bigint`, not a `number`.** That is why the page uses `useState(0n)` and `String(count)`. Server integer types map to `bigint` on the client. See [Types](../concepts/types.md).\n- **A `@get` cannot write, and a `@post` can.** If you try to call `.add(...)` from a `@get`, the compiler rejects it. Reads that scan many rows are also blocked in handlers; a single-key `get` is fine.\n- **You do not edit `shared/server.ts`.** It is regenerated on every server build. Change the route, and the client updates itself.\n- **If the editor does not know `Server.REST.likes` yet**, it is because the server has not rebuilt since you added the route. Save a server file (or restart `toiljs dev`) to regenerate `shared/server.ts`.\n- **The counter is global.** Everyone hitting `/likes` shares the same \"home\" tally, because we used one fixed key. Use a different key per page or per user to split the count.\n\n## Where to go next\n\n- Return richer objects and lists: [Data types](../backend/data.md) and [HTTP routes](../backend/rest.md).\n- Call the server without URLs, as plain function calls: [Typed RPC](../backend/rpc.md).\n- Store more than a number: [Documents](../database/documents.md), [Events](../database/events.md), and [Views](../database/views.md).\n- Add login and sessions: [Auth](../auth/README.md).\n- Style your pages: [Styling](../frontend/styling.md) and [Routing](../frontend/routing.md).\n\n## Related\n\n- [Project structure](./project-structure.md)\n- [Database overview](../database/README.md)\n- [Counters](../database/counters.md)\n- [Backend overview](../backend/README.md)\n",
47
- "getting-started/installation.md": "# Installation\n\nInstall the toiljs command-line tool (CLI) so you can create and run projects. This takes a couple of minutes.\n\n## Why and when\n\nYou need the toiljs CLI once, before you create your first project. The CLI is the single tool you use to scaffold, run, build, and check toiljs apps. After a project exists, the same CLI is also installed inside that project, so day-to-day you can run it through your package scripts (`npm run dev`) without a global install.\n\n## Prerequisites\n\nYou need **Node.js version 24.0.0 or newer**. Node.js is the JavaScript runtime that powers the toiljs CLI, the dev server, and the build. (Your backend does not run on Node.js, but the tools that build it do.)\n\nCheck your version:\n\n```sh\nnode --version\n```\n\nIf it prints `v24.0.0` or higher, you are set. If it is older or the command is not found, install a current Node.js from [nodejs.org](https://nodejs.org), or use a version manager like [nvm](https://github.com/nvm-sh/nvm) or [fnm](https://github.com/Schniz/fnm):\n\n```sh\n# with nvm\nnvm install 24\nnvm use 24\n```\n\nYou also need a package manager. **npm** comes with Node.js, so you already have it. toiljs also supports **pnpm**, **yarn**, and **bun** if you prefer one of those.\n\n### TypeScript 6, not 7\n\ntoiljs requires **TypeScript 6** (`>=6.0.0 <7.0.0`). TypeScript 7 is not supported yet.\n\nTypeScript 7 is the native (Go) port of the compiler. It ships a much faster `tsc`, but its package no longer exports the JavaScript compiler API, whose main entry is now just `{ version, versionMajorMinor }`. toiljs reads each route's static `metadata` export through that API to bake your SEO tags into the built HTML, and the `toiljs/eslint` preset loads it too. On TypeScript 7 the metadata baking silently stops (your built pages lose their tags) and typescript-eslint fails to load at all.\n\nPin TypeScript in your `package.json`:\n\n```json\n{\n \"devDependencies\": {\n \"typescript\": \"^6.0.3\"\n }\n}\n```\n\n`toiljs doctor` flags an unsupported TypeScript, and `toiljs doctor --fix` pins it back for you. `toiljs update` will not upgrade you into TypeScript 7. Support will land once the tools toiljs builds on can read the new `typescript/unstable/*` API.\n\n## Install the CLI\n\nThe package is called `toiljs`, and it provides a command named `toiljs`. You have two ways to use it.\n\n### Option A: run it on demand with npx (no install)\n\n`npx` comes with npm and runs a package without installing it globally. This is the quickest way to create your first project:\n\n```sh\nnpx toiljs create my-app\n```\n\n### Option B: install it globally\n\nIf you plan to create projects often, install it once so `toiljs` is always on your path:\n\n```sh\nnpm install -g toiljs\n```\n\nThen you can run `toiljs` directly:\n\n```sh\ntoiljs create my-app\n```\n\nBoth options end up at the same place. The rest of these docs write `toiljs <command>`; if you did not install globally, just put `npx` in front (`npx toiljs <command>`).\n\n## Verify it works\n\nCheck the version:\n\n```sh\ntoiljs --version\n```\n\nYou should see a version number printed (for example, `0.0.86`).\n\nTo see every command and flag, run help:\n\n```sh\ntoiljs --help\n```\n\nInside a project, there is a deeper health check called **doctor**. It inspects your setup and dependencies and tells you exactly what to fix. You will use it after you create a project, but it is good to know it exists:\n\n```sh\ntoiljs doctor\n```\n\n`toiljs doctor` reads your project (its `package.json`, config, routes, and build output), runs a series of checks, and prints a grouped report. One of the first checks is your Node.js version against the required `>=24.0.0`, so if your Node is too old, doctor will say so. Add `--fix` to let it repair the things it can (such as the typed-RPC wiring), or `--json` for machine-readable output in a CI pipeline.\n\n## How the tooling fits together\n\nThe `toiljs` CLI is a friendly front end over two underlying tools. You rarely call them directly, but it helps to know they exist.\n\n```mermaid\nflowchart LR\n CLI[\"toiljs CLI\"] --> TS[\"toilscript<br/>(compiles server/ to wasm)\"]\n CLI --> VITE[\"Vite<br/>(bundles client/ React)\"]\n TS --> W[\"build/server/release.wasm\"]\n VITE --> B[\"build/client/\"]\n```\n\nWhen you create a project, both `toiljs` and `toilscript` are added to it as dependencies, so everything is pinned to versions that work together. You do not install `toilscript` separately.\n\n## Gotchas and notes\n\n- **\"command not found: toiljs\"** after a global install usually means npm's global bin folder is not on your `PATH`. Either fix your `PATH` or just use `npx toiljs ...` instead.\n- **Node too old** is the most common first-time failure. WebAssembly tooling and the build depend on features in Node 24 and up. Upgrade before creating a project.\n- **Every command checks for updates.** On each run, the CLI quietly checks npm for a newer toiljs and prints a note if you are behind. It never blocks the command. To turn it off, set the environment variable `TOILJS_NO_UPDATE_CHECK=1`.\n- You do **not** need to install a database, a Docker container, or any cloud account to develop locally. The dev server includes a local ToilDB so your data-backed features run out of the box.\n\n## Related\n\n- [Create a project](./create-project.md)\n- [The CLI reference](../cli/README.md)\n- [Getting started overview](./README.md)\n",
47
+ "getting-started/installation.md": "# Installation\n\nInstall the toiljs command-line tool (CLI) so you can create and run projects. This takes a couple of minutes.\n\n## Why and when\n\nYou need the toiljs CLI once, before you create your first project. The CLI is the single tool you use to scaffold, run, build, and check toiljs apps. After a project exists, the same CLI is also installed inside that project, so day-to-day you can run it through your package scripts (`npm run dev`) without a global install.\n\n## Prerequisites\n\nYou need **Node.js version 24.0.0 or newer**. Node.js is the JavaScript runtime that powers the toiljs CLI, the dev server, and the build. (Your backend does not run on Node.js, but the tools that build it do.)\n\nCheck your version:\n\n```sh\nnode --version\n```\n\nIf it prints `v24.0.0` or higher, you are set. If it is older or the command is not found, install a current Node.js from [nodejs.org](https://nodejs.org), or use a version manager like [nvm](https://github.com/nvm-sh/nvm) or [fnm](https://github.com/Schniz/fnm):\n\n```sh\n# with nvm\nnvm install 24\nnvm use 24\n```\n\nYou also need a package manager. **npm** comes with Node.js, so you already have it. toiljs also supports **pnpm**, **yarn**, and **bun** if you prefer one of those.\n\n### TypeScript 7\n\ntoiljs requires **TypeScript 7** (`>=7.0.2 <8.0.0`). The native compiler is shipped in the\n`typescript` package and still uses the `tsc` command. Vite 8 bundles the React 19 frontend.\nRoute metadata and search indexing use Oxc parsing; they do not need the old JavaScript compiler API.\n\nFor an existing project:\n\n```sh\nnpm uninstall eslint typescript-eslint @typescript-eslint/utils @eslint/js @eslint-react/eslint-plugin eslint-plugin-react-refresh\nnpm install -D typescript@^7.0.2 oxlint@latest oxlint-tsgolint@latest\n```\n\nReplace `eslint.config.js` with `oxlint.config.ts`:\n\n```ts\nimport toiljs from 'toiljs/oxlint';\nexport default toiljs;\n```\n\nSet `lint` to `oxlint --type-aware client` and keep `typecheck` as `tsc --noEmit`.\nThe shared preset retains the former strict TypeScript rules, severities, React Hooks checks,\nFast Refresh export exceptions, blank-line rule, and custom byte-array rule. Built-in semantic\nrules run in tsgolint; the custom byte-array Oxlint plugin queries the TypeScript 7 native checker.\nSee [toolchain migration](./typescript7.md) for custom configurations and editor setup.\n\n`toiljs doctor` checks the installed compiler, the declared version range, and type-aware lint\nconfiguration. `doctor --fix` migrates TypeScript declarations to `^7.0.2`. Reinstall afterward.\n`toiljs update` only applies TypeScript ranges confined to supported 7.x versions, including when\nusing `--target patch` on an older project.\n\n## Install the CLI\n\nThe package is called `toiljs`, and it provides a command named `toiljs`. You have two ways to use it.\n\n### Option A: run it on demand with npx (no install)\n\n`npx` comes with npm and runs a package without installing it globally. This is the quickest way to create your first project:\n\n```sh\nnpx toiljs create my-app\n```\n\n### Option B: install it globally\n\nIf you plan to create projects often, install it once so `toiljs` is always on your path:\n\n```sh\nnpm install -g toiljs\n```\n\nThen you can run `toiljs` directly:\n\n```sh\ntoiljs create my-app\n```\n\nBoth options end up at the same place. The rest of these docs write `toiljs <command>`; if you did not install globally, just put `npx` in front (`npx toiljs <command>`).\n\n## Editor setup\n\nAfter creating your project, run `npm install` inside it. The **Oxc** editor plugin uses the\nproject's `oxlint` and `oxlint-tsgolint` dependencies to show lint diagnostics and fixes. Use\nNode 26 for editor tooling to match toiljs CI. Installing npm dependencies does not install\nthe editor plugin.\n\n### VS Code\n\n1. Open Extensions and install [Oxc](https://marketplace.visualstudio.com/items?itemName=oxc.oxc-vscode),\n with identifier `oxc.oxc-vscode`. Alternatively, run `code --install-extension oxc.oxc-vscode`.\n2. Merge this setting into your project's `.vscode/settings.json`:\n\n ```json\n {\n \"oxc.typeAware\": true\n }\n ```\n\n3. If you installed dependencies while the editor was open, run **Oxc: Restart oxlint Server**\n from the Command Palette. Check the **Oxc (Lint)** output channel if it does not start.\n\nNew toiljs projects already recommend the extension and enable type-aware linting; you still\nneed to install the extension. Optional lint fixes on save can be enabled with\n`\"editor.codeActionsOnSave\": { \"source.fixAll.oxc\": \"explicit\" }`.\n\n### WebStorm\n\n1. Open **Settings → Plugins → Marketplace**, search for [Oxc](https://plugins.jetbrains.com/plugin/27061-oxc),\n and install it. Restart the IDE if prompted.\n2. Set the project's Node.js interpreter to Node 26.\n3. Open **Settings → Tools → Oxlint**, select **Automatic configuration**, and check\n **Enable type aware rules**.\n4. Apply the settings, then run **Restart Oxlint Server** through Find Action.\n\nThe plugin has its own type-aware checkbox, which defaults to off. Its explicit value\n[overrides `options.typeAware` in the project config](https://oxc.rs/docs/guide/usage/linter/lsp-config-reference#typeaware),\nso installing the plugin alone does not enable tsgolint checks.\n\nIf automatic detection does not start the server, select **Manual configuration** and set\n**Path to Oxlint Language Server** to `<project>/node_modules/oxlint/bin/oxlint`. Check\n**Add --lsp CLI argument** and keep **Enable type aware rules** checked. Use the JavaScript\nlauncher at that path so `oxlint.config.ts` and the custom JavaScript rules can load.\n\nLeave **Path to Oxlint Config** blank when opening the whole toiljs repository, and leave\n**Disable nested config lookups** unchecked, so both examples use their own configurations.\nFor a standalone app whose config is not discovered, select that app's `oxlint.config.ts`.\n\nThe [Oxc editor guide](https://oxc.rs/docs/guide/usage/linter/editors) covers supported editors.\nRun `npm run lint` to check the same project rules from a terminal.\n\n## Verify it works\n\nCheck the version:\n\n```sh\ntoiljs --version\n```\n\nYou should see a version number printed (for example, `0.0.86`).\n\nTo see every command and flag, run help:\n\n```sh\ntoiljs --help\n```\n\nInside a project, there is a deeper health check called **doctor**. It inspects your setup and dependencies and tells you exactly what to fix. You will use it after you create a project, but it is good to know it exists:\n\n```sh\ntoiljs doctor\n```\n\n`toiljs doctor` reads your project (its `package.json`, config, routes, and build output), runs a series of checks, and prints a grouped report. One of the first checks is your Node.js version against the required `>=24.0.0`, so if your Node is too old, doctor will say so. Add `--fix` to let it repair the things it can (such as the typed-RPC wiring), or `--json` for machine-readable output in a CI pipeline.\n\n## How the tooling fits together\n\nThe `toiljs` CLI is a friendly front end over two underlying tools. You rarely call them directly, but it helps to know they exist.\n\n```mermaid\nflowchart LR\n CLI[\"toiljs CLI\"] --> TS[\"toilscript<br/>(compiles server/ to wasm)\"]\n CLI --> VITE[\"Vite<br/>(bundles client/ React)\"]\n TS --> W[\"build/server/release.wasm\"]\n VITE --> B[\"build/client/\"]\n```\n\nWhen you create a project, both `toiljs` and `toilscript` are added to it as dependencies, so everything is pinned to versions that work together. You do not install `toilscript` separately.\n\n## Gotchas and notes\n\n- **\"command not found: toiljs\"** after a global install usually means npm's global bin folder is not on your `PATH`. Either fix your `PATH` or just use `npx toiljs ...` instead.\n- **Node too old** is the most common first-time failure. WebAssembly tooling and the build depend on features in Node 24 and up. Upgrade before creating a project.\n- **Every command checks for updates.** On each run, the CLI quietly checks npm for a newer toiljs and prints a note if you are behind. It never blocks the command. To turn it off, set the environment variable `TOILJS_NO_UPDATE_CHECK=1`.\n- You do **not** need to install a database, a Docker container, or any cloud account to develop locally. The dev server includes a local ToilDB so your data-backed features run out of the box.\n\n## Related\n\n- [Create a project](./create-project.md)\n- [The CLI reference](../cli/README.md)\n- [Getting started overview](./README.md)\n",
48
48
  "getting-started/migrating.md": "# Migrating an existing React app\n\nHow to bring a React app you already have into toiljs. The frontend usually moves over with small changes. The backend is the real work, because a toiljs server is not Node.js, and this page is honest about what that means.\n\n## Why and when\n\nMove to toiljs when you want your frontend and backend in one typed repo, deployed to the edge, with a built-in global database, and you are willing to rewrite your server logic against toiljs's rules. If you only want a React bundler, toiljs is more than you need. If you want the full-stack, typed, edge model, this is the payoff.\n\nThe safest approach is **not** to convert your old project in place. Instead, scaffold a fresh toiljs project and move code into it piece by piece. A fresh project comes with the required presets, config, and routing already wired, so you spend your time on your code, not on plumbing.\n\n```sh\ntoiljs create my-app\ncd my-app\n```\n\n## The big picture: what moves where\n\n```mermaid\nflowchart LR\n subgraph OLD[\"Your current app\"]\n FE[\"React components,<br/>pages, styles, assets\"]\n BE[\"Node/Express backend,<br/>DB access, npm libs\"]\n end\n FE -->|\"mostly copy\"| C[\"toiljs client/\"]\n BE -->|\"rewrite\"| S[\"toiljs server/\"]\n C -.->|\"generated typed client\"| S\n S --> DB[(\"ToilDB\")]\n```\n\n- Your **React frontend** copies into `client/` with modest changes (routing and asset paths).\n- Your **backend** is **rewritten** into `server/` as toilscript, because it compiles to WebAssembly, not Node.\n\n## The frontend: what changes\n\n`client/` is a normal Vite + React app, so most of your frontend works as is. The common adjustments:\n\n- **Routing becomes file-based.** toiljs has no `<Routes>`/`<Route>` config. A file at `client/routes/about.tsx` is the `/about` page; `client/routes/blog/[slug].tsx` is `/blog/:slug`. Move each page component to the matching file and delete your router setup. Use `Toil.Link` for navigation instead of your router's `Link`. See [Routing](../frontend/routing.md).\n- **Static assets move to `client/public/`.** Files there are served as is (images at `/images/...`, plus `favicon` and `robots.txt`).\n- **Global styles move to `client/styles/`**, and are imported from `client/toil.tsx`. See [Styling](../frontend/styling.md).\n- **Your React npm packages are fine.** The client is regular JavaScript, so component libraries, state managers, and browser APIs all work.\n- **Data fetching changes** if you want the typed client: replace hand-written `fetch('/api/...')` calls with the generated `Server.REST.*` methods. You can keep raw `fetch` too, but you lose the type safety. See [Fetching data](../frontend/data-fetching.md).\n\nMetadata and SEO that you used a helmet library for is built in: set a `metadata` export per route, or use `Toil.Head`. See [Metadata and SEO](../frontend/metadata.md).\n\n## The backend: the honest part\n\nThis is where migration takes real effort. Read this section carefully before you plan the work.\n\nYour toiljs server is compiled by **toilscript** into WebAssembly. toilscript accepts a **strict subset of TypeScript**. It looks like TypeScript, but treat it as a small, separate language that happens to share the syntax. The practical consequences:\n\n### What is NOT available on the server\n\n- **No arbitrary npm packages.** You cannot `import` a library from `node_modules` into `server/`. There is no `express`, no `pg`, no `stripe` SDK, no `lodash`. If your route logic leans on npm libraries, that logic has to be reworked.\n- **No Node.js APIs.** No `fs`, `http`, `process`, `Buffer`, `path`, or other Node built-ins. The server runs in a sandbox, not in Node.\n- **No DOM or browser APIs**, because it is not a browser either.\n- **No connecting to your existing database.** There is no connection string and no driver. Your data moves to **ToilDB**, the built-in database.\n\n### What you use instead\n\ntoiljs replaces the common needs with built-in globals and decorators, so you rarely miss the missing libraries:\n\n| You used to reach for | On the toiljs server you use |\n| --- | --- |\n| `express` routes / a router | `@rest` controllers with `@get` / `@post` ([HTTP routes](../backend/rest.md)) |\n| A REST client between services | `@service` / `@remote` typed RPC ([RPC](../backend/rpc.md)) |\n| Postgres / Mongo / Redis | [ToilDB](../database/README.md) families (documents, counters, events, views) |\n| `jsonwebtoken`, session middleware | Built-in [auth](../auth/README.md) and cookies |\n| `crypto` from Node | the `crypto` global (synchronous Web Crypto) ([Crypto](../services/crypto.md)) |\n| `nodemailer` / an email SDK | the built-in email service ([Email](../services/email.md)) |\n| `process.env` | `Environment.get()` / `Environment.getSecure()` ([Environment](../services/environment.md)) |\n| `Date.now()`, timers | the `Time` global ([Time](../services/time.md)) |\n\n### Type system differences\n\ntoilscript uses precise, explicit numeric types instead of JavaScript's single `number`. You will write `i32`, `u64`, `f64`, and even `u256`, and be explicit about integer sizes. Values that must cross the wire are `@data` classes with concrete fields, not free-form objects. There is no `any`-style duck typing to lean on. The full rules, and how server types map to `bigint` and friends on the client, are in [Types](../concepts/types.md).\n\n### Two behaviors to design around\n\n- **Memory resets every request.** Each request gets a fresh `.wasm` instance and its memory is wiped afterward. A module-level variable does not persist. Anything durable goes in ToilDB. (This is the same rule you met in [Your first app](./first-app.md).)\n- **Reads and writes are split.** A `@get` is a read-only query; a `@post` is a write action. Operations that scan unbounded data are not allowed in a request handler; they move to a [`@derive`](../background/derive.md) or a background [daemon](../background/daemons.md).\n\n## Configuration\n\nTwo config files replace the various configs you may have had:\n\n- **`toil.config.ts`** is your client and build config: SEO defaults, image optimization, page transitions, and dev-server options. It uses `defineConfig`. See [Configuration](../concepts/config.md).\n- **`toilconfig.json`** is the low-level server (wasm) build config. The scaffold sets sensible defaults, and you usually leave it alone.\n\nEnvironment variables move from a `.env` you read with `process.env` to `.env` / `.env.secrets` files you read with `Environment.get()` / `Environment.getSecure()`. Secrets and plain vars are kept in separate buckets. See [Environment and secrets](../services/environment.md).\n\n## Step by step\n\n1. **Scaffold a fresh project** with `toiljs create` and get it running with `npm run dev`. Start from something that works.\n2. **Move the frontend.** Copy components into `client/components/`, turn each page into a file under `client/routes/`, move styles into `client/styles/`, and static assets into `client/public/`. Swap your router's `Link` for `Toil.Link`. Install your client-side npm dependencies.\n3. **Get the client rendering** against the routes, even if the data is still stubbed or points at your old backend. Fix routing and asset paths first.\n4. **Rebuild the backend, one route at a time.** For each old endpoint, add a `@data` model in `server/models/`, a `@rest` controller in `server/routes/`, and move its data into a ToilDB family. Import each new route in `server/main.ts`. This is the bulk of the effort; go endpoint by endpoint.\n5. **Move persistence to ToilDB.** Map each table or collection to a ToilDB family (a document store, a counter, an event log, or a view). See [Database overview](../database/README.md) for choosing a family.\n6. **Switch the client to the typed client.** Replace `fetch('/api/...')` calls with `Server.REST.*` (or `Server.<service>.*` for RPC). Now a backend change that breaks the contract shows up as a client type error.\n7. **Run the doctor.** `toiljs doctor` checks your wiring (routes, RPC generation, config, dependencies) and points at anything still off. Add `--fix` to let it repair what it can.\n\n## When toiljs is not the right move\n\nBe honest with yourself about the backend rewrite. Migration is a poor fit if:\n\n- Your server depends heavily on npm libraries or Node-only APIs that have no toiljs equivalent, and rewriting them is not worth it.\n- You must keep talking to an existing external database or service that toiljs cannot reach. (Daemons can make outbound HTTP calls in some cases, but general server-side networking is limited by design.)\n- You need long-lived in-process server state that does not fit the per-request, database-backed model.\n\nIn those cases, you can still adopt toiljs for the frontend and keep your existing backend, calling it with plain `fetch`. You just give up the single-repo, end-to-end-typed benefits for that part.\n\n## Gotchas and notes\n\n- **Do not try to `import` server code into the client or vice versa.** They compile with different rules. The only bridge is the generated `shared/server.ts`.\n- **One `@data` type per file** under `models/` matches the tooling's expectations.\n- **`shared/server.ts` is generated**, so it will not exist until your first build, and you never edit it.\n- **Plan the backend as a rewrite, not a port.** The frontend moves; the backend is re-expressed in toiljs's model. Budget for that.\n\n## Related\n\n- [Getting started overview](./README.md)\n- [Backend overview](../backend/README.md)\n- [Types](../concepts/types.md)\n- [Database overview](../database/README.md)\n- [The CLI reference](../cli/README.md)\n- [Configuration](../concepts/config.md)\n",
49
- "getting-started/project-structure.md": "# Project structure\n\nA tour of every folder and file in a toiljs project, what each one is for, and the single most important question: **where does this code run, the browser or the edge?**\n\n## Why this matters\n\ntoiljs blends frontend and backend into one repo, so the same `.ts` file extension can mean two very different things. A file in `client/` becomes JavaScript that runs in your user's browser. A file in `server/` becomes WebAssembly that runs on the edge, with different rules and different available APIs. Knowing which folder you are in tells you what you are allowed to do. Keep this mental split and everything else falls into place.\n\n```mermaid\nflowchart TD\n subgraph Browser[\"Runs in the browser\"]\n C[\"client/*\"]\n end\n subgraph Edge[\"Runs on the edge (WebAssembly)\"]\n S[\"server/* -> release.wasm\"]\n end\n subgraph Generated[\"Generated glue (types + config)\"]\n SH[\"shared/server.ts\"]\n CFG[\"toil.config.ts / toilconfig.json\"]\n end\n C -. \"typed calls\" .-> SH\n SH -. \"HTTP / realtime\" .-> S\n S --> DB[(\"ToilDB\")]\n```\n\n## The top level\n\nThese files sit in your project root.\n\n| File | What it is | Runs where |\n| --- | --- | --- |\n| `package.json` | Scripts (`dev`, `build`, `lint`, `typecheck`, `format`) and dependencies (`toiljs`, `react`, `toilscript`, ...) | tooling only |\n| `toil.config.ts` | **Client and build config.** Uses `defineConfig` to set SEO, images, page transitions, and dev options. | tooling only |\n| `toilconfig.json` | **Server (wasm) build config** for toilscript: the entry file, the output `.wasm` path, and low-level compile options. You rarely edit this. | tooling only |\n| `tsconfig.json` | TypeScript config for the client (`client/`, `shared/`, `emails/`). Extends `toiljs/tsconfig`. | tooling only |\n| `eslint.config.js` | Linting preset (`toiljs/eslint`). | tooling only |\n| `.prettierrc` | Formatting preset (`toiljs/prettier`). | tooling only |\n| `.prettierignore` | Files Prettier should skip (generated files). | tooling only |\n| `.gitignore` | Ignores `build/`, `.toil/`, generated files, and your `.env` files. | tooling only |\n| `.vscode/settings.json` | Tells VS Code to use the project's TypeScript so the toilscript editor plugin loads. | editor only |\n| `toil-env.d.ts` | **Generated** editor types for client globals like `Toil.Link` and `Toil.Image`. Do not edit. | editor only |\n| `toil-routes.d.ts` | **Generated** list of your real route names, so `Toil.Link href=\"...\"` type-checks. Filled in on the first build. | editor only |\n| `README.md` | Your project's readme. | docs |\n| `CLAUDE.md`, `AGENTS.md`, etc. | Optional AI-assistant hint files that point tools at the toiljs docs. | docs |\n\nTwo folders you may also see at the root:\n\n- **`.toil/`** is a working directory toiljs manages (a build cache and a copy of the docs). It is gitignored. You never edit it.\n- **`.env` and `.env.secrets`** are files **you** create when you need local environment variables or secrets during `toiljs dev`. They are gitignored so you never commit them, and the edge loads their real values out of band in production. Your server reads them with `Environment.get(\"KEY\")` and `Environment.getSecure(\"KEY\")`. See [Environment and secrets](../services/environment.md).\n\n## `client/` (runs in the browser)\n\nThis is a normal React app. You can use React libraries and browser APIs here freely.\n\n```text\nclient/\n toil.tsx the entry point; mounts your app\n layout.tsx the root layout wrapping every page\n 404.tsx the not-found page\n global-error.tsx the top-level error page\n routes/ file-based pages\n components/ shared React components\n styles/ global stylesheets\n public/ static files served as-is\n```\n\n- **`toil.tsx`** is the entry file. It imports your global styles and calls `Toil.mount(...)` to start the app. You rarely change it beyond the style imports.\n- **`layout.tsx`** is your root layout: the header, footer, and page shell that wrap every route. Its `children` prop is the current page.\n- **`404.tsx`** renders when no route matches. **`global-error.tsx`** renders when a route throws.\n- **`routes/`** is where pages live, and the file name **is** the URL. `routes/index.tsx` is `/`, `routes/about.tsx` is `/about`, `routes/blog/[slug].tsx` is `/blog/:slug`. This is called **file-based routing**. See [Routing](../frontend/routing.md).\n- **`components/`** holds React components you reuse across pages. Nothing here is a route.\n- **`styles/`** holds your global CSS (or Sass, Less, or Stylus, if you chose one). See [Styling](../frontend/styling.md).\n- **`public/`** holds static files served exactly as they are: `favicon`, `robots.txt`, and an `images/` folder (reachable at `/images/...`). The `public/index.html` is the base HTML shell your app mounts into.\n\nA single global, `Toil`, is available in client code without an import (for `Toil.Link`, `Toil.Image`, and `Toil.Head`). It is typed by the generated `toil-env.d.ts`.\n\n## `server/` (runs on the edge, as WebAssembly)\n\nThis is your backend. It is compiled by toilscript into one `.wasm` file. Remember the two rules: **memory resets every request**, and **this is not Node.js** (a strict TypeScript subset, no arbitrary npm packages). See [Backend overview](../backend/README.md) and [Types](../concepts/types.md).\n\n```text\nserver/\n main.ts the entry: wires the handler + imports your modules\n tsconfig.json server-only TS config (loads the toilscript editor plugin)\n toil-server-env.d.ts generated editor types for server globals\n core/ your request handler and shared logic\n models/ @data classes\n routes/ @rest controllers (HTTP)\n services/ @service / @remote (typed RPC)\n migrations/ ToilDB schema migrations\n scheduled/ reserved for scheduled tasks\n```\n\n- **`main.ts`** is the entry the build compiles. It does three required things: it sets `Server.handler` (a factory that returns one fresh handler per request), it re-exports the wasm entry points (`export * from 'toiljs/server/runtime/exports'`), and it defines the `abort` hook. It also `import`s your other server modules so a direct toilscript run builds the same code.\n- **`core/`** holds your top-level `ToilHandler` (often `AppHandler.ts`): the first code that sees each request. It can dispatch to your `@rest` controllers and then fall through to any hand-written logic.\n- **`models/`** holds your `@data` classes, one type per file. A `@data` class is a typed message that can cross the wire between client and server (and into ToilDB). See [Data types](../backend/data.md).\n- **`routes/`** holds your `@rest` controllers: classes decorated with `@rest`, `@get`, and `@post` that expose HTTP endpoints. See [HTTP routes](../backend/rest.md).\n- **`services/`** holds `@service` classes and free `@remote` functions: typed remote calls the client makes as plain function calls (no URLs). See [Typed RPC](../backend/rpc.md).\n- **`migrations/`** holds ToilDB schema migrations. When you change the shape of a stored `@data` type, you add a `<Type>.migration.ts` here that carries old records forward. The compiler enforces this convention. See [Documents](../database/documents.md).\n- **`scheduled/`** is reserved for scheduled tasks. New decorated files anywhere under `server/` are picked up automatically by the build.\n- **`tsconfig.json`** and **`toil-server-env.d.ts`** are editor-support files. They teach your editor about the server globals (`crypto`, `Cookie`, `Environment`, and friends) so it stops flagging them. They do not affect the build.\n\n### How the build discovers your server code\n\nYou do not register routes in a config file. The compiler scans every `.ts` file under `server/` and picks up anything that declares a decorated surface (`@rest`, `@service`, `@remote`, `@data`, `@user`, `@database`, and so on). Importing those files from `main.ts` is still good practice: it keeps a direct `toilscript` run building the exact same server.\n\n## `shared/` (generated glue)\n\n```text\nshared/\n server.ts GENERATED typed client (do not edit)\n```\n\n**`shared/server.ts` is written for you** by the server build. It contains:\n\n- A typed `Server` object the browser uses to call your backend: `Server.REST.*` for HTTP routes and `Server.<service>.*` for RPC.\n- The client-side codecs for every `@data` class, so responses come back as real typed objects.\n- A `getUser()` helper for reading the signed-in user on the client.\n\nBecause it is generated, it does not exist in a fresh project and it is gitignored. It appears the first time you run `toiljs dev` or `toiljs build`. Never hand-edit it; change your server code and it regenerates.\n\n## `build/` (compiled output)\n\n```text\nbuild/\n server/release.wasm your compiled backend (+ release.wat, a readable text form)\n client/ the bundled React app (from Vite)\n```\n\nThis is what actually ships. It is gitignored and recreated by `toiljs build`. You do not edit anything here.\n\n## Putting it together: one request\n\n```mermaid\nsequenceDiagram\n participant B as Browser (client/)\n participant SH as shared/server.ts\n participant W as server.wasm (server/)\n participant DB as ToilDB\n B->>SH: Server.REST.likes.like()\n SH->>W: HTTP POST /likes\n W->>DB: counter.add(key, 1)\n W-->>SH: typed LikeCount response\n SH-->>B: { count }\n```\n\nThe browser calls a typed method, the generated client turns it into an HTTP request, your `.wasm` handles it and touches ToilDB, and a typed result comes back. You wrote both ends; the middle is generated.\n\n## Gotchas and notes\n\n- **A `.ts` file's folder decides its rules.** The same code that works in `client/` may not compile in `server/`, because the server is a strict subset without Node APIs.\n- **Do not edit generated files.** `shared/server.ts`, `toil-env.d.ts`, `toil-routes.d.ts`, and `toil-server-env.d.ts` are all regenerated by the build and will overwrite your changes.\n- **One `@data` type per file** under `models/` keeps things tidy and matches the convention the tooling expects.\n- **`build/` and `.toil/` are disposable.** Delete them and the next build recreates them.\n\n## Related\n\n- [Your first app](./first-app.md)\n- [Frontend overview](../frontend/README.md) and [Routing](../frontend/routing.md)\n- [Backend overview](../backend/README.md)\n- [Database overview](../database/README.md)\n- [Configuration](../concepts/config.md)\n- [Decorators reference](../concepts/decorators.md)\n",
49
+ "getting-started/project-structure.md": "# Project structure\n\nA tour of every folder and file in a toiljs project, what each one is for, and the single most important question: **where does this code run, the browser or the edge?**\n\n## Why this matters\n\ntoiljs blends frontend and backend into one repo, so the same `.ts` file extension can mean two very different things. A file in `client/` becomes JavaScript that runs in your user's browser. A file in `server/` becomes WebAssembly that runs on the edge, with different rules and different available APIs. Knowing which folder you are in tells you what you are allowed to do. Keep this mental split and everything else falls into place.\n\n```mermaid\nflowchart TD\n subgraph Browser[\"Runs in the browser\"]\n C[\"client/*\"]\n end\n subgraph Edge[\"Runs on the edge (WebAssembly)\"]\n S[\"server/* -> release.wasm\"]\n end\n subgraph Generated[\"Generated glue (types + config)\"]\n SH[\"shared/server.ts\"]\n CFG[\"toil.config.ts / toilconfig.json\"]\n end\n C -. \"typed calls\" .-> SH\n SH -. \"HTTP / realtime\" .-> S\n S --> DB[(\"ToilDB\")]\n```\n\n## The top level\n\nThese files sit in your project root.\n\n| File | What it is | Runs where |\n| --- | --- | --- |\n| `package.json` | Scripts (`dev`, `build`, `lint`, `typecheck`, `format`) and dependencies (`toiljs`, `react`, `toilscript`, ...) | tooling only |\n| `toil.config.ts` | **Client and build config.** Uses `defineConfig` to set SEO, images, page transitions, and dev options. | tooling only |\n| `toilconfig.json` | **Server (wasm) build config** for toilscript: the entry file, the output `.wasm` path, and low-level compile options. You rarely edit this. | tooling only |\n| `tsconfig.json` | TypeScript config for the client (`client/`, `shared/`, `emails/`). Extends `toiljs/tsconfig`. | tooling only |\n| `oxlint.config.ts` | Linting preset (`toiljs/oxlint`). | tooling only |\n| `.prettierrc` | Formatting preset (`toiljs/prettier`). | tooling only |\n| `.prettierignore` | Files Prettier should skip (generated files). | tooling only |\n| `.gitignore` | Ignores `build/`, `.toil/`, generated files, and your `.env` files. | tooling only |\n| `.vscode/settings.json` | Tells VS Code to use the project's TypeScript so the toilscript editor plugin loads. | editor only |\n| `toil-env.d.ts` | **Generated** editor types for client globals like `Toil.Link` and `Toil.Image`. Do not edit. | editor only |\n| `toil-routes.d.ts` | **Generated** list of your real route names, so `Toil.Link href=\"...\"` type-checks. Filled in on the first build. | editor only |\n| `README.md` | Your project's readme. | docs |\n| `CLAUDE.md`, `AGENTS.md`, etc. | Optional AI-assistant hint files that point tools at the toiljs docs. | docs |\n\nTwo folders you may also see at the root:\n\n- **`.toil/`** is a working directory toiljs manages (a build cache and a copy of the docs). It is gitignored. You never edit it.\n- **`.env` and `.env.secrets`** are files **you** create when you need local environment variables or secrets during `toiljs dev`. They are gitignored so you never commit them, and the edge loads their real values out of band in production. Your server reads them with `Environment.get(\"KEY\")` and `Environment.getSecure(\"KEY\")`. See [Environment and secrets](../services/environment.md).\n\n## `client/` (runs in the browser)\n\nThis is a normal React app. You can use React libraries and browser APIs here freely.\n\n```text\nclient/\n toil.tsx the entry point; mounts your app\n layout.tsx the root layout wrapping every page\n 404.tsx the not-found page\n global-error.tsx the top-level error page\n routes/ file-based pages\n components/ shared React components\n styles/ global stylesheets\n public/ static files served as-is\n```\n\n- **`toil.tsx`** is the entry file. It imports your global styles and calls `Toil.mount(...)` to start the app. You rarely change it beyond the style imports.\n- **`layout.tsx`** is your root layout: the header, footer, and page shell that wrap every route. Its `children` prop is the current page.\n- **`404.tsx`** renders when no route matches. **`global-error.tsx`** renders when a route throws.\n- **`routes/`** is where pages live, and the file name **is** the URL. `routes/index.tsx` is `/`, `routes/about.tsx` is `/about`, `routes/blog/[slug].tsx` is `/blog/:slug`. This is called **file-based routing**. See [Routing](../frontend/routing.md).\n- **`components/`** holds React components you reuse across pages. Nothing here is a route.\n- **`styles/`** holds your global CSS (or Sass, Less, or Stylus, if you chose one). See [Styling](../frontend/styling.md).\n- **`public/`** holds static files served exactly as they are: `favicon`, `robots.txt`, and an `images/` folder (reachable at `/images/...`). The `public/index.html` is the base HTML shell your app mounts into.\n\nA single global, `Toil`, is available in client code without an import (for `Toil.Link`, `Toil.Image`, and `Toil.Head`). It is typed by the generated `toil-env.d.ts`.\n\n## `server/` (runs on the edge, as WebAssembly)\n\nThis is your backend. It is compiled by toilscript into one `.wasm` file. Remember the two rules: **memory resets every request**, and **this is not Node.js** (a strict TypeScript subset, no arbitrary npm packages). See [Backend overview](../backend/README.md) and [Types](../concepts/types.md).\n\n```text\nserver/\n main.ts the entry: wires the handler + imports your modules\n tsconfig.json server-only TS config (loads the toilscript editor plugin)\n toil-server-env.d.ts generated editor types for server globals\n core/ your request handler and shared logic\n models/ @data classes\n routes/ @rest controllers (HTTP)\n services/ @service / @remote (typed RPC)\n migrations/ ToilDB schema migrations\n scheduled/ reserved for scheduled tasks\n```\n\n- **`main.ts`** is the entry the build compiles. It does three required things: it sets `Server.handler` (a factory that returns one fresh handler per request), it re-exports the wasm entry points (`export * from 'toiljs/server/runtime/exports'`), and it defines the `abort` hook. It also `import`s your other server modules so a direct toilscript run builds the same code.\n- **`core/`** holds your top-level `ToilHandler` (often `AppHandler.ts`): the first code that sees each request. It can dispatch to your `@rest` controllers and then fall through to any hand-written logic.\n- **`models/`** holds your `@data` classes, one type per file. A `@data` class is a typed message that can cross the wire between client and server (and into ToilDB). See [Data types](../backend/data.md).\n- **`routes/`** holds your `@rest` controllers: classes decorated with `@rest`, `@get`, and `@post` that expose HTTP endpoints. See [HTTP routes](../backend/rest.md).\n- **`services/`** holds `@service` classes and free `@remote` functions: typed remote calls the client makes as plain function calls (no URLs). See [Typed RPC](../backend/rpc.md).\n- **`migrations/`** holds ToilDB schema migrations. When you change the shape of a stored `@data` type, you add a `<Type>.migration.ts` here that carries old records forward. The compiler enforces this convention. See [Documents](../database/documents.md).\n- **`scheduled/`** is reserved for scheduled tasks. New decorated files anywhere under `server/` are picked up automatically by the build.\n- **`tsconfig.json`** and **`toil-server-env.d.ts`** are editor-support files. They teach your editor about the server globals (`crypto`, `Cookie`, `Environment`, and friends) so it stops flagging them. They do not affect the build.\n\n### How the build discovers your server code\n\nYou do not register routes in a config file. The compiler scans every `.ts` file under `server/` and picks up anything that declares a decorated surface (`@rest`, `@service`, `@remote`, `@data`, `@user`, `@database`, and so on). Importing those files from `main.ts` is still good practice: it keeps a direct `toilscript` run building the exact same server.\n\n## `shared/` (generated glue)\n\n```text\nshared/\n server.ts GENERATED typed client (do not edit)\n```\n\n**`shared/server.ts` is written for you** by the server build. It contains:\n\n- A typed `Server` object the browser uses to call your backend: `Server.REST.*` for HTTP routes and `Server.<service>.*` for RPC.\n- The client-side codecs for every `@data` class, so responses come back as real typed objects.\n- A `getUser()` helper for reading the signed-in user on the client.\n\nBecause it is generated, it does not exist in a fresh project and it is gitignored. It appears the first time you run `toiljs dev` or `toiljs build`. Never hand-edit it; change your server code and it regenerates.\n\n## `build/` (compiled output)\n\n```text\nbuild/\n server/release.wasm your compiled backend (+ release.wat, a readable text form)\n client/ the bundled React app (from Vite)\n```\n\nThis is what actually ships. It is gitignored and recreated by `toiljs build`. You do not edit anything here.\n\n## Putting it together: one request\n\n```mermaid\nsequenceDiagram\n participant B as Browser (client/)\n participant SH as shared/server.ts\n participant W as server.wasm (server/)\n participant DB as ToilDB\n B->>SH: Server.REST.likes.like()\n SH->>W: HTTP POST /likes\n W->>DB: counter.add(key, 1)\n W-->>SH: typed LikeCount response\n SH-->>B: { count }\n```\n\nThe browser calls a typed method, the generated client turns it into an HTTP request, your `.wasm` handles it and touches ToilDB, and a typed result comes back. You wrote both ends; the middle is generated.\n\n## Gotchas and notes\n\n- **A `.ts` file's folder decides its rules.** The same code that works in `client/` may not compile in `server/`, because the server is a strict subset without Node APIs.\n- **Do not edit generated files.** `shared/server.ts`, `toil-env.d.ts`, `toil-routes.d.ts`, and `toil-server-env.d.ts` are all regenerated by the build and will overwrite your changes.\n- **One `@data` type per file** under `models/` keeps things tidy and matches the convention the tooling expects.\n- **`build/` and `.toil/` are disposable.** Delete them and the next build recreates them.\n\n## Related\n\n- [Your first app](./first-app.md)\n- [Frontend overview](../frontend/README.md) and [Routing](../frontend/routing.md)\n- [Backend overview](../backend/README.md)\n- [Database overview](../database/README.md)\n- [Configuration](../concepts/config.md)\n- [Decorators reference](../concepts/decorators.md)\n",
50
50
  "getting-started/README.md": "# Getting started\n\ntoiljs is a full-stack web framework: you write a React frontend and a TypeScript backend in one project, and toiljs ships both together. This section takes you from \"nothing installed\" to \"a small feature running end to end.\"\n\n## What toiljs is\n\nThink of a normal web app as two programs that have to agree with each other:\n\n- A **frontend**: the React code that runs in your user's browser.\n- A **backend**: the code that runs on a server, answers requests, and talks to a database.\n\nNormally these live in separate projects, speak to each other over hand-written HTTP calls, and drift apart until something breaks at runtime. toiljs puts both in one repository and wires them together with types, so a change on one side shows up as a compile error on the other side instead of a bug in production.\n\nThe twist is what your backend becomes. You write it in TypeScript, but toiljs does not run it in Node.js. Instead, a compiler called **toilscript** turns your backend into **WebAssembly** (often shortened to \"Wasm\"): a small, fast, sandboxed program that runs at the **edge**. \"Edge\" just means servers spread all over the world, close to your users, so requests do not have to travel to one far-away data center. A worldwide database called **ToilDB** is built in, so you can store and read data without setting up or connecting to a database yourself.\n\nNew terms, defined once:\n\n- **WebAssembly / Wasm**: a compact binary format that runs code in a locked-down sandbox at near-native speed. Your backend compiles to a single `.wasm` file.\n- **toilscript**: the compiler that turns your TypeScript backend into that `.wasm` file. It accepts a strict subset of TypeScript (more on that below).\n- **Dacely edge**: the global network of servers that runs your `.wasm` backend.\n- **ToilDB**: the built-in database that lives on the edge next to your code.\n\n## The client / server / shared mental model\n\nEvery toiljs project is organized into three folders. The most important thing to learn first is **where each piece of code actually runs**.\n\n```mermaid\nflowchart LR\n A[\"Your project<br/>(one repo)\"] -->|toiljs build| B[\"client bundle<br/>React + JS\"]\n A -->|toilscript compiles| C[\"server.wasm<br/>your backend\"]\n B --> U[\"User's browser\"]\n C --> E[\"Dacely edge<br/>(worldwide servers)\"]\n E --> D[(\"ToilDB<br/>global database\")]\n U <-->|\"HTTP / realtime\"| E\n```\n\n- **`client/`** is your React app: pages, components, and styles. It is bundled by [Vite](https://vitejs.dev) and runs **in the browser**.\n- **`server/`** is your backend: HTTP routes, database access, auth. It is compiled by toilscript to `build/server/release.wasm` and runs **on the edge** (and locally when you run the dev server).\n- **`shared/`** holds a file that toiljs **generates for you** (`shared/server.ts`). It is a fully typed client: the browser calls your backend through a `Server` object, and TypeScript checks every call. You do not write this file by hand.\n\nHere is the loop in one picture:\n\n```mermaid\nflowchart TD\n W[\"client/routes/page.tsx<br/>(browser)\"] -->|\"Server.REST.likes.like()\"| G[\"shared/server.ts<br/>(generated typed client)\"]\n G -->|\"HTTP request\"| S[\"server/routes/Likes.ts<br/>(your @rest route, in wasm)\"]\n S -->|\"read / write\"| DB[(\"ToilDB\")]\n S -->|\"typed response\"| W\n```\n\nYou write both ends in TypeScript, and the generated `shared/server.ts` in the middle keeps them in sync.\n\n## Two rules to keep in mind\n\nThese two facts explain most of how toiljs backends behave. They are covered in depth later, but it helps to meet them now.\n\n1. **The server runs one fresh instance per request.** Every request gets a brand-new copy of your `.wasm`, and its memory is wiped when the request ends. So a normal variable you set in one request is gone by the next request. Anything that must survive (accounts, counters, posts) has to go into **ToilDB** or another store. Nothing in a plain module-level variable persists.\n\n2. **The server is not Node.js.** toilscript compiles a strict subset of TypeScript, so you cannot `import` an arbitrary npm package into `server/` or use Node APIs like `fs`. Instead, toiljs gives you built-in globals for the common needs: `crypto`, cookies, email, the database, and more. See [Types](../concepts/types.md) and [Decorators](../concepts/decorators.md) for the details.\n\nThe client side, by contrast, is normal React. You can use React libraries and browser APIs there as usual.\n\n## The getting-started path\n\nWork through these pages in order:\n\n1. **[Installation](./installation.md)**: check your Node.js version and install the toiljs command-line tool.\n2. **[Create a project](./create-project.md)**: scaffold a new app and see what you get.\n3. **[Project structure](./project-structure.md)**: a tour of every folder and file, and where each one runs.\n4. **[Your first app](./first-app.md)**: build a tiny feature end to end, a page that calls a backend route and reads and writes one piece of ToilDB data.\n5. **[Migrating an existing app](./migrating.md)**: bring a React app you already have into toiljs.\n6. **[Deploy](./deploy.md)**: build for production and self-host it, and how the managed edge fits in.\n\n## Related\n\n- [Documentation home](../README.md)\n- [The CLI reference](../cli/README.md)\n- [Frontend overview](../frontend/README.md)\n- [Backend overview](../backend/README.md)\n- [Database overview](../database/README.md)\n- [Compute tiers (where code runs)](../concepts/tiers.md)\n",
51
+ "getting-started/typescript7.md": "# TypeScript 7 toolchain migration\n\nThe framework, CLI, client, Vite builds, and declaration output use TypeScript 7.0.2 or newer\nwithin major 7. The WebAssembly backend remains the toilscript dialect and is checked by the\ntoilscript compiler during `toiljs build --server`.\n\n## Existing applications\n\n1. Upgrade toiljs and run `toiljs doctor --fix`. Reinstall dependencies after the manifest changes.\n2. Replace `eslint.config.js` with `oxlint.config.ts` importing the default from `toiljs/oxlint`.\n3. Install `oxlint` and `oxlint-tsgolint`; set the lint script to `oxlint --type-aware client`.\n4. Run `npm run build`, `npm run typecheck`, `npm run lint`, and `toiljs doctor`.\n\nThe new preset retains the former effective ESLint rules and severities. Native Oxlint handles\nsyntax checks and Fast Refresh. `oxlint-tsgolint` handles the strict semantic rules. React\nCompiler checks use the React Hooks JavaScript plugin inside Oxlint, and the padding rule uses\nthe Stylistic plugin. Their ESLint peer dependency is a plugin dependency; no ESLint runner or\nTypeScript ESLint parser is used.\n\nThe custom `toiljs/no-uint8array-tostring` rule queries the TypeScript 7 native checker through\nits synchronous API. It follows aliases, unions, intersections, generic constraints and base\nclasses, and permits deliberate `toString()` overrides. It uses the current lint input, including\neditor buffers. It does not infer byte types from variable names or annotations alone.\n\n## Additional project rules\n\nMove project overrides into the Oxlint configuration. Use `typescript/` for the native TypeScript\nrules, `react/only-export-components` for Fast Refresh, and `react-hooks-js/` for the React Hooks\nplugin. The custom byte-array rule is `toiljs/no-uint8array-tostring`; the padding rule is\n`style/padding-line-between-statements`. Keep `options.typeAware: true` or pass `--type-aware`.\n\n```ts\nimport toiljs from 'toiljs/oxlint';\nimport { defineConfig } from 'oxlint';\n\nexport default defineConfig({\n ...toiljs,\n options: { ...toiljs.options, typeAware: true },\n rules: {\n ...toiljs.rules,\n 'typescript/no-floating-promises': 'error',\n },\n});\n```\n\nTypeScript ESLint parser services are not available to Oxlint JavaScript plugins. Custom semantic\nplugins must use the native TypeScript API, as the bundled byte-array rule does. Doctor reports\nincomplete lint migration; it does not overwrite a custom ESLint configuration.\n\n## Editor and compiler configuration\n\nFollow [editor setup](./installation.md#editor-setup) to install Oxc in VS Code or WebStorm.\nVS Code uses `\"oxc.typeAware\": true`; WebStorm also needs **Settings → Tools → Oxlint → Enable\ntype aware rules** checked, because its editor setting overrides the project config.\n\nIn VS Code, enable the TypeScript native language server (`js/ts.experimental.useTsgo`).\nRemove legacy `typescript.tsdk` settings and JavaScript language-service\nplugins. The native language server does not run toilscript's old `ts-plugin.cjs`; use the\nWebAssembly build for server dialect diagnostics.\n\nRemove compiler options deleted in TypeScript 7, including `baseUrl`. When removing `baseUrl`,\nrewrite relative `paths` targets relative to the tsconfig file so resolution stays the same.\nDoctor uses the native compiler to check inherited compiler options, including removed options.\nRun `tsc --noEmit` to check the application types too.\nThe shipped client preset already uses bundler module resolution and explicit relative paths.\n\n## Generated clients and metadata\n\nThe build preserves `ArrayBuffer` ownership in the generated RPC fetch helper. Regenerate\n`shared/server.ts` with `toiljs build`; do not edit it manually. Metadata parsing supports literal\nobjects and arrays, `as const`, `satisfies`, and nested literal values without executing routes.\nParse errors fail the build rather than silently dropping SEO tags.\n\n## Framework API documentation\n\n`npm run docs` builds the framework and writes HTML reference pages and `api.json` to\n`build/docs/`, using the TypeScript 7 native checker. The hand-written guides under `docs/`\nremain the source for generated application documentation. TypeDoc and API Extractor are no\nlonger dependencies of the framework toolchain.\n",
51
52
  "introduction/design-principles.md": "# Why toil is built this way\n\ntoil is opinionated on purpose. The purpose is one thing: reach top-tier infrastructure and ship it as the **default**. AAA-grade tech is normally something you assemble from a dozen vendors and keep alive with a team that understands distributed systems. toil makes the good version the version you get out of the box. One framework, zero configuration. A solo builder starts from the same baseline as a funded team.\n\nEvery decision on this page traces back to that goal. Below is the rubric toil grades itself against, and the choices that rubric forces.\n\n## How RSG grades an app\n\nRSG stands for Resilience and Scale Grade. It is toil's internal rubric for how resilient and scalable an app really is, scored as a single letter from **AAA** down to **D**. It grades nine axes. One rule runs the whole thing: **your grade is your weakest axis**. Never the average, never the best. To earn AAA, all nine axes must be AAA at once.\n\nA globally edged frontend on a single-region database is capped by the database. A worldwide system running slow code is capped by latency. The lowest column sets the grade, every time.\n\nThat rule kills the most common lie in this space: calling a system \"global scale\" because the read path is global, while the write path is one box in one region. Averaging lets the strong axis hide the weak one. The minimum refuses to.\n\nRSG is not an external certification, and no auditor issues it. It is a mirror the team holds up to find the weakest link and fix it first. The full rubric lives at the repository root in [`RSG.md`](../../RSG.md).\n\n## What the bar forces\n\nAiming for AAA as the default is not a slogan. It forces a specific set of decisions, and they are why toil looks the way it does.\n\n### The strong version is the default\n\nOn most stacks the resilient path is a paid tier and the secure path is a checklist for later. toil flips that. The strong version is what you get before you configure anything.\n\n- **Post-quantum login in about one line.** Enable auth and a password is stretched through an OPRF (ristretto255) and Argon2id into a deterministic ML-DSA-44 keypair. Only the public key ever leaves the device. Login runs ML-KEM-768 mutual auth, so the client verifies the server too. The password never reaches the server in a usable form. ML-DSA and ML-KEM are the NIST-standardized algorithms built to survive a quantum computer. You do not assemble any of it. You switch it on.\n- **Asset tamper-proofing is on by default.** Every shipped asset carries SHA-384 Subresource Integrity plus importmap integrity. A modified script is rejected by the browser instead of run. There is nothing to remember to turn on.\n\n### Everything is built in and first-party\n\nAuth, [ToilDB](../database/README.md), email, rate limiting, analytics, realtime streaming, background jobs, materialized views, scheduled jobs, sessions and cookies, an environment and secrets store. toil builds and owns all of it. This is the Dependencies axis, where zero third-party code on the critical path is what earns AAA. Nothing you cannot inspect or fix sits between a request and its response.\n\n### One framework instead of ten vendors\n\nYou write a React frontend and a TypeScript backend in one project. toil runs both, plus the database, near your users worldwide. The browser calls the backend through a generated, fully typed `Server` client. There is no hand-written fetch and no drift between the two halves. One project, one deploy, one mental model. The alternative is a frontend host plus an API host plus a database vendor plus an auth vendor plus a queue, each glued to the next.\n\n### Modern foundations\n\ntoil bets on current technology instead of propping up old tooling.\n\n- **WebAssembly backends.** Your TypeScript compiles to a small, sandboxed WASM module. Because the sandbox is real, one edge box safely runs many tenants at once. That multi-tenancy is what makes running near everyone affordable.\n- **Post-quantum crypto** as the login default, not classical crypto you swap out later.\n- **End-to-end types** from the generated `Server` client. A backend change that breaks the frontend is a compile error, not a production 500.\n- **QUIC and WebTransport** carry realtime ([@stream](../concepts/tiers.md)).\n\n### Honest limits\n\ntoil grades itself on honesty, so the docs state the limits plainly.\n\n- **Distributed writes** are real in design and tested in the core. Every key has one home region that orders its writes, while data replicates outward for fast local reads (see [How toil is distributed](./distributed.md)). But live multi-region deployment is configuration-gated, not on by default, and the local dev database is a single in-process store. toil is built to distribute writes worldwide, and the mechanism is real. The claim is not that every app is already running a live global write cluster today.\n- **Compute tiers.** The per-request edge (L1) is live and real. Regional, continental, and global-daemon tiers are opt-in and deployment-gated, not always-on for every app.\n- **Analytics** is real on the edge. The dev server returns sample data.\n- **Auth secrets** ship as clearly insecure dev placeholders so `toiljs dev` just works. A real deployment must set its own.\n- toil is younger than the incumbents, and its integration catalog is smaller. [vs other frameworks](./vs-other-frameworks.md) covers when not to reach for it.\n\n## The nine axes\n\nEach axis names a way a system can be weak. Each row is the single design choice toil makes so that axis cannot be the thing that caps it.\n\n| RSG axis | What it grades | The toil design choice that hits it |\n| --- | --- | --- |\n| **Topology + distribution** | How close your code runs to users, and in how many places | Edge compute: frontend and backend both run on nodes next to users, worldwide, not in one origin region. |\n| **Availability** | What survives a failure | Cross-region failover with no single point of failure, so losing a node or a region does not take the app down. |\n| **Data path** | Where data is read and *written* (the hard one) | ToilDB's per-key-home model distributes the **writes**, not just the reads. See [How toil is distributed](./distributed.md). |\n| **Delivered p99 latency** | The end-to-end time the user actually feels | An allocation-free hot path, measured rather than assumed, so the response is fast for real, not just on paper. |\n| **Program performance + architecture** | Hot-path code quality and cost per request (no brute-forcing latency with a big server bill) | No blocking work on the request path; speed comes from the code, not from overprovisioning. |\n| **Dependencies** | How much of your critical path you own (zero third-party on it = AAA) | An owned, batteries-included stack: nothing third-party sits on the critical request path for you to be unable to inspect or fix. |\n| **Security** | How hard the system is to break, and how bad a breach would be | Post-quantum password login, sandboxed WebAssembly backends, and Subresource Integrity on every asset. See [Security](../concepts/security.md). |\n| **Client performance + reach** | How well the shipped app runs on old and low-end devices as data grows | A lean React client: a small bundle and linear-or-better hot paths, so it stays smooth on weak hardware, not just new flagships. |\n| **Modern stack + compatibility** | Current foundations, *with* graceful fallback for older clients | WebAssembly backends, post-quantum auth, and QUIC/WebTransport realtime, negotiating down cleanly so a client one version behind still works. |\n\n## Why every axis has to hold\n\ntoil is opinionated because being AAA on every axis at once forces these choices. It delivers them as the default, so you do not have to earn each one by hand. You cannot reach the top grade with a fast frontend on a centralized database. You cannot reach it with a global system running slow code, or a modern stack that only works on the newest browser. The weakest-link rule closes every one of those escape hatches. Any framework that lets a single axis slip is, by its own honest scoring, not AAA. Holding all nine at once, out of the box, is why toil looks the way it does.\n\n## Related\n\n- [How toil is distributed](./distributed.md): the data-path axis in depth, and why distributing writes is the hard one.\n- [What makes toil hyper-scalable](./hyperscale.md): the topology, latency, and program axes in practice.\n- [Security](../concepts/security.md): the security axis and its hard caps.\n- [vs other frameworks](./vs-other-frameworks.md): the honest \"when not to use toil\".\n- [`RSG.md`](../../RSG.md) at the repository root: the full rubric, the internal mirror this page summarizes.\n",
52
53
  "introduction/distributed.md": "# How toil is distributed\n\nSpreading a website's reads across the world is easy. Spreading its writes is hard, and almost nobody solves it. That is why most \"global\" apps are only global for reading. This page covers the problem and how ToilDB, the database built into toil, is built to distribute the writes.\n\n## Why writes are the hard part\n\nA read changes nothing, so you copy your data to servers worldwide and let each user read the nearest copy. Every copy agrees. A reader in Tokyo and one in Paris both get a fast, local answer.\n\nA write is a change, and two writes to the same key can collide. A counter says `10`. Tokyo and Paris both read `10`, both add one, both write `11`. The real answer was `12`. One add vanished, with no error. That is a **write conflict**.\n\nYou could make every copy agree before accepting a write. The network, though, will eventually split into a **partition**. During a split the **CAP tradeoff** says you keep only two of three: consistency, availability, and partition tolerance. You either refuse the write, which is correct but unavailable, or accept it on one side and reconcile later, which is available but briefly inconsistent. Distributing writes is a tradeoff to design around. There is no patch that removes it.\n\n## The usual fix: one write database\n\nSo nearly every stack keeps **one** primary write database in **one** region and spreads only read replicas worldwide. Every write funnels to that one box, one at a time, so conflicts cannot happen.\n\nIt is a reasonable choice. It also hides two costs the [RSG rubric](./design-principles.md) flags on its data-path axis:\n\n- **Far writes are slow.** Post from Tokyo to a primary in Virginia, and the write crosses the planet and back before anything saves. The page loaded locally. The write did not.\n- **The primary is a single point of failure.** One region holds every write. If it has a bad day, nothing anywhere can change.\n\nThe read path is global. The write path is one machine in one city. Under RSG's weakest-link rule, that single data path caps the whole system.\n\n## ToilDB's model: one home per key\n\nToilDB gives **every key its own home**. A key is the label you store data under: a user id, a username, a room name. See the [database overview](../database/README.md). Each key gets one home: the single source of truth that orders its writes.\n\nTwo things follow from that:\n\n- **Writes to one key are safe.** Every write to a key travels to that key's home, which **serializes** them: it applies them one at a time, in order. Both counter adds are ordered at the counter's home, so the result is `12`. No global lock over the whole database.\n- **Writes spread worldwide.** Different keys get different homes, so total write load spreads out. Tokyo users' data can home near Tokyo, Paris users' near Paris. No single box carries every write, so no single bottleneck.\n\n```mermaid\nflowchart TB\n WA[\"write user:aiko\"] --> KA[\"home near Tokyo<br/>key: user:aiko\"]\n WB[\"write user:bruno\"] --> KB[\"home near Paris<br/>key: user:bruno\"]\n WC[\"write room:general\"] --> KC[\"home near Ohio<br/>key: room:general\"]\n KA -.->|\"replicate for<br/>local reads\"| KB\n KA -.-> KC\n```\n\nReads stay local. Each key still replicates outward, so a reader anywhere gets a nearby copy. Those copies are **eventually consistent**. For a brief moment after a write lands at the home, a far read can lag before it catches up. That moment is usually a few milliseconds, and for almost all app data it is invisible. The [database overview](../database/README.md) has the full picture.\n\nA shared formula decides which location owns a key. It is called rendezvous hashing, and every node computes it the same way, so any node routes a write to the right home with no central coordinator. A key's home can move to follow demand, without rehashing the database.\n\n## The seven families\n\nOne \"home orders the writes\" rule fixes the counter. Different jobs, though, want different guarantees. So ToilDB ships **seven families**. Each is a collection type tuned for one shape of data, and each exposes only the operations that are safe and fast for it.\n\n| Family | What it gives |\n| --- | --- |\n| [Documents](../database/documents.md) | A record you look up by id |\n| [Counters](../database/counters.md) | Conflict-free tallies: adds from anywhere merge, no lost updates |\n| [Unique](../database/unique.md) | A one-of-a-kind claim (a username); the home picks exactly one winner |\n| [Capacity](../database/capacity.md) | Limited stock (tickets); reserve/confirm/cancel holds prevent overselling |\n| [Events](../database/events.md) | An append-only log in one agreed order |\n| [Membership](../database/membership.md) | Sets of who belongs to what |\n| [View](../database/views.md) | A read-optimized result a background job builds |\n\nDistributing writes has no single answer. Each family is the right tool for one shape of data.\n\n## The machinery underneath\n\nThe per-key-home model only works if a lot of unglamorous machinery runs reliably across a flaky network. ToilDB owns all of it:\n\n- Per-key **placement** and safe **rehoming**: a rising epoch plus a fencing token, so the old owner stops the instant the new one takes over.\n- Ordered **cross-region replication** with per-stream cursors that detect and backfill gaps.\n- **Idempotent apply**, so a redelivered write cannot double-count.\n- **Capacity escrow** and **tenant quotas**.\n- **Failover** with snapshot re-seeding for a cell that has fallen too far behind.\n\nGetting all of these right at once is why truly distributed websites are rare.\n\n## What is real today\n\nThis is the hard part that almost nobody ships, so here is a straight account of where it stands.\n\nThe per-key-home model and all its core logic are **built and tested**: placement, rehoming, replication, idempotent apply, escrow, quotas, failover. This is running code, and it passes its tests.\n\nThe **live multi-region deployment** is not on by default. Wiring many real regions into one running mesh (the WAN routing) and the [ScyllaDB](https://www.scylladb.com/) storage that backs a production cluster are **configuration-gated**. You switch them on for a real deployment. You do not get a live global write cluster automatically. The full database-level **leader fencing** on the write path is also still landing, and a host-side leader gate is the current version. The design is settled. What remains is the last-mile host wiring.\n\nOn your laptop, `toiljs dev` runs a single **in-process** database. Everything homes in one place, so your code behaves exactly as it will worldwide. The distribution only spreads out once you deploy with the multi-cell backing configured. You write your app the same way either way.\n\ntoil is **built** to distribute writes worldwide. The mechanism runs today. Turning it on across live regions is a deployment step, not a rewrite of your app.\n\n## Related\n\n- [The database (ToilDB)](../database/README.md): families, keys and values, and eventual consistency in depth.\n- [Compute tiers](../concepts/tiers.md): where your code runs, the compute side of the same story.\n- [What makes toil hyper-scalable](./hyperscale.md): the mechanisms that let one small program serve the planet.\n- [Why toil is built this way (the RSG bar)](./design-principles.md): the rubric behind the data-path axis.\n",
53
54
  "introduction/how-it-works.md": "# How toil works\n\nThis page walks one app from source to response. What you write, what the build produces, and what happens when a request arrives. Every term is defined where it first appears. You configure none of the machinery below. This page just explains it.\n\n## The client and the server\n\nA toil app is one project. Your code lives in two folders.\n\n- **`client/`** is a **React** frontend built with Vite. File-based routing, data loaders, the usual components. It runs in the user's browser.\n- **`server/`** is your backend, written in **TypeScript with decorators**. Mark a class `@rest` for HTTP endpoints, `@service` for typed RPC, `@stream` for realtime, `@daemon` for background jobs, `@database` or `@collection` for stored data, `@auth` or `@user` for login. There are no servers to provision and no routes to wire by hand. The decorator is the wiring.\n\nThat is the whole surface. One repo, one language family, one `toiljs dev`.\n\n## What the build produces\n\n`toiljs build` turns that one project into three kinds of output. The browser and the edge need different things.\n\n**The client bundle.** Your React app, plus any pages toil renders ahead of time. It ships as ordinary web files that run in the browser: HTML, JS, CSS, images. Every asset is fingerprinted with SHA-384 Subresource Integrity, so the browser refuses a file that was tampered with in transit.\n\n**The server WASM.** Your `server/` backend, compiled by the **toilscript** compiler into a small sandboxed **WebAssembly** module. It runs on the edge, not in the browser and not in Node. The build emits three artifacts, one per compute tier.\n\n| Artifact | Holds | Runs |\n| --- | --- | --- |\n| `release.wasm` | the hot request path (`@rest`, `@service`) | on every request |\n| `release-stream.wasm` | realtime handlers (`@stream`) | for the life of a connection |\n| `release-cold.wasm` | background and scheduled jobs (`@daemon`) | on a timer, worldwide-singleton |\n\n**The generated typed client, `shared/server.ts`.** toil reads the shape of your backend and generates a small browser-side client from it. Your React code calls a normal async function, `Server.REST.*` or `Server.Stream.*` or `Server.<service>`, and the types line up end to end. Rename a field on the server and the frontend stops compiling until you fix it. No hand-written `fetch`, no drift.\n\nA word on **WebAssembly**. It is a compact binary format that runs at close to native speed with no interpreter warm-up. It runs inside a **sandbox**: a locked box that cannot open files, reach the operating system, or make network calls on its own. The guest touches the outside world only through a small fixed set of functions the host hands it. Read the request, build a response, query the database. That narrow surface is what makes it safe to pack many apps onto one shared machine. ([Why that matters for scale](./hyperscale.md).)\n\ntoilscript is a strict TypeScript dialect: explicit value types, no `any`, no runtime `RegExp`. The [decorators reference](../concepts/decorators.md) covers the full set.\n\n## The edge nodes\n\nYour WASM does not run on a laptop or in one central data center. It runs on the **edge**: a fleet of Rust runtime nodes spread across many cities. Each user is served by whichever node is physically closest. There is no **origin server**, no single far machine every request funnels back to. Your backend and its data are already at the edge.\n\nTwo things make this work at the density it needs.\n\n- **Multi-tenant on shared boxes.** Each app is a tiny sandboxed module, so one edge box safely runs many apps at once. That shared density is what makes putting compute near everyone affordable instead of a luxury.\n- **A custom userspace datapath.** The node handles network packets itself in userspace, multi-queue, bypassing the operating system's network stack. This keeps the per-request cost low as traffic grows. **QUIC** and **WebTransport** ride on top to power realtime `@stream` connections.\n\n## What happens on a request\n\nA request never leaves the neighborhood. It lands on the nearest edge node, and everything happens right there.\n\n```mermaid\nsequenceDiagram\n participant U as User's browser\n participant E as Nearest edge node (Rust)\n participant W as WASM host\n participant G as Your guest module\n participant DB as ToilDB (local copy)\n\n U->>E: Request over the network\n alt Page or static asset\n E-->>U: Prerendered / SSR page or file\n else Dynamic API call\n E->>W: Decode raw bytes into a Request\n W->>G: Call your handler\n G->>DB: Read / write nearby data\n DB-->>G: Result\n G-->>W: Response object\n W-->>E: Encode to bytes\n E-->>U: Response\n end\n```\n\n1. **The request lands on the closest edge node.** The network routes the user there automatically.\n2. **Page or dynamic call.** A prerendered page, a server-rendered page, or a static asset is served directly by the edge, which never wakes your backend. This is the fast path for most page loads.\n3. **Otherwise the host runs your guest.** The **WASM host**, trusted Rust, decodes the raw bytes into a `Request`. It calls into your **guest module**, your sandboxed WASM, at its single entry point, which routes to the handler you wrote.\n4. **Your handler reads and writes locally.** When it needs stored data it talks to [ToilDB](../database/README.md), which keeps a copy right there at the edge. No ocean crossing.\n5. **Your handler returns a `Response`.** The host encodes it back to bytes and the edge sends it to the browser.\n\nThe mental model for your backend is a function of the request. Bytes in, bytes out, one request at a time.\n\n### Why the backend is stateless\n\nA fresh copy of your handler serves each request. The next request might land on a node on the other side of the planet, so anything you set on an instance field does not survive. This is deliberate. Interchangeable copies with nothing to coordinate are what let the backend scale worldwide by adding more nodes. When you need something to persist, write it to ToilDB. See the [backend overview](../backend/README.md#stateless-by-default).\n\n## The database, ToilDB\n\nState lives in [ToilDB](../database/README.md), toil's own database, replicated next to your code at the edge. It offers seven purpose-built families: Documents, Unique, Counter, Events, Membership, Capacity, View. You pick the shape that fits instead of forcing everything into one table.\n\nThe hard part is **distributed writes**. Every key has one **home region** that orders its writes, while the data replicates outward so reads stay fast and local everywhere. Most stacks distribute reads but pin writes to one region. toil is built to distribute both.\n\nWhere this stands today. The home-region model and its core logic are real and tested. A live multi-region deployment, meaning WAN routing over the ScyllaDB backing, is configuration-gated. It is not switched on for every app by default. A far read can also lag its home by a few milliseconds, the eventual-consistency trade-off. Your local `toiljs dev` database is a single in-process store. The [distributed writes page](./distributed.md) covers the write model and its trade-off in full.\n\n## The four compute tiers\n\nNot all backend code has the same lifespan. toil sorts it into four tiers, and the build assigns each piece from its decorator automatically.\n\n| Tier | What runs here | Status |\n| --- | --- | --- |\n| **L1** | the stateless per-request hot path (`@rest`, `@service`) | live, the default |\n| **L2 / L3** | longer-lived regional and continental work, `@stream` realtime | opt-in, deployment-gated |\n| **L4** | a single worldwide `@daemon` for background and scheduled jobs | opt-in, deployment-gated |\n\nMost apps live entirely on **L1**, which is real and running today. L2 through L4 are built and opt-in, not always-on for every app. Full detail is on the [tiers page](../concepts/tiers.md).\n\n## The parts of a running app\n\nFive parts make up a running toil app. You have now met all of them.\n\n| Piece | What it is | Where it runs |\n| --- | --- | --- |\n| **React client** | your frontend UI, the client bundle from the build | the user's browser |\n| **toilscript backend** | your `server/` code compiled to sandboxed WASM | the edge |\n| **The edge node** | the Rust runtime that terminates the connection, serves pages, and runs your WASM | servers in many cities |\n| **ToilDB** | the database with distributed writes, replicated next to your code | the edge |\n| **The four tiers** | where and for how long a piece of backend code lives | L1 nearest, up to L4 worldwide |\n\nNone of these need configuring to get the good version. You write React and decorated TypeScript. The build, the edge, and the database come as the default, not a stack you assemble and babysit.\n\n## Related\n\n- [Backend overview](../backend/README.md): the request/response model and the sandbox in depth.\n- [The database (ToilDB)](../database/README.md): families, home regions, and eventual consistency.\n- [Compute tiers](../concepts/tiers.md): L1 request, L2/L3 stream, L4 daemon.\n- [Realtime streams](../realtime/streams.md) and [background daemons](../background/daemons.md): the other two artifacts.\n- [What makes toil hyper-scalable](./hyperscale.md): why this design serves the planet cheaply.\n",
@@ -55,7 +56,7 @@ export const TOIL_DOCS = {
55
56
  "introduction/modern-stack.md": "# The modern stack: what toil gives you that others do not\n\nMost frameworks give you a way to write code, then send you shopping. A database, an auth provider, email, a rate limiter, analytics, a realtime service, a job runner. Each one is its own account, its own bill, and its own SDK, and you keep them all in sync.\n\ntoil owns those parts instead. They ship in the framework, they are toil's own code, and they run from the first line with no configuration. Nothing third-party sits on your critical path.\n\nThe good version is the default version. A solo builder gets the same baseline a funded team would rent from ten vendors, with nothing to assemble or babysit. This page is the full catalog. For how the edge and worldwide distribution work, see [How toil works](./how-it-works.md) and [How toil is distributed](./distributed.md).\n\n## Built-in backend features\n\nYour TypeScript backend declares what it needs with a decorator or a one-line config flag. toil provides the machinery.\n\n| Feature | What it is | Why it matters |\n| --- | --- | --- |\n| [Post-quantum auth](../auth/README.md) | Password login via `server: { auth: true }`. The password is stretched (OPRF + Argon2id) into an ML-DSA-44 keypair in the browser; only the public key is sent; login runs ML-KEM-768 mutual auth so the client also verifies the server. | Real post-quantum cryptography (the NIST-standardized algorithms), in about one line. The password never reaches the server in a usable form, so a breached server yields nothing replayable, and there is no identity vendor to rent. |\n| [ToilDB](../database/README.md) | A global database with no connection string and seven purpose-built families. Its differentiator is distributed writes: every key has one home region that orders its writes, while data replicates outward for fast local reads. | Distributes writes, not just reads, so a thousand servers can write at once with no single-region bottleneck. This is the hard part almost nobody does (trade: eventual consistency, a far read can lag a few ms). |\n| [Email](../services/email.md) | `EmailService.send(...)`, or a reusable `EmailTemplate` with `{{placeholder}}` bodies, through a provider you configure once. | Verification codes, resets, and receipts are one validated, per-tenant-capped call, off the worker while the provider replies. |\n| [Rate limiting](../services/ratelimit.md) | `@ratelimit` on a route, counted at the edge in an exact cross-worker limiter keyed on the caller's network address. | Over-limit clients are rejected before your code runs, blunting brute-force and spam. |\n| [Analytics](../services/analytics.md) | The `Analytics` global reads your site's own counters: `self()` for a snapshot, `series(metric, range)` for a graph. | A real usage dashboard with no extra code and no analytics vendor. |\n| [Realtime streaming](../realtime/README.md) | A `@stream` class with `@connect` / `@message` / `@close` / `@disconnect` hooks, opened from React with `useChannel`. WebTransport (over QUIC) in production, WebSocket in dev. | A resident instance stays alive per connection and remembers state next to the user: chat, presence, live progress. |\n| [Background jobs (`@daemon`)](../background/daemons.md) | One global background worker on a schedule (interval or cron), with lease-based failover so exactly one instance runs worldwide. | Nightly jobs run once globally, with no cron server, queue, or leader election to run yourself. |\n| [Scheduled jobs (`@scheduled`)](../background/daemons.md) | A method that fires on an interval or cron expression, driven by the same lease so it runs once across the fleet. | Recurring work (cleanup, digests, syncs) without standing up a scheduler. |\n| [Materialized views (`@derive`)](../background/derive.md) | A function that re-runs when its source data changes and writes the result into a View family. | Rollups and denormalized read models stay fresh automatically, so reads are cheap without a hand-rolled pipeline. |\n| [Sessions and cookies](../services/cookies.md) | No-import cookie read/set, signed or encrypted, plus the session layer that backs `@auth`. | Login state and small per-user data without a session store to provision. |\n| [Environment and secrets](../services/environment.md) | A per-tenant environment store for config and secrets, read at the edge, never baked into the shipped WASM. | Your compiled program carries no credential; secrets live in one place, disjoint from public config. |\n\n### The seven ToilDB families\n\nOne database, seven shapes. Each family is tuned to its own access pattern instead of forced out of a single table model.\n\n| Family | For |\n| --- | --- |\n| [Documents](../database/documents.md) | General structured records. |\n| [Unique](../database/unique.md) | Uniqueness constraints (one email, one handle). |\n| [Counter](../database/counters.md) | High-throughput increments (views, likes, quotas). |\n| [Events](../database/events.md) | Append-only logs and feeds. |\n| [Membership](../database/membership.md) | Set membership and relationships (who is in what). |\n| [Capacity](../database/capacity.md) | Bounded resources and seat/slot allocation. |\n| [View](../database/views.md) | Read models materialized by `@derive`. |\n\n## Built-in frontend features\n\nThe React client is Vite-fast and typed end to end. The pieces that usually take a build config, a fetch layer, and an SEO plugin are already wired.\n\n| Feature | What it is | Why it matters |\n| --- | --- | --- |\n| [File routing](../frontend/routing.md) | Routes are files under `client/routes/`. The tree is your URL map. | No router config to hand-maintain; add a file, get a route. |\n| [Loaders with revalidation](../frontend/data-fetching.md) | A route `loader` fetches its data before render and revalidates on demand. | Data arrives with the page, not after a client-side waterfall. |\n| [Two rendering paths](../frontend/rendering.md) | Build-time prerender for SEO by default, plus opt-in edge SSR with `export const ssr = true`, both hydrated with `hydrateRoot`. | Static-fast pages where you can, live server rendering where you need it, one codebase. |\n| [Typed `Server` client](../frontend/data-fetching.md) | A generated client (`Server.REST.*`, `Server.Stream.*`, `Server.<service>`) so the browser calls the backend with end-to-end types. | No hand-written fetch and no drift: rename a field on the server and the frontend stops compiling until you fix it. |\n| [Image with LQIP](../frontend/images.md) | A built-in `Image` component with blur / low-quality placeholders and reserved aspect ratio. | Fast, layout-stable images with no CLS and no separate image service. |\n| [Metadata and SEO](../frontend/metadata.md) | Per-route title, description, canonical, and Open Graph (including `og:image`), baked into the served HTML head. | Correct SEO and share cards in view-source, not assembled by JavaScript after load. |\n| [Page search](../frontend/search.md) | A static index of each route's title, description, keywords, and Open Graph, generated at build. | In-app search with no search vendor (see the caveat below). |\n| [SHA-384 SRI](../concepts/security.md) | Subresource Integrity plus importmap integrity on every shipped script, preload, and stylesheet, across the whole module graph. | A tampered asset simply does not run, even if a CDN or cache hop is compromised. |\n\n## toil versus a typical stack\n\nEverything above is toil's own code on your critical path. You can inspect it, patch it, and secure it. The table below draws the difference.\n\n| Capability | toil (built in, zero setup) | Typical stack (you assemble it) |\n| --- | --- | --- |\n| Database | ToilDB: global, distributed writes, seven families | A managed database, usually single-region for writes |\n| Auth | Post-quantum login; the server holds no password | A rented identity provider or hand-rolled hashing |\n| Email, rate limiting, analytics | Built-in primitives, one call each | A separate SDK or vendor per capability |\n| Realtime and background | `@stream`, `@daemon`, `@scheduled`, `@derive` | A realtime service plus a cron server, queue, and leader election |\n| Sessions, secrets, cookies | Owned globals, no store to provision | A session store and a secrets manager to run |\n| Frontend integrity and SEO | Automatic SHA-384 SRI, Image/LQIP, metadata, page search | Plugins and services bolted on per concern |\n| Critical-path ownership | The core is toil's own | A mix of vendors you cannot inspect or fix |\n\nOwned means the core of a working app is toil's. It does not mean outside services are banned. You can still call a payment provider or any other API.\n\n## Honest limits\n\ntoil grades itself on honesty. Read these before you count on anything.\n\n- **Distributed writes are built, live multi-cell is config-gated.** The home-region model and its core logic are real and tested. Live multi-region deployment (WAN routing, the ScyllaDB backing) is opt-in, not on by default. The local dev database is a single in-process store. toil is built to distribute writes worldwide. Not every app is running a live global write cluster today.\n- **Analytics is a dev stub locally.** It is real on the edge. The local dev server returns sample data.\n- **Auth secrets ship as dev placeholders.** The session HMAC, OPRF seed, and ML-KEM key are clearly insecure placeholders so `toiljs dev` just works. A real deployment must set its own. Per-tenant auto-generation at domain registration is the plan.\n- **Page search indexes static metadata only.** Routes whose metadata comes from a dynamic `generateMetadata` are not in the index.\n\n## Why these ship by default\n\nNone of this is an upgrade you unlock later. toil grades itself against [RSG](./design-principles.md), the Resilience and Scale Grade. Its one rule: your grade is your weakest axis, never the average. These built-in parts exist so no single axis quietly caps the whole.\n\nThe trades are real. ToilDB is not general SQL. The server language is a strict TypeScript subset. The catalog is younger than long-established platforms. Where those trades fit your project, the built-in stack earns its place. [Why toil](./why-toil.md) covers where it does not.\n",
56
57
  "introduction/README.md": "# Understanding toil\n\ntoil is one framework for a whole web app: the frontend, the backend, and the database, in a single project, all running close to your users.\n\nYou write React for the client and TypeScript for the server. toil compiles the server to WebAssembly and runs it at the edge, next to whoever is asking. Auth, database, email, realtime, and background jobs are already built in. Nothing to wire up, nothing to configure. A pizza site gets the same infrastructure a funded team would rent from ten separate vendors.\n\nThis section explains what that means and why it holds up. Start here, then follow the links at the end.\n\n## What toil is\n\nA toil project has three parts in one folder:\n\n- `client/` is your React app: file-based routing, data loaders, and a typed client for calling the server.\n- `server/` is your backend, plain TypeScript marked up with decorators like `@rest`, `@data`, and `@auth`. toil compiles it to a small sandboxed WebAssembly module.\n- ToilDB is your database. It is already there. No connection string, no instance to spin up.\n\nTypes tie the three together. Change a field on the server and the client stops compiling until you fix it. Edge deployment, post-quantum login, and asset tamper-proofing are on by default, not features you bolt on later.\n\n## The problem it solves\n\nA good web app should be fast everywhere, secure by default, and able to grow without a rewrite. Getting there is the hard part, and most of it has nothing to do with your actual product.\n\nYou assemble it from rented parts: a host, serverless functions, a database, an auth provider, email, a cache, a queue, analytics, a realtime service. Each is its own account, bill, and SDK, and you keep them all in sync. Under that sits a decade of legacy tooling and a node_modules folder heavier than your app. Worse, the fast and secure version is never the default. A CDN, careful caching, region tuning, hardened auth, current crypto: all of it is extra work, so most apps ship slower and less safe than they should.\n\ntoil deletes that assembly. One framework runs your frontend, backend, and database at the edge, next to users. Auth, email, realtime, and background jobs are built in and owned. Login is post-quantum out of the box. There is nothing to wire up and no infrastructure to reason about. The good version is the only version.\n\nDistributing writes worldwide is one of the hard problems it handles under the hood. Most stacks spread reads everywhere but pin every write to one region, so a user far from it waits for a round trip and that region is a single point of failure. ToilDB is built to distribute the writes too: every key has a home region that orders its writes while the data copies outward for fast local reads. You never set any of it up.\n\n```mermaid\nflowchart TB\n subgraph Assemble[\"The usual way: rent and wire the parts yourself\"]\n direction TB\n A[\"Your app\"] --> H[\"host\"]\n A --> F[\"functions\"]\n A --> D[\"database\"]\n A --> Au[\"auth\"]\n A --> Em[\"email\"]\n A --> Rt[\"realtime\"]\n A --> J[\"jobs\"]\n end\n subgraph Toil[\"toil: one framework, at the edge, by default\"]\n direction TB\n A2[\"Your app\"] --> T[\"frontend + backend + database + auth +<br/>email + realtime + jobs, built in and near users\"]\n end\n```\n\n## Where to go from here\n\nRead these in order. Each one builds on the last.\n\n1. **[Why toil, and who it is for](./why-toil.md)** What is wrong with a modern stack, who gains most from toil, and the honest cases against it.\n2. **[What comes built in](./modern-stack.md)** The full list of what toil owns and runs for you, and what it does not.\n3. **[How toil works](./how-it-works.md)** The whole path, from a React click through WebAssembly to ToilDB and back.\n4. **[Why it scales cheaply](./hyperscale.md)** How one small program can serve the whole planet without a per-app server bill.\n5. **[How toil distributes writes](./distributed.md)** One of the hardest problems in web infrastructure, and how ToilDB is built to solve it.\n6. **[toil next to other stacks](./vs-other-frameworks.md)** A fair comparison with Next.js, Rails, serverless, and the rest, wins and losses both.\n7. **[The bar toil holds itself to](./design-principles.md)** The RSG rubric, and its one rule: your grade is your weakest part.\n\n## The short answer\n\n- **Who it is for:** anyone shipping a real product who wants global speed without a platform team or ten stitched-together services.\n- **Why it is fast:** the code runs next to the user, with no trip to a distant origin.\n- **Why it is different:** the whole stack is built in and owned, so there is nothing to assemble, and it distributes writes worldwide, not only reads.\n- **Why it is safe:** the backend is sandboxed, passwords never reach the server in a usable form, secrets never ship in the code, and the browser checks every file it loads.\n\nReady to build? Jump to [Getting started](../getting-started/README.md).\n",
57
58
  "introduction/vs-other-frameworks.md": "# toil versus other stacks\n\nThis page compares toil to the stacks you already use, axis by axis. Every tool below is good at what it was built for. The aim is to be concrete about what you gain and what you give up. toil is younger than all of them, and that shows in a few rows.\n\ntoil trades a large, mature ecosystem for one owned framework where the fast, secure, global version is the default. You give up SQL, the Node package universe, and a big integration catalog. You gain a stack that is built in and yours, runs near users, ships post-quantum auth and end-to-end types with no setup, and distributes writes worldwide instead of pinning them to one region. Whether that trade fits your project is what this page answers.\n\n## The comparison table\n\n| Axis | A typical modern stack | toil |\n| --- | --- | --- |\n| Pieces on the critical path | A dozen rented vendors (host, functions, database, auth, email, queue, cache, analytics, realtime), each its own account, bill, and SDK | One framework you write in; the core services are toil's own, so nothing third-party sits on the critical path |\n| Where writes land | One primary region: reads go global, writes crawl back to that box | Every key has one home region that orders its writes, while data replicates outward for fast local reads (built to distribute; live multi-cell is configuration-gated) |\n| Server runtime | A Node process, container, or VM per app | A small sandboxed WebAssembly module; one edge box safely runs many apps (multi-tenant), which is what makes running near everyone affordable |\n| Client-to-server calls | Hand-written fetch/REST, types drift until something breaks in production | A generated, fully typed `Server` client with no raw fetch: change a field on the server and the frontend stops compiling until you fix it |\n| Auth | Bolt on a provider or roll your own | Post-quantum login (ML-DSA + ML-KEM) built in, enabled in about one line |\n| Getting to production | Assemble CDN, cache, regions, hardened auth, and CI yourself | Zero config: the good version is the default version |\n| Toolchain | A decade of bundler configs, transpiler shims, CommonJS-versus-ESM interop, and `node_modules` churn | Built on modern web tech (React + Vite client, one CLI), far less legacy to fight |\n| Database | Mature SQL: joins, ad-hoc queries, huge tooling | Seven purpose-built families (Documents, Unique, Counter, Events, Membership, Capacity, View): fast and distributed, but not relational |\n| Package ecosystem | The full npm and Node universe | A strict TypeScript subset with built-in globals; no arbitrary npm packages or Node APIs on the server |\n| Integration catalog | Large, mature, well documented | Smaller and younger |\n| Single-region app | Dead simple, nothing to distribute | Distribution you may not need |\n\nThe top rows lean toil's way. The bottom rows lean the incumbents' way. toil wins the structural axes: one owned stack, distributed writes, a multi-tenant sandbox, end-to-end types, and built-in modern auth. It concedes the ecosystem axes that maturity buys: SQL, the Node universe, and catalog size.\n\n## How toil compares to each stack\n\n**Next.js / Vercel.** Superb React DX and global edge reads. The backend, though, is still yours to assemble: a database, auth, email, a queue, and a realtime service, each rented and wired up on its own. Serverless cold starts add latency and per-invocation cost right when a spike hits, and writes still resolve against one primary region. toil keeps a React-first client but ships the whole backend built in, running near the data with no cold start.\n\n**Rails / Django.** Mature, productive, and batteries included, with a deep hiring pool. If one region suits you, this is a great and boring choice. The default shape is a single-region monolith with one primary that every write must reach. You can add replicas and standbys to scale, but distributed writes are not in the model.\n\n**Serverless functions (Lambda, Cloud Functions).** Elastic, stateless compute that scales to zero. It still sits in front of a central database, so a write burst bottlenecks on that store. Each cold invocation bills and lags on its own.\n\n**Edge runtimes (Workers, Deno Deploy).** The closest in spirit to toil's compute model: your code runs near users. The catch is everything you bolt on around it. The database you attach is usually single-region, and auth, email, realtime, and typed client calls are still yours to assemble. toil ships those built in, on a database designed to distribute writes.\n\nCloudflare Durable Objects and D1 are the closest mainstream analog to toil's idea, and credit is due. A Durable Object gives one object a single-writer home that orders its writes, the same shape as ToilDB's per-key home. The difference is packaging. With the edge-runtime approach you assemble the pieces yourself: runtime, object or database product, auth, email, and realtime. toil ships distributed writes, the seven database families, auth, email, streaming, and background jobs as one owned stack. Which you prefer is a real trade-off.\n\n**Backend-as-a-service (Supabase, Firebase).** The fastest thing to start with, and often the right call for a prototype. The convenience is a managed service on your critical path that you cannot inspect or patch, and writes still resolve against a primary.\n\n## What incumbents do better\n\ntoil does not win every axis. Some gaps are real today:\n\n- **Mature ecosystems.** More tutorials, more answered questions, more hosting options, and more people who already know the tool.\n- **SQL and joins.** If your data is relational and you live in ad-hoc queries and joins, a SQL database is the right tool. ToilDB is seven purpose-built families, not a relational engine (see the [database overview](../database/README.md)).\n- **The Node package universe.** The toil server is a strict TypeScript subset compiled to WebAssembly: no arbitrary npm packages or Node APIs, built-in globals instead. That is the price of the small, fast, multi-tenant sandbox.\n- **Bigger integration catalogs.** toil is younger, so the catalog of ready-made integrations is smaller. If your project is defined by a large existing integration catalog, that gap matters.\n- **Single-region simplicity.** If one region already fits your users, toil's distribution is effort you do not need.\n\nNone of these are permanent, and the right tool is the one that fits the job in front of you.\n\n## The trade toil makes\n\nMost stacks cap in the same place. The write path is one box in one region, or the critical path leans on services you rent and cannot fix. toil refuses both. It owns the whole stack and distributes the writes, so the structural caps are designed out. The limits left are the ones your own code sets.\n\nOne caveat, to keep the mechanism honest. The home-region model and its core logic are real and tested. Live multi-region deployment is configuration-gated rather than on by default: the WAN routing and the ScyllaDB backing are opt-in, and the local dev database is a single in-process store. Compute works the same way. Your code runs at the edge on every request, while the regional, continental, and global-daemon tiers are opt-in. So the honest claim is that toil is built to distribute writes worldwide and the mechanism is real. It does not mean every app is already running a live global write cluster today.\n\nWhether the trade fits your project is the checklist in [Why toil](./why-toil.md).\n\n## Related pages\n\n- [Why toil? Who is it for?](./why-toil.md): the problem toil solves and the honest cases where you should not use it.\n- [The modern stack](./modern-stack.md): the full, verified catalog of what is built in.\n- [How toil is distributed](./distributed.md): the mechanism behind distributed writes (every key's single home region).\n- [Why toil is built this way](./design-principles.md): the bar toil grades itself against.\n",
58
- "introduction/why-toil.md": "# Why toil? Who is it for?\n\ntoil is a full-stack framework. You write a React frontend and a TypeScript backend in one project, and toil runs both, plus a database, close to every user worldwide.\n\nShipping a fast, secure, global app normally takes a pile of rented services and a team that understands infrastructure. toil gives you that as the default: one framework, zero config, quantum-proof login, built on modern tech, running near your users from day one. The same setup serves a pizza site and a planet-scale app. Distributing writes worldwide is one of the hard things it handles for you, not the reason it exists.\n\n## The problem with modern stacks\n\nShipping a good app today means fighting your own infrastructure. It fights back in five ways.\n\n**Reads go global, writes go to one region.** Your pages load fast from caches worldwide. A write does not. A comment, a like, or an order has to crawl back to a single database in a single region. A user in Sydney writing to Virginia pays for the whole round trip. That one region is also a single point of failure for everyone.\n\n**You become a systems integrator by accident.** A production stack is a dozen rented services stitched together: a frontend host, serverless functions, a managed database, auth, email, a queue, a cache, analytics, realtime. Each has its own account, bill, SDK, and outage. Every one on your critical path is a black box you cannot inspect, patch, or secure. When it is slow, you are slow. When it is breached, part of you is breached.\n\n**It runs on yesterday's tooling.** The typical stack sits on a decade of legacy: bundler configs, transpiler shims, CommonJS-versus-ESM interop, framework churn, and a `node_modules` folder heavier than the app itself. You fight the toolchain and chase version bumps instead of building. The web platform moved on. The stack did not.\n\n**Every layer adds overhead.** Heavy runtimes, slow builds, serverless cold starts, oversized JavaScript in the browser, and a network hop between every service. Each layer adds latency and cost. The user feels it and you debug it.\n\n**Fast and safe are opt-in, never default.** Good performance and real security are things you assemble by hand: a CDN here, a cache there, careful data fetching, region tuning, hardened auth, current cryptography. Miss a piece and you are slow or exposed. The good version is always extra work, so most people ship the lesser one.\n\nA solo builder and a funded startup hit the same wall. Top-tier infrastructure means assembling and babysitting a lot of parts, so most settle for less.\n\n## What toil delivers\n\ntoil was built to reach real scale cheaply, and every design choice serves that. You get top-tier infrastructure by default: one framework instead of ten vendors, zero configuration, and no networking knowledge required. Quantum-proof login is on from the start. Four things make it work.\n\n### AAA-grade infrastructure by default\n\nTop-tier infrastructure on day one, on the smallest project, with zero setup. Your code runs at the edge near users worldwide. A global database is already there. Every shipped asset is tamper-proofed automatically with SHA-384 Subresource Integrity. Login is quantum-proof in about one line.\n\nThat login is real post-quantum cryptography. Your password is stretched into a signing key on your device and never reaches the server in a usable form. The handshake uses ML-DSA and ML-KEM, the NIST-standardized algorithms meant to survive a quantum computer. Most stacks bolt current crypto on by hand, if at all. Here it is the default.\n\nA pizza site and a planet-scale app start from the same baseline. AAA-grade is the actual bar toil grades itself against. See [design principles](./design-principles.md).\n\n### Everything built in, and owned\n\nAuth, database, email, rate limiting, analytics, realtime streaming, and background jobs are all built in, and all toil's own. Nothing third-party sits on your critical path.\n\nBecause they are one system, the parts already fit. You are not gluing ten SDKs together and hoping they agree. Full catalog: [The modern stack](./modern-stack.md).\n\n```mermaid\nflowchart TB\n subgraph assemble[\"Usual way: assemble ten vendors\"]\n direction TB\n A1[\"Your app\"] --> V1[\"host\"]\n A1 --> V2[\"functions\"]\n A1 --> V3[\"database\"]\n A1 --> V4[\"auth\"]\n A1 --> V5[\"email\"]\n A1 --> V6[\"rate limiter\"]\n A1 --> V7[\"analytics\"]\n A1 --> V8[\"realtime\"]\n A1 --> V9[\"jobs\"]\n end\n subgraph toil[\"toil: one owned stack\"]\n direction TB\n A2[\"Your app\"] --> T[\"edge + database + auth + email +<br/>rate limiting + analytics +<br/>streaming + jobs, built in\"]\n end\n```\n\nHonest boundary: owned means the core of a working app is toil's, not that outside services are banned. Call a payment provider or another API and you still can.\n\n### Modern tooling that just works\n\nBuilt on today's web platform, not a decade of legacy. TypeScript end to end, one repo, one deploy, wired by types. Change a field on the server and the frontend stops compiling until you fix it. The bug surfaces at your desk as a compile error, not in production.\n\nThe toolchain is set up for you: ESLint, Prettier with a plugin for toil's decorators, an editor plugin, one CLI, and a `doctor` command that fixes common problems in place. The docs are LLM-friendly too, so an AI assistant reads your current conventions instead of guessing. More in [The modern stack](./modern-stack.md).\n\n### Hyper-scale and distributed writes\n\nYour backend compiles to a tiny sandboxed **WebAssembly** module, a compact locked-down binary that runs at near-native speed. That is modern tech, not a heavyweight legacy runtime. One edge box safely runs many apps, so running near everyone stays affordable.\n\nThe database, **ToilDB**, distributes the writes, not just the reads. Every key has one **home** region that orders its writes, while data replicates outward for fast local reads. Distributing writes is the hard part almost nobody does. toil is built to do it worldwide, and the mechanism is real and tested. Live multi-region deployment is configuration-gated rather than on by default. Once it is on, a far read can lag a few milliseconds: the eventual-consistency trade. See [How toil works](./how-it-works.md) and [How toil is distributed](./distributed.md).\n\n## Who it is for\n\n- **Solo builders and small teams:** a full, global, secure stack without hiring a platform team. The pizza site is first-class.\n- **Latency-sensitive apps:** writes that resolve near the user, not across an ocean.\n- **Global apps:** logic and data near users on every continent.\n- **Realtime apps:** chat, presence, and live cursors on built-in streaming, not a bolted-on vendor.\n\nSame install for the smallest project and the largest. You grow into the scale. You do not rebuild to reach it.\n\n## When not to use toil\n\n- **You need SQL or heavy joins.** ToilDB is seven purpose-built families, not a general SQL engine. See the [database overview](../database/README.md).\n- **You lean on the Node ecosystem.** The server is a strict TypeScript subset compiled to WebAssembly. No arbitrary npm packages, no Node APIs, built-in globals instead.\n- **You are happy single-region and simple.** If one region already fits, toil's distribution is effort you do not need.\n- **You need a big integration catalog today.** toil is younger, and that catalog is smaller.\n\nNone of these are permanent. The right tool is the one that fits the job in front of you.\n\n## Related\n\n- [The modern stack](./modern-stack.md): the full, verified catalog of what is built in.\n- [How toil works](./how-it-works.md): the whole machine end to end, from React client to ToilDB.\n- [toil versus other frameworks](./vs-other-frameworks.md): an honest, axis-by-axis comparison.\n- [Security](../concepts/security.md): the defaults that are already on.\n- [Getting started](../getting-started/README.md): install the tool and build a small feature.\n",
59
+ "introduction/why-toil.md": "# Why toil? Who is it for?\n\ntoil is a full-stack framework. You write a React frontend and a TypeScript backend in one project, and toil runs both, plus a database, close to every user worldwide.\n\nShipping a fast, secure, global app normally takes a pile of rented services and a team that understands infrastructure. toil gives you that as the default: one framework, zero config, quantum-proof login, built on modern tech, running near your users from day one. The same setup serves a pizza site and a planet-scale app. Distributing writes worldwide is one of the hard things it handles for you, not the reason it exists.\n\n## The problem with modern stacks\n\nShipping a good app today means fighting your own infrastructure. It fights back in five ways.\n\n**Reads go global, writes go to one region.** Your pages load fast from caches worldwide. A write does not. A comment, a like, or an order has to crawl back to a single database in a single region. A user in Sydney writing to Virginia pays for the whole round trip. That one region is also a single point of failure for everyone.\n\n**You become a systems integrator by accident.** A production stack is a dozen rented services stitched together: a frontend host, serverless functions, a managed database, auth, email, a queue, a cache, analytics, realtime. Each has its own account, bill, SDK, and outage. Every one on your critical path is a black box you cannot inspect, patch, or secure. When it is slow, you are slow. When it is breached, part of you is breached.\n\n**It runs on yesterday's tooling.** The typical stack sits on a decade of legacy: bundler configs, transpiler shims, CommonJS-versus-ESM interop, framework churn, and a `node_modules` folder heavier than the app itself. You fight the toolchain and chase version bumps instead of building. The web platform moved on. The stack did not.\n\n**Every layer adds overhead.** Heavy runtimes, slow builds, serverless cold starts, oversized JavaScript in the browser, and a network hop between every service. Each layer adds latency and cost. The user feels it and you debug it.\n\n**Fast and safe are opt-in, never default.** Good performance and real security are things you assemble by hand: a CDN here, a cache there, careful data fetching, region tuning, hardened auth, current cryptography. Miss a piece and you are slow or exposed. The good version is always extra work, so most people ship the lesser one.\n\nA solo builder and a funded startup hit the same wall. Top-tier infrastructure means assembling and babysitting a lot of parts, so most settle for less.\n\n## What toil delivers\n\ntoil was built to reach real scale cheaply, and every design choice serves that. You get top-tier infrastructure by default: one framework instead of ten vendors, zero configuration, and no networking knowledge required. Quantum-proof login is on from the start. Four things make it work.\n\n### AAA-grade infrastructure by default\n\nTop-tier infrastructure on day one, on the smallest project, with zero setup. Your code runs at the edge near users worldwide. A global database is already there. Every shipped asset is tamper-proofed automatically with SHA-384 Subresource Integrity. Login is quantum-proof in about one line.\n\nThat login is real post-quantum cryptography. Your password is stretched into a signing key on your device and never reaches the server in a usable form. The handshake uses ML-DSA and ML-KEM, the NIST-standardized algorithms meant to survive a quantum computer. Most stacks bolt current crypto on by hand, if at all. Here it is the default.\n\nA pizza site and a planet-scale app start from the same baseline. AAA-grade is the actual bar toil grades itself against. See [design principles](./design-principles.md).\n\n### Everything built in, and owned\n\nAuth, database, email, rate limiting, analytics, realtime streaming, and background jobs are all built in, and all toil's own. Nothing third-party sits on your critical path.\n\nBecause they are one system, the parts already fit. You are not gluing ten SDKs together and hoping they agree. Full catalog: [The modern stack](./modern-stack.md).\n\n```mermaid\nflowchart TB\n subgraph assemble[\"Usual way: assemble ten vendors\"]\n direction TB\n A1[\"Your app\"] --> V1[\"host\"]\n A1 --> V2[\"functions\"]\n A1 --> V3[\"database\"]\n A1 --> V4[\"auth\"]\n A1 --> V5[\"email\"]\n A1 --> V6[\"rate limiter\"]\n A1 --> V7[\"analytics\"]\n A1 --> V8[\"realtime\"]\n A1 --> V9[\"jobs\"]\n end\n subgraph toil[\"toil: one owned stack\"]\n direction TB\n A2[\"Your app\"] --> T[\"edge + database + auth + email +<br/>rate limiting + analytics +<br/>streaming + jobs, built in\"]\n end\n```\n\nHonest boundary: owned means the core of a working app is toil's, not that outside services are banned. Call a payment provider or another API and you still can.\n\n### Modern tooling that just works\n\nBuilt on today's web platform, not a decade of legacy. TypeScript end to end, one repo, one deploy, wired by types. Change a field on the server and the frontend stops compiling until you fix it. The bug surfaces at your desk as a compile error, not in production.\n\nThe toolchain is set up for you: Oxlint + tsgolint, Prettier with a plugin for toil's decorators, an editor plugin, one CLI, and a `doctor` command that fixes common problems in place. The docs are LLM-friendly too, so an AI assistant reads your current conventions instead of guessing. More in [The modern stack](./modern-stack.md).\n\n### Hyper-scale and distributed writes\n\nYour backend compiles to a tiny sandboxed **WebAssembly** module, a compact locked-down binary that runs at near-native speed. That is modern tech, not a heavyweight legacy runtime. One edge box safely runs many apps, so running near everyone stays affordable.\n\nThe database, **ToilDB**, distributes the writes, not just the reads. Every key has one **home** region that orders its writes, while data replicates outward for fast local reads. Distributing writes is the hard part almost nobody does. toil is built to do it worldwide, and the mechanism is real and tested. Live multi-region deployment is configuration-gated rather than on by default. Once it is on, a far read can lag a few milliseconds: the eventual-consistency trade. See [How toil works](./how-it-works.md) and [How toil is distributed](./distributed.md).\n\n## Who it is for\n\n- **Solo builders and small teams:** a full, global, secure stack without hiring a platform team. The pizza site is first-class.\n- **Latency-sensitive apps:** writes that resolve near the user, not across an ocean.\n- **Global apps:** logic and data near users on every continent.\n- **Realtime apps:** chat, presence, and live cursors on built-in streaming, not a bolted-on vendor.\n\nSame install for the smallest project and the largest. You grow into the scale. You do not rebuild to reach it.\n\n## When not to use toil\n\n- **You need SQL or heavy joins.** ToilDB is seven purpose-built families, not a general SQL engine. See the [database overview](../database/README.md).\n- **You lean on the Node ecosystem.** The server is a strict TypeScript subset compiled to WebAssembly. No arbitrary npm packages, no Node APIs, built-in globals instead.\n- **You are happy single-region and simple.** If one region already fits, toil's distribution is effort you do not need.\n- **You need a big integration catalog today.** toil is younger, and that catalog is smaller.\n\nNone of these are permanent. The right tool is the one that fits the job in front of you.\n\n## Related\n\n- [The modern stack](./modern-stack.md): the full, verified catalog of what is built in.\n- [How toil works](./how-it-works.md): the whole machine end to end, from React client to ToilDB.\n- [toil versus other frameworks](./vs-other-frameworks.md): an honest, axis-by-axis comparison.\n- [Security](../concepts/security.md): the defaults that are already on.\n- [Getting started](../getting-started/README.md): install the tool and build a small feature.\n",
59
60
  "realtime/channels.md": "# Channels (`useChannel`)\n\n`useChannel` is the client-side React hook for realtime. It opens a live connection from the browser to a server `@stream` box, tracks whether it is connected, collects the messages that arrive, and gives you a `send` function. It reconnects on its own if the connection drops.\n\n## What a channel is\n\nA **channel** is simply an open, two-way connection between one browser and the server, viewed from the client's side. On the server that connection is handled by a [`@stream`](./streams.md) box. On the client you hold the other end with `useChannel` (a React hook) or `connectChannel` (a plain function, no React).\n\nYou have already met the two-piece model in the [realtime overview](./README.md): a `@stream` on the server, a channel on the client. This page is the client half.\n\n## The `useChannel` hook\n\n`useChannel` is available on the global `Toil` object in your route files, so no import is needed:\n\n```tsx\nexport default function Ping() {\n const chan = Toil.useChannel({ path: '/echo' });\n\n return (\n <main>\n <p>Status: <strong>{chan.connected ? 'connected' : 'offline'}</strong></p>\n <button type=\"button\" onClick={() => chan.send('ping')}>Send ping</button>\n <ul>\n {chan.messages.map((m, i) => (\n <li key={i}><code>{typeof m === 'string' ? m : '(binary frame)'}</code></li>\n ))}\n </ul>\n </main>\n );\n}\n```\n\n### What it returns\n\n`useChannel(options?)` returns an object with three things:\n\n| Field | Type | What it is |\n| ----------- | -------------------------- | ---------------------------------------------------------------------- |\n| `connected` | `boolean` | `true` while the socket is open. Re-renders when it changes. |\n| `messages` | `(string \\| ArrayBuffer)[]`| every frame received so far, in order. A new frame re-renders. |\n| `send` | `(data) => void` | send one frame to the server (a no-op until the socket is open). |\n\nA frame is either a **string** (text) or an **`ArrayBuffer`** (binary). Send accepts a string or binary data. To send or read structured data, encode and decode it yourself:\n\n```ts\nchan.send(new TextEncoder().encode('hello')); // send bytes\nconst text = (m: string | ArrayBuffer) =>\n typeof m === 'string' ? m : new TextDecoder().decode(m); // read either kind as text\n```\n\n### Options\n\n```ts\nToil.useChannel({\n path: '/echo', // which @stream route to connect to. Default: '/_toil'\n url: 'wss://...', // full ws(s):// override (wins over path). Usually omit this.\n reconnect: true, // auto-reconnect after an unexpected close. Default: true\n reconnectDelay: 1000 // ms to wait before each reconnect attempt. Default: 1000\n});\n```\n\nThe most important option is **`path`**: it points the channel at a specific `@stream` route. `{ path: '/echo' }` connects to a stream mounted with `@stream('echo')`. If you omit `path`, it connects to the default `/_toil` channel.\n\nYou do not build the `ws://` or `wss://` URL yourself. The hook derives it from the current page (a page served over `https` uses `wss`, otherwise `ws`), and on the production edge the transport is upgraded to WebTransport for you. Your code stays the same.\n\n### Lifecycle and cleanup\n\n`useChannel` connects when the component mounts and closes when it unmounts, so a channel lives exactly as long as the page that uses it is on screen. React's rules apply: call it at the top level of your component, not inside a loop or condition.\n\n### `connectChannel`: the non-React version\n\nIf you need a channel outside a React component (in a plain module, a store, an event handler), use `connectChannel`. It takes a callback for each incoming frame and returns a handle:\n\n```ts\nimport { connectChannel } from 'toiljs/client';\n\nconst chan = connectChannel(\n (frame) => console.log('got', frame),\n { path: '/echo' }\n);\nchan.send('hello');\nchan.close(); // stop and stop reconnecting\n```\n\n`useChannel` is `connectChannel` wrapped for React (it manages the `connected` / `messages` state for you).\n\n## The typed client: `Server.Stream`\n\n`useChannel` deals in raw frames. For a typed experience, toiljs also generates a client per `@stream` class at `Server.Stream.<ClassName>`, described in full on the [Streams](./streams.md#reaching-a-stream-from-the-browser) page. Use whichever fits: `useChannel` for a quick reactive hook, `Server.Stream.<Name>.connect()` for typed messages and explicit `onMessage` / `onClose` callbacks. Both open the same kind of connection to the same box.\n\n## A full chat-style example\n\nHere is a chat UI wired end to end: a React page that sends what you type and shows every reply, plus the server box it talks to.\n\n### Client (`client/routes/chat.tsx`)\n\n```tsx\nimport { useState } from 'react';\n\nexport default function Chat() {\n const chan = Toil.useChannel({ path: '/room' });\n const [draft, setDraft] = useState('');\n\n const asText = (m: string | ArrayBuffer): string =>\n typeof m === 'string' ? m : new TextDecoder().decode(m);\n\n function submit(): void {\n if (draft.length === 0) return;\n chan.send(draft); // one send() becomes one @message on the server\n setDraft('');\n }\n\n return (\n <main>\n <h1>Chat</h1>\n <p>Status: <strong>{chan.connected ? 'connected' : 'offline'}</strong></p>\n <ul>\n {chan.messages.map((m, i) => <li key={i}>{asText(m)}</li>)}\n </ul>\n <input\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n onKeyDown={(e) => { if (e.key === 'Enter') submit(); }}\n placeholder=\"Say something\"\n />\n <button type=\"button\" onClick={submit}>Send</button>\n </main>\n );\n}\n```\n\n### Server (`server/streams/Room.ts`)\n\n```ts\n@stream('room')\nclass Room {\n private seen: i32 = 0;\n\n @connect\n onConnect(): void {\n this.seen = 0;\n }\n\n @message\n onMessage(packet: StreamPacket): StreamOutbound {\n this.seen = this.seen + 1;\n const text = new TextDecoder().decode(packet.bytes());\n const reply = '#' + this.seen.toString() + ': ' + text;\n return StreamOutbound.reply(Uint8Array.wrap(String.UTF8.encode(reply)));\n }\n}\n```\n\nDo not forget to import the stream in `server/main.stream.ts` (see [Streams](./streams.md#the-mainstreamts-file-a-separate-tier)):\n\n```ts\nimport './streams/Room';\n```\n\nRun `toiljs dev`, open the page, and every line you send comes back numbered, live.\n\n### An honest limitation: this echoes, it does not broadcast\n\nRead the example carefully. Each browser has its **own** `Room` box, and that box replies only to **its own** connection. So in this version, two people in \"the same room\" do **not** see each other's messages: each just sees their own, echoed back. That is enough for a private assistant, a progress feed, or a single-player game, but it is **not** a shared chat room where one message reaches everyone.\n\nTo send one message to **many** connected users (true broadcast, the heart of a group chat or a live feed), you need the server to **fan out** a message to every subscriber. That is what `@channel` is for.\n\n## `@channel`: server broadcast (not yet available)\n\n> **Status: planned, not live in the current runtime.** A `@stream` class that declares a `@channel` hook is **rejected** by the edge today (\"stream channels are not a v1 runtime ABI\"), and the compiler does not yet emit it. The information below describes the intended shape so you can plan for it, not an API you can call right now.\n\nThe idea is **publish/subscribe** (\"pub/sub\"), a standard pattern for broadcast:\n\n- A **channel** is a named topic, for example a chat room id.\n- A connection **subscribes** to a channel to start receiving everything sent to it.\n- Anyone can **publish** a message to the channel, and every subscriber receives it.\n- A connection **unsubscribes** (or disconnects) to stop.\n\nWith `@channel`, the server box would join a connection to a named topic and publish messages to it, and the edge would deliver each published message to every subscriber across all the connections in that topic, no matter which worker or node each one landed on. That is the missing \"one to many\" piece that turns the echo example above into a real shared room. The plan already reserves per-plan limits for it (a cap on subscribers per channel and on message size), so the surface is designed; it is the runtime delivery that is not shipped yet.\n\n### The picture: world-wide sync (the design)\n\nWhen `@channel` ships, one `publish` will fan out across the whole edge mesh, reaching every subscriber wherever they are, at nearly the same moment. That is the **world-wide sync** idea: a live session where everyone sees the same update together, whether they are in the same city or on opposite sides of the planet.\n\n```mermaid\nflowchart TB\n Pub[\"One publish()<br/>(a chat line, a goal, a game tick)\"] --> Mesh[\"Dacely edge mesh<br/>(routes to every subscriber's core)\"]\n Mesh --> A[\"Subscriber in Tokyo\"]\n Mesh --> B[\"Subscriber in Paris\"]\n Mesh --> C[\"Subscriber in New York\"]\n Mesh --> D[\"...every other subscriber, at once\"]\n```\n\nThe mechanism meant to make this fast is the same one that already spreads plain connections across the edge: because the edge always knows where each subscriber's connection lives, a broadcast is a direct fan-out to a known set of destinations, not a search for who is listening. For the wider picture, see [Built for massive fan-out and world-wide sync](./README.md#built-for-massive-fan-out-and-world-wide-sync) in the overview.\n\nKeep the status in mind: the diagram above is the **intended shape**. The delivery runtime is not shipped yet, so treat it as the plan, not an API you can call today.\n\n**What you can build today:** anything where each user talks to their own box (assistants, per-user live updates, single-player sync, progress streams). **What needs `@channel`:** shared rooms and one-to-many broadcast. Until it ships, a common workaround is to persist messages to [the database](../database/README.md) and have clients poll or re-read, which is simpler but not instant.\n\n## Gotchas\n\n- **Frames are `string` or `ArrayBuffer`.** Decode binary frames with `TextDecoder`; there is no automatic parsing in `useChannel` (the typed `Server.Stream` client is the structured option).\n- **`send` before \"connected\" is dropped.** It is a no-op until the socket is open. Guard on `chan.connected`, or expect the first eager sends to be lost.\n- **`messages` grows forever.** It keeps every frame received. For a long-lived page, slice it or keep your own bounded list.\n- **One box per connection.** `useChannel` does not broadcast between users; that is the `@channel` feature described above.\n\n## Related\n\n- [Realtime overview](./README.md): the two-piece model and when to use realtime.\n- [Streams](./streams.md): the server `@stream` class this hook talks to.\n- [The database (ToilDB)](../database/README.md): where to persist messages that must outlive a connection or reach users who are offline.\n",
60
61
  "realtime/README.md": "# Realtime\n\nRealtime is how your app pushes data to the browser the instant it happens, instead of the browser having to ask again and again. In toiljs you get realtime from two small pieces: a server class marked `@stream`, and a client React hook called `useChannel`.\n\n## What \"realtime\" means\n\nA normal web request is one round trip. The browser asks (\"give me the todos\"), the server answers, and the connection closes. If you want fresh data a second later, you have to ask again. That is fine for a page load or a form submit, but it is a poor fit for anything that changes on its own: a live chat, a game, a price ticker, a progress bar, a presence indicator (\"3 people online\").\n\nRealtime flips it around. The browser opens **one long-lived connection** and keeps it open. After that, either side can send a message at any time, as many times as it likes, with no new handshake. That open connection is often called a **socket**.\n\n## WebTransport in plain words\n\nTo hold that long-lived connection open, the browser and the Dacely edge speak a protocol called **WebTransport**.\n\nHere is all you need to know as a beginner:\n\n- WebTransport is a modern, built-in browser feature (like `fetch`, but for a persistent two-way connection).\n- It runs on top of **HTTP/3** and **QUIC**, which are the newest, fastest versions of the web's plumbing. They are built for low latency (messages arrive quickly) and for surviving network changes (your phone switching from Wi-Fi to cellular without dropping the connection).\n- You do not write any WebTransport code by hand. toiljs gives you a tiny client API, and it uses WebTransport underneath on the production edge.\n\nOne helpful detail for later: in local development (`toiljs dev`) the same client API runs over a **WebSocket** instead, because a WebSocket is simpler to serve from one local process. A WebSocket is the older, widely supported \"keep a socket open\" browser feature. The point is that **your code is identical** in dev and in production; only the transport underneath differs, and toiljs picks it for you.\n\n## When to use realtime (and when not to)\n\nReach for realtime when **the server needs to talk first**, or when messages fly back and forth quickly:\n\n| Use realtime when... | Use a plain HTTP request when... |\n| --------------------------------------------- | ------------------------------------------------ |\n| A chat or comment thread updates live. | You load a page or a list once. |\n| A multiplayer game syncs moves and positions. | You submit a form and get one answer. |\n| You show live presence or typing indicators. | You fetch data on a button click. |\n| You stream progress of a long job. | The data rarely changes, or the user pulls it. |\n\n**Games are a first-class use case, not an afterthought.** Realtime multiplayer (player moves, low-latency input, live presence, per-player state) is one of the hardest things to build on the plain web, and it is exactly what toil streaming is designed for. More on why, and how far it is built to scale, in [Built for massive fan-out and world-wide sync](#built-for-massive-fan-out-and-world-wide-sync) below.\n\nIf a single request-and-response does the job, prefer that: it is simpler, it caches well, and it needs no open connection. Plain requests in toiljs are [HTTP routes](../backend/rest.md) and [typed RPC](../backend/rpc.md). Reach for realtime only when the \"ask again and again\" model genuinely gets in your way.\n\n## The two pieces\n\nRealtime in toiljs is always a pair:\n\n1. **The server: a `@stream` class.** You write a small class and mark it `@stream`. The edge turns it into a **resident box**: a live instance that is created when a connection opens, handles every message on that connection, and is torn down when it closes. It has four lifecycle hooks (`@connect`, `@message`, `@close`, `@disconnect`). See [Streams](./streams.md).\n\n2. **The client: a hook.** From your React UI you open the connection and send or receive messages. The low-level way is the `useChannel` hook; the typed, generated way is `Server.Stream.<ClassName>.connect()`. See [Channels](./channels.md).\n\n```mermaid\nsequenceDiagram\n participant B as Browser (React)\n participant E as Dacely edge\n participant S as Your @stream box\n B->>E: open connection (WebTransport, or WebSocket in dev)\n E->>S: create the box, run @connect\n Note over S: the box is now resident for this connection\n B->>E: send \"hello\"\n E->>S: @message(\"hello\")\n S-->>B: reply \"hi back\"\n B->>E: send \"still here\"\n E->>S: @message(\"still here\")\n S-->>B: reply \"yep\"\n B->>E: close\n E->>S: run @close, then destroy the box\n```\n\nNotice that the box lives across **many** messages in that diagram. That is the whole idea: because it is the same instance every time, it can remember things between messages (a counter, who you are, what room you joined). A normal HTTP handler forgets everything after each request; a stream box does not, for as long as the connection stays open.\n\n## Built for massive fan-out and world-wide sync\n\nRealtime gets exciting when a single event reaches **many** people at nearly the same moment. Picture a live sports app: a goal is scored, and every fan watching sees it light up together. Picture a multiplayer game: one player moves, and everyone in the match sees it happen right away. That shape, one event fanning out to a huge live audience, is what toil streaming is built for. We call it **world-wide packet sync**: everyone in the same live session gets the same update at nearly the same time, wherever they are on the planet.\n\n```mermaid\nflowchart TB\n P[\"One event<br/>(a goal, a chat line, a game move)\"] --> E[\"Dacely edge<br/>(spread across cores and cities)\"]\n E --> S1[\"Player in Tokyo\"]\n E --> S2[\"Player in Paris\"]\n E --> S3[\"Player in Sao Paulo\"]\n E --> Sn[\"...many more, at once\"]\n```\n\n### Why it can go so wide\n\nThree design choices make massive scale possible, without any single machine or single lock becoming the ceiling.\n\n1. **Connections spread across the fleet in parallel.** Every new connection is handled on its own, with no shared lock in the middle for connections to fight over. There is no central bottleneck that every connection has to pass through, so adding machines adds capacity in a straight line.\n2. **The session follows the user.** A phone that switches from Wi-Fi to cellular keeps its exact session, in-memory game state and all. A network change does not drop the connection or reset the box.\n3. **The session sits near the user.** The edge is a fleet of servers in many cities. A `@stream` runs at a regional or continental node (see [Compute tiers](../concepts/tiers.md)), so the round trip to a player stays short no matter where on the map they are.\n\n### An honest word on numbers\n\nThe framework is **designed** for very large live audiences, aiming at the scale of millions of concurrent connections in a single live session as its ambition. That is the target the architecture is built toward, not a number measured in these docs. Real capacity depends entirely on your deployment: how many machines and cores you run, how big each message is, how often it is sent, and where your users are. We do not publish a benchmarked figure here. What we can say honestly is **why** it scales (the three design choices above), and that the design removes the usual single-machine and single-lock ceilings that cap other realtime stacks.\n\nOne more honest note. Fanning a single message out to **other** users' connections (true one-to-many broadcast, the arrows in the diagram above) is the job of `@channel`. The client side of that (`useChannel`, `Server.Stream`) is live today. The server-side broadcast that reaches every subscriber across the mesh is **planned, not shipped yet**. See [Channels](./channels.md) for exactly what works now and what is coming, so you can build on solid ground.\n\n## Where to go next\n\n- [Streams](./streams.md): the server side. How to declare a `@stream` class, the four lifecycle hooks, per-connection state, replying, and the separate `main.stream.ts` file.\n- [Channels](./channels.md): the client side. The `useChannel` React hook, the generated typed client, and a chat-style example. Also covers the planned `@channel` broadcast feature.\n\n## Related\n\n- [Compute tiers (L1 to L4)](../concepts/tiers.md): where a stream box runs on the edge.\n- [HTTP routes (`@rest`)](../backend/rest.md) and [Typed RPC](../backend/rpc.md): the plain request-and-response alternatives.\n- [Daemons](../background/daemons.md): long-lived background work that is not tied to a browser connection.\n",
61
62
  "realtime/streams.md": "# Streams (`@stream`)\n\nA `@stream` is a server class that handles one live connection from start to finish. You mark a class with `@stream`, add lifecycle hooks, and the Dacely edge keeps one instance of that class alive for as long as the browser stays connected.\n\n## What a stream is (and why it is different)\n\nWhen you write an [HTTP route](../backend/rest.md), the server builds a **fresh** handler for every request and throws it away afterward. Anything you stored on the handler's fields is gone the moment the response is sent. That is perfect for one-shot requests, but it means the handler cannot \"remember\" anything between requests on its own.\n\nA `@stream` is the opposite. It is a **resident box**: one live instance, created when a connection opens and kept alive until it closes. Because it is the *same* instance for every message on that connection, values you store on its fields **persist across messages**. That is what makes it the right tool for a conversation, a game session, or anything stateful that lasts for the life of a connection.\n\nThe word \"resident\" just means \"stays in memory and keeps running.\" The word \"box\" is toiljs's name for one sandboxed instance of your compiled server code.\n\n```ts\n@stream('echo')\nclass Echo {\n private count: i32 = 0;\n\n @connect\n onConnect(): void {\n this.count = 0; // a fresh connection starts a fresh box, so count begins at 0\n }\n\n @message\n onMessage(packet: StreamPacket): StreamOutbound {\n this.count = this.count + 1; // survives across messages: same box every time\n const text = 'pong #' + this.count.toString();\n return StreamOutbound.reply(Uint8Array.wrap(String.UTF8.encode(text)));\n }\n\n @close\n onClose(): void {\n // the box is destroyed after this hook runs\n }\n}\n```\n\nConnect, send three messages, and you get back `pong #1`, `pong #2`, `pong #3`. The advancing number is proof that the same box handled all three.\n\n## Declaring a stream\n\nMark a class with `@stream` and give it a **name**. The name becomes the route the browser connects to.\n\n```ts\n@stream('echo') // mounted at /echo\nclass Echo { /* ... */ }\n\n@stream // bare form: the route is the class name (/Echo)\nclass Echo { /* ... */ }\n```\n\nThere are three forms:\n\n- `@stream('name')`: an explicit mount name (connect at `/name`).\n- `@stream` (bare): the mount name is the class name.\n- `@stream({ ... })`: a config object (see [Configuration](#configuration) below).\n\n## The four lifecycle hooks\n\nA stream method becomes a lifecycle hook when you tag it with one of these decorators. Every hook is **optional**: declare only the ones you need, and a missing hook is simply a no-op (it does nothing, it never crashes).\n\n| Decorator | Fires when... |\n| ------------- | ----------------------------------------------------------------------- |\n| `@connect` | the connection opens (the box has just been created). |\n| `@message` | an inbound frame arrives from the browser. |\n| `@close` | the connection closes cleanly (the box is destroyed after this hook). |\n| `@disconnect` | the connection is lost abruptly (network dropped, browser killed). |\n\nA **frame** is one message: one call to `send()` on the client becomes one `@message` on the server.\n\n```mermaid\nsequenceDiagram\n participant B as Browser\n participant S as @stream box\n B->>S: open\n activate S\n Note over S: @connect runs, box is created\n B->>S: frame \"a\"\n S-->>B: @message -> reply\n B->>S: frame \"b\"\n S-->>B: @message -> reply\n B->>S: close\n Note over S: @close runs, box is destroyed\n deactivate S\n```\n\n### `@connect`\n\nRuns once, right after the box is created. Use it to set up per-connection state (reset a counter, read the requested path, decide whether to accept the connection). It can return a `StreamOutbound` to accept or reject (see below). The Echo example uses it to zero its counter.\n\n**What it receives.** `@connect` is handed a `StreamInbound`, a small read-only object the host fills in with the details of the connection that just opened. It lets you look at *where* the connection came from before you decide to keep it.\n\n| Member | Type | What it gives you |\n| ------------- | -------- | ----------------------------------------------------------------------- |\n| `streamId` | `u64` | A unique id for this connection. |\n| `transport` | `i32` | A numeric tag for the transport that carried the connection. |\n| `authority()` | `string` | The host the browser connected to (for example `example.com`). |\n| `path()` | `string` | The path the browser opened (for example `/echo`). |\n\nWatch the shape: `authority()` and `path()` are **methods** (call them with `()`), while `streamId` and `transport` are plain properties (no parentheses).\n\n```ts\n@connect\nonConnect(info: StreamInbound): StreamOutbound {\n // Inspect where the connection is headed, then decide whether to keep it.\n if (info.path() != '/echo') {\n return StreamOutbound.reject(1); // refuse: any u16 reason code\n }\n this.count = 0; // fresh connection, fresh state\n return StreamOutbound.accept(); // keep the connection open\n}\n```\n\nYou do not have to take the argument. If the hook needs none of these details, declare it with no parameter (`onConnect(): void`), exactly like the Echo example above.\n\n### `@message`\n\nRuns for every inbound frame. This is where most of your logic lives. It receives the frame and may reply. Details in [Reading and replying](#reading-and-replying-to-messages).\n\n### `@close` and `@disconnect`\n\nBoth mean \"the connection is over,\" and both are your chance to clean up. The difference is *how* it ended:\n\n- `@close` is a **graceful** close: the browser (or your server) ended it on purpose.\n- `@disconnect` is an **abrupt** loss: the network dropped or the tab was killed with no goodbye.\n\nAfter either one, the box is destroyed.\n\n**What they receive.** Both hooks are handed a `StreamConnectionEvent`, a read-only summary of the connection that just ended.\n\n| Member | Type | What it gives you |\n| -------------- | ----- | ---------------------------------------------------------- |\n| `connectionId` | `u64` | The id of the connection that ended. |\n| `reason` | `u16` | A numeric close code explaining why it ended. |\n| `durationMs` | `u64` | How long the connection stayed open, in milliseconds. |\n\nAll three are plain properties (getters), so read them without `()`.\n\n```ts\n@close\nonClose(ev: StreamConnectionEvent): void {\n // Last chance to clean up. `ev` tells you how the connection ended.\n const heldSeconds = ev.durationMs / 1000;\n // e.g. record the session length or flush a buffer here.\n}\n\n@disconnect\nonDisconnect(ev: StreamConnectionEvent): void {\n // Same shape, but this fired because the connection dropped abruptly.\n // ev.reason carries the close code the edge assigned to the drop.\n}\n```\n\nAs with `@connect`, the argument is optional: declare `onClose(): void` if you do not need it.\n\n## Per-connection state (and its limits)\n\nState on the box's fields lasts for **one connection**. It does **not** survive:\n\n- a **reconnect**: if the browser drops and reopens, it gets a brand-new box that starts clean.\n- a **different user**: every connection gets its own box, so one connection's state can never leak into another. This is a safety property, not just a convenience.\n\nSo treat box fields as **per-connection scratch space** only. For anything that must outlive the connection (a saved message, a score, who a user is across reconnects), write it to [the database](../database/README.md), not to a class field.\n\n## Reading and replying to messages\n\nBy default, a `@message` receives a `StreamPacket`, which is a thin view over the raw bytes that arrived, and returns a `StreamOutbound`, which stages the reply.\n\n```ts\n@message\nonMessage(packet: StreamPacket): StreamOutbound {\n const raw = packet.bytes(); // the inbound frame as bytes\n return StreamOutbound.reply(raw); // echo the same bytes back\n}\n```\n\n`StreamPacket` (the inbound frame):\n\n| Member | What it gives you |\n| ------------ | ---------------------------------------------------------- |\n| `bytes()` | the whole frame as a `Uint8Array` (copy it if you keep it). |\n| `length` | the number of bytes in the frame. |\n| `at(i)` | the byte at index `i`. |\n\n`StreamOutbound` (what you return):\n\n| Call | Meaning |\n| ----------------------------- | ------------------------------------------------------------------ |\n| `StreamOutbound.reply(bytes)` | send one frame back to the browser. |\n| `StreamOutbound.empty()` | accept the frame and send nothing back. |\n| `StreamOutbound.reject(code)` | refuse (used from `@connect` to turn a connection away). |\n| `StreamOutbound.accept()` | accept a connection with no reply frame. |\n\nA `@message` may also return `void` when it never replies.\n\n> **Bytes, not strings.** A frame is raw bytes on the wire. To send text, encode it with `String.UTF8.encode(...)` (server) or `new TextEncoder().encode(...)` (browser), and decode it with `new TextDecoder().decode(...)` on the other side.\n\n### Typed messages\n\nRaw bytes are flexible but fiddly. If your messages are structured, declare a [`@data`](../backend/data.md) class and pass it as the stream's `message` type. Your `@message` hook then receives the **decoded object** instead of raw bytes.\n\n```ts\n@data\nclass ChatMsg {\n text: string = '';\n constructor(text: string = '') { this.text = text; }\n}\n\n@stream({ message: ChatMsg })\nclass Chat {\n @message\n onMessage(msg: ChatMsg): StreamOutbound { // decoded @data, not raw bytes\n const echoed = 'you said: ' + msg.text;\n return StreamOutbound.reply(Uint8Array.wrap(String.UTF8.encode(echoed)));\n }\n}\n```\n\nThe reply is still raw (`StreamOutbound` deals in bytes). Only the **inbound** side is decoded for you. On the client, a typed stream lets you `send(new ChatMsg('hi'))` and toiljs encodes it for you.\n\n## Games and interactive apps\n\nA stream box remembers state for the life of a connection, which is exactly what a game session needs. Each connected player gets their **own** box, and that box holds the player's live state (position, health, score) in memory, right next to the core handling their packets. The lifecycle hooks map cleanly onto a session: `@connect` spawns the player, `@message` handles each input, `@close` and `@disconnect` remove them.\n\n```ts\n// server/streams/Match.ts\n@data\nclass Move { // the input a player sends each tick\n dx: i32 = 0;\n dy: i32 = 0;\n constructor(dx: i32 = 0, dy: i32 = 0) { this.dx = dx; this.dy = dy; }\n}\n\n@stream({ message: Move, scope: StreamScope.Regional })\nclass Match {\n // Per-connection state: this player's position. It lives in memory for the\n // whole session, on the one core that owns this connection.\n private x: i32 = 0;\n private y: i32 = 0;\n private moves: u32 = 0;\n\n @connect\n onConnect(info: StreamInbound): StreamOutbound {\n this.x = 50; // spawn point\n this.y = 50;\n this.moves = 0;\n return StreamOutbound.accept();\n }\n\n @message\n onMove(move: Move): StreamOutbound {\n // Apply the input to this player's live position, then confirm it.\n this.x = this.x + move.dx;\n this.y = this.y + move.dy;\n this.moves = this.moves + 1;\n const state = '{\"x\":' + this.x.toString() + ',\"y\":' + this.y.toString() + '}';\n return StreamOutbound.reply(Uint8Array.wrap(String.UTF8.encode(state)));\n }\n\n @disconnect\n onDrop(ev: StreamConnectionEvent): void {\n // The player left (tab closed, network died). Their box is destroyed\n // next. Persist a score here if it must outlive the session.\n }\n}\n```\n\nEvery input a player sends is one `@message`, applied to **their** box's position and confirmed straight back to them with low latency. Because the box is resident, the player's position is always there in memory, on the same core, with no database round trip per move. That is what makes fast input loops (a game tick, a cursor drag, a live editor) feel instant.\n\n**What this version does, and does not, do.** Each player has their own box, so this handles per-player input, per-player state, and instant confirmation back to that player. To make players see **each other** (the other half of multiplayer), one player's move must fan out to everyone else in the match. That cross-connection broadcast is the `@channel` feature, which is **planned, not live yet** (see [Channels](./channels.md)). Until it ships, the common patterns are to write shared match state to [the database](../database/README.md) and have clients read it, or to keep a match to a single authoritative box. The per-connection pieces above (input, state, and presence via `@connect` / `@disconnect`) work today.\n\nFor **presence** (\"who is online\"), `@connect` and `@disconnect` are your join and leave signals: bump a counter or write a row when a player connects, and undo it when they drop.\n\n## The `main.stream.ts` file (a separate tier)\n\nStreams live in their **own entry file**, `server/main.stream.ts`, separate from the request entry `server/main.ts`. Importing your `@stream` classes there pulls them into a **separate compiled artifact**, `build/server/release-stream.wasm`.\n\n```ts\n// server/main.stream.ts\nimport { revertOnError } from 'toiljs/server/runtime/abort/abort';\n\nimport './streams/Echo'; // add each @stream module here as you grow\nimport './streams/Chat';\n\n// Re-export the WASM entry points the host binds, exactly like main.ts.\nexport * from 'toiljs/server/runtime/exports';\nexport function abort(message: string, fileName: string, line: u32, column: u32): void {\n revertOnError(message, fileName, line, column);\n}\n```\n\nWhy a separate file? A stream box and a request handler are **different kinds of program** that run on different parts of the edge (see [Compute tiers](../concepts/tiers.md)). toiljs compiles each into its own `.wasm`:\n\n```sh\n$ toiljs build\n$ ls build/server/*.wasm\nbuild/server/release.wasm # L1 request (@rest / @service)\nbuild/server/release-stream.wasm # L2/L3 stream (@stream)\nbuild/server/release-cold.wasm # L4 daemon (@daemon)\n```\n\nYou do not run this by hand. `toiljs build` produces `release-stream.wasm` automatically whenever your project has a `@stream` surface, and shared helper code and `@data` types are compiled into every artifact.\n\n> **One file cannot be both a stream and an RPC surface.** A single source file may not declare both a `@stream` and a `@service` / `@remote` ([RPC](../backend/rpc.md)), because one compiled artifact cannot be two tiers at once. Keep them in separate files: `@stream` in `main.stream.ts`, `@rest` / `@service` in `main.ts`. They coexist happily side by side in the same project, just not in the same file.\n\n## Configuration\n\nThe config-object form lets you tune a stream:\n\n```ts\n@stream({\n scope: StreamScope.Regional, // where the box runs (see below)\n message: ChatMsg, // decode inbound frames into this @data type\n maxFrameBytes: 65536, // reject frames larger than this\n ingressRingBytes: 262144 // size of the inbound buffer\n})\nclass Chat { /* ... */ }\n```\n\n- **`scope`** picks how close to the user the box runs. `StreamScope.Regional` (the default) runs it at a regional node; `StreamScope.Continental` runs it at a wider continental node. See [Compute tiers](../concepts/tiers.md) for what L2 and L3 mean.\n- **`message`** is the typed-message shortcut described above.\n- **`maxFrameBytes`** and **`ingressRingBytes`** cap frame size and buffer size to protect the box from oversized or flooding input.\n\n## Reaching a stream from the browser\n\nEvery `@stream` class gets a generated, typed client at `Server.Stream.<ClassName>`, wired up for you in `shared/server.ts` (the same place the [RPC](../backend/rpc.md) client lands). Call `connect()` to open the connection:\n\n```ts\nimport '../shared/server'; // attaches globalThis.Server (browser-only)\n\nconst chat = await Server.Stream.Echo.connect();\nchat.onMessage((bytes) => { /* a reply frame, always raw bytes */ });\nchat.send(new TextEncoder().encode('hello'));\nchat.onClose((code) => { /* the connection ended */ });\nchat.close();\n```\n\nThe client is keyed by the **class name** (`Server.Stream.Echo`) and connects to the class's **mount route** (`/echo`). Inbound replies are always raw bytes. The full client walkthrough, plus the lower-level `useChannel` hook, is in [Channels](./channels.md).\n\n## How placement works (you do not manage it)\n\nOn the production edge, your box lives in **one place** for the connection's whole life. Every message from that connection is routed to the exact instance holding your box, so its in-memory state is always there. You never configure this; the edge does it automatically. In `toiljs dev` there is only one process, so this is a non-issue.\n\n## Why this scales\n\nA `@stream` box is deliberately cheap to run at scale, and the edge is built to spread a very large number of them across many machines and cities. Three design choices do the heavy lifting:\n\n- **Connections spread across the fleet in parallel.** Each connection is handled on its own, with no shared lock in the middle for connections to fight over. There is no central bottleneck every connection has to pass through, so adding machines adds capacity in a straight line.\n- **The connection survives network changes.** If a client's network changes (Wi-Fi to cellular, or a NAT rebind), the edge keeps the connection attached to the same box. The user keeps their session and their in-memory state; the box is never orphaned.\n- **Each box is isolated and bounded.** Every connection gets its own sandboxed box with its own linear memory. One tenant's boxes are capped to a slice of the node's RAM (a noisy-neighbor guard), and a single box is hard-capped (64 MiB) so a runaway connection can only fill itself, then it traps. Each lifecycle hook also runs under a hard compute cap, so a hook that loops forever is stopped instead of hogging the machine.\n\nPut together: connections spread across the fleet, run in parallel, the session sticks to its box even when the network moves, and each box is walled off from the others. That is what lets one deployment hold a very large number of live connections at once. How large depends on your hardware and message sizes; these docs do not publish a benchmarked number.\n\n> **The wider picture.** For how the edge spreads connections across the fleet and the mesh adds up to world-wide fan-out, see [Built for massive fan-out and world-wide sync](./README.md#built-for-massive-fan-out-and-world-wide-sync) in the overview.\n\n## Gotchas\n\n- **Box fields are per-connection only.** They reset on reconnect and are never shared between users. Persist anything durable to [the database](../database/README.md).\n- **Frames are bytes.** Encode and decode text yourself, or use a typed `message` so toiljs does it.\n- **Copy `packet.bytes()` if you keep it.** The inbound buffer is reused after the hook returns, so store a copy if you need the bytes later.\n- **A file cannot mix `@stream` with `@service` / `@remote`.** Keep streams in `main.stream.ts`.\n- **`@channel` is not live yet.** A stream that declares a `@channel` hook is rejected by the edge today. Broadcasting to many subscribers is a planned feature; see [Channels](./channels.md).\n\n## Related\n\n- [Realtime overview](./README.md): the big picture and when to reach for realtime.\n- [Channels](./channels.md): the client `useChannel` hook and a chat-style example.\n- [Compute tiers (L1 to L4)](../concepts/tiers.md): where the stream artifact runs.\n- [Data types (`@data`)](../backend/data.md): typed messages.\n- [The database (ToilDB)](../database/README.md): where to keep state that outlives a connection.\n",