toiljs 0.0.85 → 0.0.87

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 (132) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +2 -2
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/index.js +303 -293
  5. package/build/compiler/.tsbuildinfo +1 -1
  6. package/build/compiler/config.d.ts +2 -0
  7. package/build/compiler/config.js +1 -0
  8. package/build/compiler/docs.js +8 -24
  9. package/build/compiler/generate.js +4 -2
  10. package/build/compiler/index.d.ts +1 -1
  11. package/build/compiler/index.js +69 -6
  12. package/build/compiler/toil-docs.generated.js +64 -22
  13. package/build/devserver/.tsbuildinfo +1 -1
  14. package/build/devserver/analytics/index.js +7 -3
  15. package/build/devserver/db/database.d.ts +4 -0
  16. package/build/devserver/db/database.js +43 -1
  17. package/build/devserver/db/index.d.ts +1 -1
  18. package/build/devserver/db/index.js +1 -1
  19. package/build/devserver/db/types.d.ts +3 -0
  20. package/build/devserver/db/types.js +2 -0
  21. package/build/devserver/runtime/module.js +4 -1
  22. package/docs/README.md +104 -65
  23. package/docs/auth/README.md +102 -0
  24. package/docs/auth/configuration.md +94 -0
  25. package/docs/auth/extending.md +202 -0
  26. package/docs/auth/how-it-works.md +138 -0
  27. package/docs/auth/usage.md +188 -0
  28. package/docs/backend/README.md +143 -0
  29. package/docs/backend/data.md +351 -0
  30. package/docs/backend/rest.md +402 -0
  31. package/docs/backend/rpc.md +226 -0
  32. package/docs/background/README.md +114 -0
  33. package/docs/background/daemons.md +230 -0
  34. package/docs/background/derive.md +179 -0
  35. package/docs/cli/README.md +377 -0
  36. package/docs/concepts/config.md +416 -0
  37. package/docs/concepts/decorators.md +127 -0
  38. package/docs/concepts/security.md +108 -0
  39. package/docs/concepts/tiers.md +166 -0
  40. package/docs/concepts/types.md +216 -0
  41. package/docs/database/README.md +143 -0
  42. package/docs/database/capacity.md +350 -0
  43. package/docs/database/counters.md +174 -0
  44. package/docs/database/documents.md +255 -0
  45. package/docs/database/events.md +307 -0
  46. package/docs/database/membership.md +246 -0
  47. package/docs/database/setup.md +155 -0
  48. package/docs/database/unique.md +216 -0
  49. package/docs/database/views.md +246 -0
  50. package/docs/frontend/README.md +101 -0
  51. package/docs/frontend/data-fetching.md +243 -0
  52. package/docs/frontend/images.md +148 -0
  53. package/docs/frontend/metadata.md +344 -0
  54. package/docs/frontend/rendering.md +236 -0
  55. package/docs/frontend/routing.md +344 -0
  56. package/docs/frontend/scripts.md +118 -0
  57. package/docs/frontend/search.md +191 -0
  58. package/docs/frontend/styling.md +147 -0
  59. package/docs/getting-started/README.md +81 -0
  60. package/docs/getting-started/create-project.md +131 -0
  61. package/docs/getting-started/deploy.md +101 -0
  62. package/docs/getting-started/first-app.md +215 -0
  63. package/docs/getting-started/installation.md +106 -0
  64. package/docs/getting-started/migrating.md +125 -0
  65. package/docs/getting-started/project-structure.md +163 -0
  66. package/docs/introduction/README.md +41 -0
  67. package/docs/introduction/design-principles.md +55 -0
  68. package/docs/introduction/distributed.md +74 -0
  69. package/docs/introduction/how-it-works.md +74 -0
  70. package/docs/introduction/hyperscale.md +51 -0
  71. package/docs/introduction/modern-stack.md +36 -0
  72. package/docs/introduction/vs-other-frameworks.md +48 -0
  73. package/docs/introduction/why-toil.md +93 -0
  74. package/docs/llms.txt +90 -0
  75. package/docs/realtime/README.md +102 -0
  76. package/docs/realtime/channels.md +211 -0
  77. package/docs/realtime/streams.md +369 -0
  78. package/docs/services/README.md +60 -0
  79. package/docs/services/analytics.md +268 -0
  80. package/docs/services/caching.md +175 -0
  81. package/docs/services/cookies.md +235 -0
  82. package/docs/services/crypto.md +209 -0
  83. package/docs/services/email.md +289 -0
  84. package/docs/services/environment.md +141 -0
  85. package/docs/services/ratelimit.md +174 -0
  86. package/docs/services/time.md +85 -0
  87. package/examples/basic/client/routes/analytics.tsx +13 -12
  88. package/examples/basic/server/models/SiteAnalytics.ts +29 -17
  89. package/examples/basic/server/routes/Analytics.ts +15 -17
  90. package/examples/basic/server/routes/UserId.ts +22 -0
  91. package/package.json +15 -11
  92. package/scripts/gen-toil-docs.mjs +24 -35
  93. package/server/auth/AuthController.ts +336 -0
  94. package/server/auth/AuthUser.ts +23 -0
  95. package/server/auth/index.ts +16 -0
  96. package/server/globals/auth.ts +31 -0
  97. package/server/globals/userid.ts +107 -0
  98. package/src/compiler/config.ts +13 -0
  99. package/src/compiler/docs.ts +16 -33
  100. package/src/compiler/generate.ts +6 -2
  101. package/src/compiler/index.ts +114 -6
  102. package/src/compiler/toil-docs.generated.ts +64 -22
  103. package/src/devserver/analytics/index.ts +10 -4
  104. package/src/devserver/db/database.ts +67 -1
  105. package/src/devserver/db/index.ts +1 -0
  106. package/src/devserver/db/types.ts +13 -0
  107. package/src/devserver/runtime/module.ts +7 -0
  108. package/test/analytics-dev.test.ts +2 -1
  109. package/test/devserver-database.test.ts +113 -0
  110. package/docs/auth-todo.md +0 -149
  111. package/docs/auth.md +0 -322
  112. package/docs/caching.md +0 -115
  113. package/docs/cli.md +0 -17
  114. package/docs/client.md +0 -39
  115. package/docs/cookies.md +0 -457
  116. package/docs/crypto.md +0 -130
  117. package/docs/daemon.md +0 -123
  118. package/docs/data.md +0 -131
  119. package/docs/derive.md +0 -159
  120. package/docs/email.md +0 -326
  121. package/docs/environment.md +0 -97
  122. package/docs/getting-started.md +0 -128
  123. package/docs/index.md +0 -30
  124. package/docs/ratelimit.md +0 -95
  125. package/docs/routing.md +0 -259
  126. package/docs/rpc.md +0 -149
  127. package/docs/server.md +0 -61
  128. package/docs/ssr.md +0 -632
  129. package/docs/streams.md +0 -178
  130. package/docs/styling.md +0 -22
  131. package/docs/tiers.md +0 -133
  132. package/docs/time.md +0 -43
@@ -5,26 +5,68 @@
5
5
 
6
6
  /** The framework guides written into `.toil/docs/`, keyed by filename, generated from `docs/`. */
7
7
  export const TOIL_DOCS: Record<string, string> = {
8
- "index.md": "# toiljs\n\nA full-stack React framework: a Vite-bundled client SPA with file-based routing, plus a\ntoilscript-to-WebAssembly server target.\n\n## Project layout\n\n- `client/`, the app: `routes/` (file-based), `layout.tsx`, `components/`, `styles/`,\n `public/`, and `toil.tsx` (the entry that calls `Toil.mount`).\n- `server/`, the toilscript WASM target (`@main` entry), compiled by `toilscript`.\n `@data`/`@remote`/`@service` here generate the typed client `Server` API (see [server.md](./server.md)).\n- `toil.config.ts`, configuration via `defineConfig` (`toiljs.config.ts` also works).\n- Generated, gitignored, do not edit: `.toil/` (working dir), `toil-env.d.ts` (ambient\n globals), `toil-routes.d.ts` (typed routes), `shared/server.ts` (the typed RPC module,\n emitted by the server build; import `@data` classes from `shared/server`).\n\n## Key ideas\n\n- `Toil` is a native global (no import): `Toil.Link`, `Toil.useRouter`, `Toil.useLoaderData`,\n etc. The IO classes (`FastMap`, `FastSet`, `DataWriter`, `DataReader`), `parseError`, and the\n generated `Server` RPC surface are globals too.\n- Scripts: `npm run dev` (HMR), `npm run build` (→ `build/client` + `build/server`),\n `npm start` (self-host the build).\n- Compute tiers: the server can span L1 request (`server/main.ts`, `@rest`/`@service`/`@remote`),\n L2/L3 stream (`server/main.stream.ts`, `@stream`), and L4 daemon (`server/main.daemon.ts`,\n `@daemon`/`@scheduled`); each tier compiles into its own artifact. See [tiers.md](./tiers.md).\n\nSee [routing.md](./routing.md), [client.md](./client.md), [styling.md](./styling.md),\n[server.md](./server.md), [ssr.md](./ssr.md), [rpc.md](./rpc.md), [tiers.md](./tiers.md),\n[streams.md](./streams.md), [daemon.md](./daemon.md), [derive.md](./derive.md), [cli.md](./cli.md).\n",
9
- "getting-started.md": "# Getting started\n\nA toiljs app has two halves that build and ship together:\n\n- a **server** written in ToilScript, compiled to a single WebAssembly module\n (`build/server/release.wasm`), and\n- a **client** (Vite + React) that talks to the server through a generated,\n fully typed `Server` proxy.\n\nThe server runs one fresh wasm instance per request, identically on the dev\nserver and on the edge. There is no Node.js in the request path: your handler is\nwasm.\n\n## Project layout\n\n```\nproject/\n toilconfig.json server (wasm) build config: entries, target, AS options\n toil.config.ts client config (defineConfig: dev/build/SEO options)\n\n server/\n main.ts wires Server.handler, re-exports the wasm exports + abort\n routes/*.ts @rest controllers (auto-discovered)\n services/*.ts @service / @remote (auto-discovered)\n core/AppHandler.ts your top-level ToilHandler\n models/*.ts @data / @user classes\n\n shared/\n server.ts GENERATED by the server build (--rpcModule): the typed\n client surface (Server proxy, @data codecs, getUser)\n\n client/\n routes/*.tsx file-based pages\n layout.tsx, 404.tsx root layout / not-found\n styles/*.css\n\n build/\n server/release.wasm compiled server (+ release.wat text form)\n client/ Vite output\n```\n\nThe compiler discovers every `.ts` under `server/` that declares a decorated\nsurface (`@rest`, `@service`, `@remote`, `@data`, `@user`) on its own. Importing\nthose modules from `main.ts` is still good practice: it keeps a direct\n`toilscript` run (which only sees the `toilconfig.json` entries) building the\nexact same server.\n\n## `main.ts`\n\nThree things are required, and the comments in the scaffold say so:\n\n```ts\nimport { Server } from 'toiljs/server/runtime';\nimport { revertOnError } from 'toiljs/server/runtime/abort/abort';\n\nimport { AppHandler } from './core/AppHandler';\n\n// Pull every decorated surface into a direct `toilscript` build.\nimport './routes/Players';\nimport './services/Stats';\n\n// 1. The handler factory: one fresh handler instance per request.\nServer.handler = () => new AppHandler();\n\n// 2. Re-export the wasm entrypoints (`handle`, `render`).\nexport * from 'toiljs/server/runtime/exports';\n\n// 3. The AssemblyScript trap hook.\nexport function abort(message: string, fileName: string, line: u32, column: u32): void {\n revertOnError(message, fileName, line, column);\n}\n```\n\nIf all you need is `@rest` routing, your handler can be `RestHandler` (see\n[Routing](./routing.md)) and you do not have to write an `AppHandler` at all.\n\n## The request lifecycle\n\nFor each request the runtime (`server/runtime/exports`):\n\n1. decodes the request envelope into a [`Request`](./routing.md#request),\n2. publishes it ambiently as `Server.currentRequest` (so `AuthService.getUser()`\n and friends can read its cookies with no argument),\n3. builds the handler via `Server.handler()` and calls\n `onRequestStarted` `handle(req)` `onRequestCompleted`,\n4. encodes the returned [`Response`](./routing.md#response) and clears the\n ambient request.\n\nBecause the instance is fresh and memory is wiped between requests, **nothing in\nmodule globals survives across requests.** Anything that must persist (accounts,\nsessions you do not put in a cookie, rate-limit counters) belongs in an external\nstore reached through a host binding.\n\n## CLI\n\nThe `toiljs` CLI drives both halves:\n\n| Command | What it does |\n| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `toiljs create [name]` | Scaffold a new app (templates, styling, options). |\n| `toiljs dev` | Dev server with hot reload: watches `server/`, rebuilds the wasm via toilscript, regenerates `shared/server.ts`, and runs Vite for the client. Flags: `--root <dir>`, `--port <n>`, `--host`. |\n| `toiljs build` | Production build: server wasm first (so `shared/server.ts` is fresh), then the Vite client + static prerender. Flags: `--root <dir>`, `--server` (server only). |\n| `toiljs start` | Self-host a built app with production uWS/static workers, no Vite. Flags: `--root`, `--port`, `--host`, `--threads`. |\n| `toiljs doctor` | Diagnose setup/deps (`--json`, `--fix`). |\n\nIn dev, requests whose method matches a dispatchable verb go into the wasm\nfirst; if the guest reports \"no route matched\" (the `x-toil-unhandled` marker)\nthe request falls through to Vite, so client routes and assets just work\nalongside your API.\n\n## Building the server by hand\n\n`toiljs build` runs toilscript for you, but you can invoke it directly (this is\nwhat the examples do):\n\n```sh\ntoilscript --target release --rpcModule shared/server.ts\n```\n\n`--target release` reads `toilconfig.json` and emits the wasm at\n`targets.release.outFile`; `--rpcModule shared/server.ts` writes the generated\ntyped client (see [RPC](./rpc.md)).\n\n## Next\n\n- [Routing](./routing.md) to expose HTTP endpoints.\n- [Data codec](./data.md) for request/response bodies.\n- [Auth](./auth.md) for login and sessions.\n",
10
- "routing.md": "# Routing\n\ntoiljs routing is decorator-driven. You write a controller class, annotate it\nwith `@rest` and its methods with verb decorators, and the ToilScript compiler\ngenerates the dispatcher. Routes can take a typed body, read path params and the\nraw request through a `RouteContext`, and return either a `Response` or a typed\nvalue that is auto-encoded.\n\n```ts\nimport { Response, RouteContext } from 'toiljs/server/runtime';\n\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 @post('/')\n public create(input: NewPlayer): Player {\n // `input` is the decoded request body; returning a @data value JSON-encodes it\n return Player.from(input);\n }\n}\n```\n\n## `@rest` controllers\n\n`@rest` marks a class as a route controller and mounts it at a prefix.\n\n```ts\n@rest('api') // mounted at /api\n@rest('/') // or @rest('') // mounted at the root\n@rest({ stream: DataStream.Binary }) // root mount, binary codec by default\n```\n\n- The string argument is the mount prefix. `\"api\"`, `\"/api\"`, and `\"api/\"` all\n normalize to `/api`; `\"\"` and `\"/\"` mean the root.\n- The object form sets class-wide defaults. `stream: DataStream.Binary` makes\n every route in the class use the binary `@data` codec; the default is\n `DataStream.JSON`. Individual routes override this with `@route`.\n\nThe compiler injects, at module init, a registration that adds the controller to\nthe global `Rest` registry. Controllers dispatch in the order their modules are\nloaded; routes within a controller try in declaration order, first match wins.\n\n## Verb decorators\n\nEach HTTP method has a decorator taking a single path string:\n\n```ts\n@get('/path') @post('/path') @put('/path') @delete('/path')\n@patch('/path') @head('/path') @options('/path')\n```\n\nThe full path is the controller prefix joined with the route path\n(`prefix=\"/api\"`, `@get(\"/todos/:id\")` → `/api/todos/:id`).\n\n### `@route` (explicit form)\n\n`@route` is the general form; use it when you need to set the stream mode per\nroute or prefer an object:\n\n```ts\n@route({ method: Methods.POST, path: '/upload', stream: DataStream.Binary })\npublic upload(body: FileData): FileResult { /* ... */ }\n```\n\n`method` (from the `Methods` enum) and `path` are required; `stream` is\noptional and overrides the controller default.\n\n## Path parameters\n\nA `:name` segment captures that URL segment. Read it with `ctx.param(\"name\")`:\n\n```ts\n@get('/todos/:id/items/:itemId')\npublic getItem(ctx: RouteContext): Response {\n const id = ctx.param('id');\n const itemId = ctx.param('itemId');\n return Response.json(`{\"todo\":\"${id}\",\"item\":\"${itemId}\"}`);\n}\n```\n\nMatching is segment-exact: the request path must have the same number of\nsegments, static segments must match literally, and `:param` segments capture\nthe value. The query string is stripped before matching.\n\n## Method parameters\n\nA route method takes zero, one, or two parameters, classified by type:\n\n- a `RouteContext` parameter receives the match context (path params, query,\n headers, raw body);\n- any other type is treated as the **request body**, decoded as a `@data` value.\n\n```ts\n@get('/status')\npublic status(): StatusResponse { /* no body, no context */ }\n\n@get('/user/:id')\npublic getUser(ctx: RouteContext): User { /* context only */ }\n\n@post('/create')\npublic create(input: NewTodo): Todo { /* body only */ }\n\n@post('/user/:id/score')\npublic addScore(input: ScoreDelta, ctx: RouteContext): Player {\n const id = ctx.param('id'); /* body AND context */\n}\n```\n\nThe body is decoded per the route's stream mode: in JSON mode from\n`JSON.parse(ctx.text())`, in Binary mode from `Body.decode(req.body)`. See\n[Data codec](./data.md).\n\n## Return types\n\nThe compiler encodes the return value by its type:\n\n| Return type | Result |\n| --- | --- |\n| `Response` | Returned as-is. Full control over status, headers, body. |\n| `void` | `204 No Content`. |\n| a `@data` type, JSON stream | `Response.json(value.toJSON().toString())`. |\n| a `@data` type, Binary stream | `Response.bytes(value.encode())`. |\n\nReturning a `Response` lets you set status, headers, cookies, and caching\ndirectly; returning a typed value is the terse path when you just want the data\nserialized.\n\n## Data streams\n\nEach route is either **JSON** (default) or **Binary**:\n\n- **JSON**, the body is `JSON.parse`d and revived via the `@data` type's\n `fromJSON`; the response is the type's `toJSON()`. 64-bit-and-larger integers\n cross the wire as decimal strings (exact at any size). Best for endpoints a\n browser or third party calls directly.\n- **Binary**, the body is `Body.decode(bytes)` and the response is\n `value.encode()`, using the deterministic `DataWriter`/`DataReader` codec. No\n precision loss, smaller, faster. Best for app-to-app and anything\n security-sensitive.\n\nSet the mode on the controller (`@rest({ stream: DataStream.Binary })`) or per\nroute (`@route({ ..., stream: DataStream.Binary })`).\n\n## Dispatch and the 404 fallback\n\nAt runtime the global `Rest` registry tries each controller in order:\n\n```ts\nconst hit = Rest.dispatch(req); // Response | null\nif (hit != null) return hit; // first matching route's Response\nreturn Response.unhandled(); // no route matched\n```\n\n`RestHandler` is a ready-made handler that does exactly this, so a REST-only app\nneeds no custom handler:\n\n```ts\nimport { RestHandler } from 'toiljs/server/runtime';\nServer.handler = () => new RestHandler();\n```\n\n`Response.unhandled()` is a `404` carrying the `x-toil-unhandled` marker header.\nOn the dev server and edge that marker means \"no route matched here\" and lets\nthe request fall through to the next layer (Vite in dev, static/SSR on the\nedge). A deliberate `Response.notFound()` does **not** carry the marker and is\nsent to the client verbatim.\n\n---\n\n## `Request`\n\nThe decoded incoming request (`server/runtime/request.ts`).\n\n### Fields\n\n| Field | Type | Notes |\n| --- | --- | --- |\n| `method` | `Method` | `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `UNKNOWN`. |\n| `path` | `string` | Path including the query string. |\n| `headers` | `Array<Header>` | Ordered; a `Header` is `{ name, value }`. |\n| `body` | `Uint8Array` | Raw request body bytes. |\n\n### Methods\n\n| Method | Signature | Notes |\n| --- | --- | --- |\n| `header` | `header(name: string): string \\| null` | Case-insensitive lookup, `null` if absent. |\n| `cookies` | `cookies(): CookieMap` | Parses the `Cookie` header (percent-decoded values); cached for the request. |\n| `cookie` | `cookie(name: string): string \\| null` | A single cookie value, or `null`. |\n\nThe `Method` enum and `Header` class are exported from\n`toiljs/server/runtime`.\n\n## `RouteContext`\n\nPassed to any route method that declares a `RouteContext` parameter\n(`server/runtime/rest/RouteContext.ts`).\n\n| Member | Signature | Notes |\n| --- | --- | --- |\n| `request` | `Request` | The raw incoming request. |\n| `param` | `param(name: string): string` | Captured path param; `\"\"` if absent. |\n| `query` | `query(name: string): string` | Query-string value; `\"\"` if absent. Not URL-decoded in v1. |\n| `header` | `header(name: string): string \\| null` | Case-insensitive request header. |\n| `text` | `text(): string` | The request body decoded as UTF-8. |\n\n## `Response`\n\nThe outgoing response builder (`server/runtime/response.ts`). Construct one with\na static factory, then chain instance methods (each returns the same `Response`).\n\n### Constructor\n\n```ts\nnew Response(status: u16, body: Uint8Array, headers: Array<Header> | null = null)\n```\n\n### Static factories\n\n| Factory | Signature | Status | Content-Type |\n| --- | --- | --- | --- |\n| `Response.text` | `text(body: string, status: u16 = 200)` | 200 | `text/plain; charset=utf-8` |\n| `Response.html` | `html(body: string, status: u16 = 200)` | 200 | `text/html; charset=utf-8` |\n| `Response.json` | `json(body: string, status: u16 = 200)` | 200 | `application/json; charset=utf-8` |\n| `Response.bytes` | `bytes(body: Uint8Array, status: u16 = 200)` | 200 | `application/octet-stream` |\n| `Response.empty` | `empty(status: u16)` | custom | (none) |\n| `Response.notFound` | `notFound()` | 404 | text |\n| `Response.badRequest` | `badRequest(msg = 'bad request')` | 400 | text |\n| `Response.internalError` | `internalError(msg = 'internal error')` | 500 | text |\n| `Response.unhandled` | `unhandled()` | 404 | text + `x-toil-unhandled` marker |\n\n`json` takes an already-serialized string; build it with `DataWriter`-free JSON\nor a `@data` type's `toJSON().toString()`. For binary, prefer `bytes`.\n\n### Instance methods\n\n| Method | Signature | Notes |\n| --- | --- | --- |\n| `setHeader` | `setHeader(name: string, value: string): Response` | Appends a header (repeatable). |\n| `setCookie` | `setCookie(cookie: Cookie): Response` | Appends a `Set-Cookie`. Call again for more. |\n| `setCookieKV` | `setCookieKV(name: string, value: string): Response` | Shorthand for `setCookie(new Cookie(name, value))`. |\n| `clearCookie` | `clearCookie(name: string, path = '/', domain = ''): Response` | Emits a deletion `Set-Cookie` (empty value, `Max-Age=0`). |\n| `cache` | `cache(edgeTtlMinutes: u16, browserTtlSeconds: u32 = 0, privateScope: bool = false, allowAuth: bool = false): Response` | Marks the response cacheable. See [Caching](./caching.md). |\n| `cacheFor` | `cacheFor(minutes: u16): Response` | Shorthand for `cache(minutes)` (edge only). |\n\n```ts\nreturn Response.json('{\"id\":42}')\n .setHeader('x-trace', traceId)\n .setCookie(Cookie.create('sid', token).httpOnly().secure())\n .cacheFor(5);\n```\n\nSee [Cookies](./cookies.md) for the cookie builder, and [Caching](./caching.md)\nfor the cache directives.\n",
11
- "client.md": "# Client runtime\n\nEverything is on the `Toil` global, no imports needed in route files.\n\n## Entry\n\n`client/toil.tsx` imports the route table + global styles and mounts the app:\n\n```tsx\nimport { routes, layout, notFound } from \"toiljs/routes\";\nimport \"./styles/main.css\";\nToil.mount(routes, layout, notFound);\n```\n\n## API (on `Toil`)\n\n- Components: `Link`, `NavLink`, `Head`\n- Navigation: `navigate`, `useRouter`, `useNavigate`\n- Location: `usePathname`, `useSearchParams`, `useParams`, `useNavigationPending`\n- Data: `useLoaderData` (see [routing.md](./routing.md))\n- Head: `useHead`, `useTitle`, `<Head>`, set the `<title>` / meta per route\n- Realtime: `useChannel`, `connectChannel` (WebSocket to the backend at `/_toil`)\n- IO globals (no `Toil.` prefix): `FastMap`, `FastSet`, `DataWriter`, `DataReader`\n- `parseError(err)` global: message from an unknown caught value (handy in `catch`)\n- `Server` global: the typed RPC surface generated from the server (see [server.md](./server.md))\n- `Server.REST.<controller>.<route>(args)`: a working, typed `fetch` client for your\n `@rest` controllers, e.g. `await Server.REST.todos.getTodo({ params: { id } })` or\n `await Server.REST.todos.add({ body: new AddTodo(\"milk\") })`. `args` is\n `{ params?, body?, query?, headers? }`; returns are typed (`@data` classes are parsed for\n you). The REST client attaches when you import from `shared/server`.\n\n## Head example\n\n```tsx\nToil.useHead({\n title: \"Blog\",\n meta: [{ name: \"description\", content: \"...\" }],\n});\n```\n",
12
- "styling.md": "# Styling\n\nThe app imports one stylesheet from `client/toil.tsx` (e.g. `./styles/main.css`).\n\n## Preprocessors & Tailwind\n\nPick a CSS preprocessor (none / Sass / Less / Stylus) and optionally Tailwind at\n`toiljs create`, or change it later on an existing project:\n\n```sh\ntoiljs configure # interactive\ntoiljs configure --tailwind # add Tailwind\ntoiljs configure --style sass # switch preprocessor\n```\n\n`configure` installs/removes the right packages and rewrites the imports. Tailwind lives\nin its own `styles/tailwind.css` (`@import \"tailwindcss\";`).\n\n## Imports\n\n`.css` / `.scss` / `.sass` / `.less` / `.styl` and image imports (`.svg`, `.png`, …) are\ntyped via `toil-env.d.ts`.\n",
13
- "server.md": "# Server (toilscript WebAssembly)\n\n`server/` is the toilscript source, compiled to WebAssembly by `toilscript`.\n\n- `server/main.ts`, the `@main` entry, exported as the WASM `main`.\n- `server/index.ts`, your functions.\n- `server/tsconfig.json`, extends `toilscript/std/assembly.json` (AssemblyScript/toilscript\n globals like `i32`, not the DOM), so editors resolve server types correctly.\n- `npm run build:server` (or `npm run build`) emits `build/server/release.wasm` and\n regenerates `shared/server.ts` (the typed client RPC module).\n\n## Typed RPC (`@data` / `@remote` / `@service`)\n\nTag server code and the build generates a typed client `Server` surface:\n\n- `@data class X {}`, a serializable struct. Generates a client class with the same fields\n plus `encode`/`decode`; construct it on the client: `import { X } from \"shared/server\"`.\n- `@remote function f(a: T): R`, a client-callable endpoint, becomes `Server.f(a)`.\n- `@service class S { @remote m(...) {} }`, namespaces methods: `Server.s.m(...)`.\n\nOn the client, `Server` is a global (no import) and fully typed; every call is async\n(`Promise<R>`). Inputs/outputs are scalars, arrays, or `@data` classes, both directions.\n\nNote: the client↔server transport is not wired yet, so calling a `Server` method throws\nuntil it lands; the typed surface + codec are generated and ready.\n\n## HTTP REST (`@rest` / `@route`)\n\nTag a class `@rest` and its methods with a verb to expose a real HTTP API. Unlike RPC,\nthe generated client is working `fetch` code (it is just HTTP).\n\n- `@rest(\"api\") class Todos {}`, mounts the controller at `/api` (bare `@rest` `/`).\n- `@get(\"/todos/:id\")` / `@post` / `@del` / `@put` / `@patch` / `@head` / `@options`, verb\n shortcuts; or `@route({ method: Methods.GET, path: \"/todos\", stream: DataStream.JSON })`.\n- A method takes an optional `@data` body + an optional `ctx: RouteContext` (path params via\n `ctx.param(\"id\")`, `ctx.query(...)`, `ctx.header(...)`). It returns either a `@data` type,\n which the compiler encodes per `stream` (`DataStream.JSON` default, or `DataStream.Binary`,\n lossless for large `u64`/bignum), or a `Response` for full control - custom status and\n headers, e.g. `Response.json(value.toJSON().toString()).setHeader(\"cache-control\", \"no-store\")`\n or `Response.notFound()`. (The editor sees the compiler-injected `@data` `toJSON`/`encode`\n members via the toilscript plugin, so serializing into a `Response` is editor-clean.)\n\nEach `@rest` class self-registers; dispatch them from your handler - it composes, it never\ntakes over `handle()`:\n\n```ts\nimport { ToilHandler, Request, Response, Rest } from \"toiljs/server/runtime\";\nexport class App extends ToilHandler {\n public handle(req: Request): Response {\n const hit = Rest.dispatch(req); // try every @rest controller\n if (hit != null) return hit;\n return Response.notFound(); // your own logic / static fallback\n }\n}\n```\n\nFor a REST-only project, `Server.handler = () => new RestHandler()` does the same with no\nboilerplate. On the client: `Server.REST.todos.getTodo({ params: { id } })` (see [client.md](./client.md)).\n\nFor the full reference (`@rest`/verb decorators, `RouteContext`, `Request`, `Response`,\ndispatch + the 404 fallback) see [routing.md](./routing.md).\n",
14
- "ssr.md": "# Server-side rendering (SSR)\n\ntoiljs server-renders a route by splitting it into two halves that the build\nkeeps coherent:\n\n- **the template**, the HTML shell with the dynamic bits punched out (named\n *holes*). It is React's own `renderToStaticMarkup` output with the holes\n removed, precompiled at build time and held (mmap'd) by the edge.\n- **the values**, the hole values for one request (text, attributes, repeated\n rows, headers, status). The wasm guest's `render` entrypoint returns *only*\n these. The edge splices them into the template.\n\nA 32-byte template **hash** travels with the values so the edge can reject a\nguest that was compiled against a different template than the one deployed.\n\nThis split is the whole point. The guest never re-runs React and never emits the\nstatic page bytes, it stamps a tiny `(slot_id, kind, value)` list, so the wasm\nstays small and a request is served about as fast as a static file, while still\ndelivering real first-paint HTML and SEO. The browser then hydrates the spliced\nmarkup in place with no flash and no client re-render, because the holes are\nescaped exactly the way React escapes them, so the bytes match.\n\nThis page is about server-rendered HTML. JSON / binary API endpoints use\n[Routing](./routing.md) and `@rest` (see [Server](./server.md)) instead.\n\nThe running example throughout is the basic example's `/hello` route:\n\n- `examples/basic/client/routes/hello.tsx`, the route (`ssr = true`, holes, loader)\n- `examples/basic/server/SsrHelloRender.ts`, the server `render` + `Ssr.register`\n- `examples/basic/server/_ssr/hello.slots.ts`, the generated `Slot` + `HASH` (gitignored, never hand-edited)\n\n---\n\n## 1. Authoring an SSR route\n\nOpt a route in with `export const ssr = true`. At build time toiljs renders the\npage ONCE (under its real layout chain, with sample loader data) into the\ntemplate, generates the route's typed `Slot` module, and writes the template\nmanifest the edge serves. Routes without `ssr = true` are untouched and render\npurely on the client as before.\n\nMark the dynamic bits with the four hole markers from `toiljs/client`. They are\n**transparent in the browser**, `<Hole>` renders its children, `<Repeat>`\nrenders `each.map(...)`, `<RawHtml>` renders a `dangerouslySetInnerHTML`\nwrapper, `<Island>` renders its children, so the same component is your normal\nclient UI. Only the build extractor and the server `render` treat them\nspecially.\n\n```tsx\nimport { Hole, Island, RawHtml, Repeat, useLoaderData } from 'toiljs/client';\n\nexport const ssr = true;\n\nexport const loader = ({ params }: { params: Record<string, string> }) => ({\n name: params.name ?? 'world',\n blurbHtml: 'Rendered at the <strong>edge</strong>.',\n services: [{ name: 'record', region: 'us-east' }],\n});\n\nexport default function Hello() {\n const d = useLoaderData<typeof loader>();\n return (\n <section className=\"hello\">\n <h1>Hello, <Hole id=\"name\">{d.name}</Hole>!</h1>\n\n <p><RawHtml id=\"blurb\" html={d.blurbHtml} as=\"span\" /></p>\n\n <ul>\n <Repeat id=\"services\" each={d.services}>\n {(s) => (\n <li>\n <strong><Hole id=\"svcName\">{s.name}</Hole></strong>\n <span className=\"hello-region\"><Hole id=\"svcRegion\">{s.region}</Hole></span>\n </li>\n )}\n </Repeat>\n </ul>\n\n <Island>\n <p>Hydrated at {new Date().toLocaleTimeString()}.</p>\n </Island>\n </section>\n );\n}\n```\n\n### The loader at build time\n\nThe build calls your `loader` once with synthesized sample params to obtain\nrepresentative data, then renders the page with it. Only the **shape** of the\ndata matters at build time, it drives which holes exist and (for `<Repeat>`)\ncaptures the row sub-template. The real per-request values come from the\n**server** `render`, not from this loader. Note in particular that `<Repeat>`\nneeds the sample to have **at least one row** so the build can capture the row\nmarkup; an empty array at build time leaves nothing to stamp.\n\n### The four hole markers\n\n| Marker | Prop(s) | Server (build + render) | Browser |\n| --- | --- | --- | --- |\n| `<Hole id>` | `id` | a text insertion point; the guest fills it with the **escaped** value | renders `children` |\n| `<RawHtml id html as?>` | `id`, `html`, `as?` (wrapper tag, default `div`) | emits `<as>…</as>`; the guest fills the inner HTML **verbatim** (you sanitize) | `<as dangerouslySetInnerHTML>` |\n| `<Repeat id each>` | `id`, `each`, child `(item, index) => node` | captures the **one** row sub-template; the guest stamps it per item and concatenates | renders `each.map(children)` |\n| `<Island>` | `children` | renders **nothing** (empty in the server HTML) | renders `children` |\n\n`<RawHtml>` always needs a host element so the server and client DOM agree; that\nis the `as` wrapper (defaults to `div`). The captured `<as>` tag is part of the\ntemplate, only its inner HTML is a hole.\n\n### Attribute holes (`attr()`)\n\nAn attribute value is not a child node, so it cannot be a JSX marker element.\nUse the `attr(id, value)` helper from `toiljs/client` in attribute position\ninstead:\n\n```tsx\nimport { attr } from 'toiljs/client';\n\n<a href={attr('profileUrl', d.url)} class={attr('cls', d.cls)}>…</a>\n```\n\nBrowser: `attr` returns `value` unchanged. Build: it emits an `attr` hole at the\nattribute's byte offset (stripping to an empty value in the `.tmpl`). The server\n`render` fills it with `v.setAttr(Slot.profileUrl, url)` (React-escaped, the same\nas `setText`), and the host splices it between the quotes. It composes with\nliteral text in the same attribute (`` class={`btn ${attr('x', v)}`} ``).\n\n### SSR-safe routes (and `<Island>`)\n\nFor hydration to be byte-clean, the route **and the layouts above it** must\nrender under static markup: use the hole markers and `useLoaderData`, and put\nanything that needs router hooks (`useRouter`, `usePathname`, …) or browser-only\nAPIs (`window`, `Date.now`, …) inside an `<Island>`. An island is empty in the\nfirst paint and appears only after hydration, so it gets no first-paint HTML or\nSEO, which is exactly right for \"client only\" content.\n\nOpting in is always safe: a route (or a layout in its chain) that **throws**\nunder static markup is **skipped at build with a warning** and falls back to\nnormal client rendering. You never get a broken page from adding `ssr = true`;\nworst case you get client rendering and a build warning telling you why.\n\n---\n\n## 2. The server `render`\n\nThe wasm `render(req_ofs, req_len) -> i64` export is surfaced by your\n`server/main.ts` via `export * from 'toiljs/server/runtime/exports'` (the same\nline that surfaces `handle`). At request time it decodes the request, runs the\n`Ssr` router to find a matching render function, serializes that function's\n`SlotValues` into the values envelope, and returns it packed as\n`(ptr << 32) | len`.\n\nA render function takes the `Request`, returns a filled `SlotValues` for a path\nit owns, or `null` to let the next registered renderer try.\n\n```ts\nimport { HtmlBuilder, Request, SlotValues, Ssr } from 'toiljs/server/runtime';\nimport { HASH, Slot } from './_ssr/hello.slots';\n\nfunction renderHello(req: Request): SlotValues | null {\n // The guest re-derives WHICH route this is from the path (the template name\n // is not in the request envelope), exactly as a @rest controller matches its\n // own prefix. req.path includes the query string, so match both forms.\n if (req.path != '/hello' && !req.path.startsWith('/hello?')) return null;\n\n const v = new SlotValues(HASH);\n\n v.setText(Slot.name, greetingName(req)); // escaped\n v.setRaw(Slot.blurb, 'Rendered at the <strong>edge</strong>.'); // verbatim\n\n const rows = new HtmlBuilder();\n for (let i = 0; i < services.length; i++) {\n const s = services[i];\n rows.raw('<li><strong>').text(s.name)\n .raw('</strong><span class=\"hello-region\">').text(s.region)\n .raw('</span></li>');\n }\n v.setRepeat(Slot.services, rows);\n\n return v;\n}\n\nSsr.register(renderHello); // side-effect registration\n```\n\n### Registration is manual; the import is load-bearing\n\n`ssr-codegen` generates ONLY the `Slot` enum and the `HASH`, it does **not**\nemit the render body and does **not** auto-register it. You write `renderHello`\nand call `Ssr.register(renderHello)` yourself.\n\nCrucially, `Ssr.register` runs as a **module side effect**, so the module must\nbe imported somewhere the build reaches. Non-surface files (a plain render\nmodule is not a `@rest`/`@service`/`@data` file) are **not** auto-discovered, so\nyou must `import './SsrHelloRender'` in `server/main.ts`. Forgetting the import\nmeans the renderer never registers, `Ssr.dispatch` returns `null`, and the route\nfalls back to the fail-safe 500.\n\n### What `render` does for a request the router misses\n\nIf no registered renderer matches, the `render` export emits a **fail-safe**\nenvelope: status 500 with a **zeroed** 32-byte hash and no slots (a malformed\nrequest envelope yields the same fail-safe with status 400). The edge rejects\nthe zero hash as a coherence mismatch, so a miss surfaces as a clean error\nrather than a corrupt page.\n\n---\n\n## 3. Reference: `SlotValues`\n\nConstruct it with the route's compiled-in hash: `new SlotValues(HASH)`. Each\nsetter targets a slot id (a `Slot` enum member); the **kind** determines how the\nedge splices it. All setters return `this` for chaining.\n\n| Method | Signature | Escaping | Use |\n| --- | --- | --- | --- |\n| `setText` | `setText(slotId, value: string)` | **React-escaped** | text content (safe by default) |\n| `setRaw` | `setRaw(slotId, html: string)` | **none (verbatim)** | raw HTML, *you* sanitize |\n| `setAttr` | `setAttr(slotId, value: string)` | **React-escaped** | an attribute value |\n| `setRepeat` | `setRepeat(slotId, rows: HtmlBuilder)` | per `HtmlBuilder` calls | a repeat region, pre-stamped row by row |\n| `setHeader` | `setHeader(name, value)` |, | a response header (e.g. `Cache-Control`, `Set-Cookie`) |\n| `setStatus` | `setStatus(code)` |, | the HTTP status (default 200) |\n\n`setText` and `setAttr` escape identically (React escapes text and attributes\nthe same way). Slot ids passed are the `Slot` enum members; AS enums are `i32`,\nso they pass without a cast and are narrowed to `u16` only at encode time.\n\n### `HtmlBuilder`\n\nAssembles a repeat region (or any HTML fragment) with the same escaping\nguarantees. Chain `raw` (verbatim template bytes) and `text` / `attr`\n(React-escaped values):\n\n```ts\nconst rows = new HtmlBuilder();\nfor (let i = 0; i < items.length; i++) {\n rows.raw('<li>').text(items[i]).raw('</li>'); // items[i] is escaped\n}\nv.setRepeat(Slot.list, rows);\n```\n\nYou are hand-writing the row markup, so it must match what `<Repeat>`'s child\nproduces for the same item, the build captured exactly that markup as the row\nsub-template, and the edge inserts your stamped rows verbatim at the region\noffset. Keep the **structure** the same across rows; only the leaf hole values\nvary.\n\n| Method | Signature | Escaping |\n| --- | --- | --- |\n| `raw` | `raw(s: string): HtmlBuilder` | verbatim |\n| `text` | `text(s: string): HtmlBuilder` | React-escaped |\n| `attr` | `attr(s: string): HtmlBuilder` | React-escaped (identical to `text`) |\n\n---\n\n## 4. Escaping (React-exact)\n\n`setText`, `setAttr`, and `HtmlBuilder.text` / `.attr` escape **exactly as\nReact does** (`react-dom/server`'s `escapeTextForBrowser`, regex `/[\"'&<>]/`),\nso the server-rendered markup and the client hydration agree byte-for-byte:\n\n```\n\" → &quot; & → &amp; ' → &#x27;\n< → &lt; > → &gt;\n```\n\nThe detail that bites: `'` becomes `&#x27;` (React's exact choice), **not**\n`&#39;`. If your escaping deviates from this by even one entity, `hydrateRoot`\nsees different markup and React throws a hydration mismatch and re-renders the\nsubtree. The guest's `escapeHtml` and the build's `reactEscapeHtml` are pinned\nto be byte-identical for exactly this reason.\n\n`setRaw` and `HtmlBuilder.raw` do **not** escape, they insert your bytes\nverbatim. That is the right tool for markup you produced or sanitized yourself\n(the same contract as `dangerouslySetInnerHTML`), and the wrong tool for\nanything derived from request input.\n\n---\n\n## 5. The build flow and generated artifacts\n\n`extractTemplates` (driven by `toiljs build`) does, for each `ssr = true` route:\n\n1. loads the route + its layout chain through a short-lived Vite SSR server;\n2. calls the `loader` with sample params to get representative data;\n3. renders the page under its layouts with the markers in **sentinel mode**\n (`__setSsrBuild(true)`), each marker emits a Private-Use-Area sentinel token\n instead of rendering normally;\n4. splices that into the built shell's `<div id=\"root\">` and adds the SSR marker\n `<template id=\"__toil_ssr\"></template>` (this is what the client `mount` looks\n for to switch to `hydrateRoot`);\n5. strips the sentinel tokens, records their **byte offsets**, and writes the\n artifacts.\n\n### Where the artifacts land\n\nFor a route named `<name>` (see below), under `build/client/_ssr/`:\n\n| File | Consumer | Contents |\n| --- | --- | --- |\n| `<name>.tmpl` | edge host (mmap'd) | the stripped static HTML shell, holes removed |\n| `<name>.slots` | edge host | the binary manifest (offsets, ids, kinds, tmpl_len, hash) |\n| `<name>.slots.ts` | guest build | the generated `Slot` enum + `HASH` AssemblyScript module |\n| `templates.json` | index | `[{ route, name, hash }]` for every extracted template |\n\nThe `.tmpl` and `.slots` are then **copied** into the edge host bundle at\n`hosts/edge/_tmpl/<name>.{tmpl,slots}`.\n\nThe build also writes the **server-importable** `Slot` + `HASH` module to\n`server/_ssr/<name>.slots.ts`, the one your `render` imports. It is generated\nand gitignored; never hand-edit it (see the two-pass note below).\n\n### Route name derivation (`routeTemplateName`)\n\nThe `<name>` is a file-safe slug of the route pattern: non-alphanumerics collapse\nto `_`, leading/trailing `_` are trimmed, empty → `index`.\n\n| Route pattern | `<name>` |\n| --- | --- |\n| `/hello` | `hello` |\n| `/` | `index` |\n| `/u/:name` | `u_name` |\n| `/blog/[id]` | `blog_id` |\n\n### The two-pass build: no hand-kept slots\n\nThe final extraction runs **after** the Vite client build (the built shell's\nhashed `<script>`/`<link>` tags are part of the template, so they must be in the\n`HASH`), but the server (guest wasm) is compiled **before** it. A naive build\ntherefore can't generate `<name>.slots.ts` in time for the `render` to import\nit. toiljs closes this with a **two-pass** build, so a clean build needs **zero\nhand-maintained slots**:\n\n1. **Slots pre-pass** (before the server build) renders every `ssr = true`\n route to its `Slot` enum + `HASH` and writes the server-importable module at\n `server/_ssr/<name>.slots.ts`. This is the file your `render` imports, it is\n **generated, gitignored, and never hand-edited**.\n2. The server compiles against that module.\n3. The client (Vite) builds.\n4. **Final extraction** re-renders against the real built shell and rewrites\n `server/_ssr/<name>.slots.ts` with the authoritative `HASH`. If the hash\n rotated since the pre-pass, the build recompiles the server **once** so the\n guest bakes the deployed hash.\n\nSo authoring an SSR route is just the route + the `render`; the `Slot` / `HASH`\nmodule is entirely build-managed. (On an unchanged rebuild the pre-pass reuses\nthe prior build's shell, so the hashes already match and step 4's recompile is\nskipped.)\n\n---\n\n## 6. Hash coherence and the values envelope\n\nEvery values envelope carries the guest's compiled-in 32-byte `HASH`. The edge\ncompares it against the deployed template's hash and **rejects a mismatch** with\na fail-safe 500 rather than splicing values into the wrong template. A mismatch\nmeans deploy skew: the guest was built against one version of the template and a\ndifferent one is deployed.\n\nThe hash is `sha256(tmpl || \\0 || canonicalManifest(slots))`, so any change to\nthe static HTML, a hole's id/kind, or the repeat nesting rotates it.\n\nThe guest serializes `SlotValues` to this little-endian, no-padding layout (the\nedge decodes it and splices against the template manifest):\n\n```\nu16 status\n[32] template_hash\nu16 n_headers\n for each header: u16 name_len, u16 val_len, name bytes, val bytes\nu16 n_slots\n for each value: u16 slot_id, u8 kind, u32 value_len, value bytes\n```\n\n`kind` is `0=text, 1=raw, 2=attr, 3=repeat`. The host keys values by `slot_id`\nand inserts each at the **manifest-fixed** offset, so the guest can never choose\n*where* bytes land, only what they are. If a value cannot be represented\n(a count or length overflows its field width, or the hash is the wrong size),\nthe encoder writes the same fail-safe 500/zero-hash envelope instead of corrupt\nbytes.\n\nThe matching `.slots` manifest the host reads is a 46-byte header\n(`\"TSLT\"` magic, u16 version, u16 flags, u32 tmpl_len, 32-byte hash, u16 n_slots)\nfollowed by 8-byte entries (`u32 offset, u16 slot_id, u8 kind, u8 reserved`).\n\n---\n\n## 7. Dev server and testing\n\n`toiljs dev` serves SSR routes the same way the edge does. It runs the **real**\n`render` export (`WasmServerModule.dispatchRender`), decodes the values\nenvelope, and splices the values into the route's template, so you get real\nserver-rendered HTML locally (`curl` a route, or view source), which then\nhydrates in place. The dev template is extracted once at startup against the\nlive (Vite-transformed) dev shell rather than a built one; a route's per-request\n**values** are always live, but a change to its **markup** needs a dev restart\nto re-extract. A fail-safe envelope (no renderer matched) falls back to client\nrendering.\n\nThe end-to-end test (`test/ssr-render.test.ts`) drives the same `dispatchRender`\npath directly: it calls `dispatchRender({ path: '/hello' })`, decodes the\nenvelope, asserts the slots, and splices against the built `hello.tmpl`.\n\n---\n\n## 8. Complete worked example: `/hello`\n\nThis is the full, copy-pasteable chain. All four files are real and tested.\n\n### `client/routes/hello.tsx` (the route)\n\n```tsx\nimport { Hole, Island, RawHtml, Repeat, useLoaderData } from 'toiljs/client';\n\nexport const ssr = true;\n\nexport const metadata: Toil.Metadata = {\n title: 'Edge SSR',\n description: 'A server-rendered greeting, filled at the edge.',\n};\n\ninterface Service { name: string; region: string; }\ninterface GreetingData {\n name: string;\n blurbHtml: string;\n services: Service[];\n}\n\n// Build-time sample data, only the SHAPE matters; the real per-request values\n// come from the SERVER render. The repeat sample needs at least one row.\nexport const loader = ({ params }: { params: Record<string, string> }): GreetingData => ({\n name: params.name ?? 'world',\n blurbHtml: 'Rendered at the <strong>edge</strong> from a tiny values envelope.',\n services: [\n { name: 'record', region: 'us-east' },\n { name: 'unique', region: 'eu-west' },\n { name: 'counter', region: 'ap-south' },\n ],\n});\n\nexport default function Hello(): React.JSX.Element {\n const d = useLoaderData<typeof loader>();\n return (\n <section className=\"hello\">\n <h1>Hello, <Hole id=\"name\">{d.name}</Hole>!</h1>\n\n <p className=\"hello-blurb\">\n <RawHtml id=\"blurb\" html={d.blurbHtml} as=\"span\" />\n </p>\n\n <h2>Service snapshot</h2>\n <ul className=\"hello-services\">\n <Repeat id=\"services\" each={d.services}>\n {(s: Service) => (\n <li>\n <strong><Hole id=\"svcName\">{s.name}</Hole></strong>\n <span className=\"hello-region\"><Hole id=\"svcRegion\">{s.region}</Hole></span>\n </li>\n )}\n </Repeat>\n </ul>\n\n <Island>\n <p className=\"hello-island\">\n Hydrated in your browser at {new Date().toLocaleTimeString()}.\n </p>\n </Island>\n </section>\n );\n}\n```\n\n### `server/_ssr/hello.slots.ts` (generated by the build; do not edit)\n\n```ts\n// AUTO-GENERATED by toil (edge SSR). Do not edit.\n\n/** Stable hole ids for this route's template (document order). */\nexport enum Slot {\n name = 0,\n blurb = 1,\n services = 2,\n}\n\n/** Coherence hash (32 bytes), written by the build's slots pre-pass; the host\n * rejects a response whose hash != the deployed template. */\nexport const HASH: StaticArray<u8> = [\n 0xcb, 0x12, 0x5e, 0x19, 0x46, 0x32, 0x58, 0x25, 0xd3, 0xf0, 0x44, 0xc5, 0x41, 0x0c, 0x34, 0x3b,\n 0x69, 0xd3, 0x62, 0xb3, 0x24, 0x25, 0x79, 0xc4, 0x76, 0x89, 0xfb, 0x25, 0x6e, 0x35, 0x02, 0x31,\n];\n```\n\n(Only the **top-level** holes get a `Slot` id, `name`, `blurb`, `services`. The\nnested `svcName` / `svcRegion` live inside the repeat row sub-template, which the\nguest stamps with `HtmlBuilder`, so they are not separate slots.)\n\n### `server/SsrHelloRender.ts` (the render)\n\n```ts\nimport { HtmlBuilder, Request, SlotValues, Ssr } from 'toiljs/server/runtime';\nimport { HASH, Slot } from './_ssr/hello.slots';\n\nclass Service {\n constructor(public name: string, public region: string) {}\n}\n\n/** Pull `?name=...`, defaulting to `world` (matches the route loader default). */\nfunction greetingName(req: Request): string {\n const q = req.path.indexOf('?');\n if (q < 0) return 'world';\n const parts = req.path.substring(q + 1).split('&');\n for (let i = 0; i < parts.length; i++) {\n if (parts[i].startsWith('name=')) {\n const v = parts[i].substring(5);\n return v.length > 0 ? v : 'world';\n }\n }\n return 'world';\n}\n\nfunction renderHello(req: Request): SlotValues | null {\n if (req.path != '/hello' && !req.path.startsWith('/hello?')) return null;\n\n const v = new SlotValues(HASH);\n\n // Text hole, React-escaped (so ?name=<a>&b is safe).\n v.setText(Slot.name, greetingName(req));\n\n // Raw hole, verbatim; a fixed, trusted blurb (no request data).\n v.setRaw(Slot.blurb, 'Rendered at the <strong>edge</strong> from a tiny values envelope.');\n\n // Repeat, stamp the captured row markup once per item. The row sub-template\n // is <li><strong>{svcName}</strong><span class=\"hello-region\">{svcRegion}</span></li>;\n // .text(...) escapes each nested hole exactly as React does.\n const services: Service[] = [\n new Service('record', 'us-east'),\n new Service('unique', 'eu-west'),\n new Service('counter', 'ap-south'),\n ];\n const rows = new HtmlBuilder();\n for (let i = 0; i < services.length; i++) {\n const s = services[i];\n rows.raw('<li><strong>').text(s.name)\n .raw('</strong><span class=\"hello-region\">').text(s.region)\n .raw('</span></li>');\n }\n v.setRepeat(Slot.services, rows);\n\n return v;\n}\n\n// Side-effect registration: main.ts imports this module so the build compiles\n// it in and this renderer joins the SSR router.\nSsr.register(renderHello);\n```\n\n### `server/main.ts` (the load-bearing import)\n\n```ts\nimport { Server } from 'toiljs/server/runtime';\n// ... other surface imports ...\n\n// Edge SSR: importing the render module compiles it in and self-registers its\n// /hello renderer. Without this import the renderer never registers.\nimport './SsrHelloRender';\n\nServer.handler = () => new AppHandler();\n\nexport * from 'toiljs/server/runtime/exports'; // surfaces `handle` AND `render`\n```\n\nThe spliced first-paint HTML for `GET /hello` is byte-identical to what React\nrenders for the same data:\n\n```html\n<section class=\"hello\"><h1>Hello, world!</h1>\n<p class=\"hello-blurb\"><span>Rendered at the <strong>edge</strong> from a tiny values envelope.</span></p>\n<h2>Service snapshot</h2>\n<ul class=\"hello-services\">\n<li><strong>record</strong><span class=\"hello-region\">us-east</span></li>\n<li><strong>unique</strong><span class=\"hello-region\">eu-west</span></li>\n<li><strong>counter</strong><span class=\"hello-region\">ap-south</span></li>\n</ul></section>\n```\n\nThe `<Island>` is empty here (no first paint); it fills in after hydration.\n\n---\n\n## 9. Pitfalls and debugging\n\n- **Route skipped at build (warning, no SSR).** The route or a layout above it\n threw under static markup, almost always a router hook (`useRouter`,\n `usePathname`, …) or a browser-only API rendered outside an `<Island>`. The\n build prints `toil: SSR skipped <pattern> (...)` and the route falls back to\n client rendering. Move the offending content into an `<Island>`.\n\n- **Hash mismatch / clean 500 after editing a template.** Any change to the\n page's static markup, a hole id/kind, or the repeat structure rotates the\n `HASH`. The host rejects a stale guest hash. A normal `toiljs build`\n regenerates `server/_ssr/<name>.slots.ts` and rebakes the guest, so this only\n surfaces from a partial or stale deploy (a guest built against a different\n template than the one deployed), never from hand-copied slots.\n\n- **Hydration mismatch (flash / React re-render in the browser).** Two common\n causes. (1) The client **loader** does not reproduce the values the server\n `render` stamped. Hydration re-renders the route with the loader's data, so for\n any request-derived hole the loader must derive the **same** value the server\n `render` does (e.g. read the same `?query` / param). The two are separate\n sources (the client loader is TypeScript; the server `render` is the wasm\n guest), so keeping them in sync is the author's contract; if the client cannot\n reproduce a value, put that content in an `<Island>`. (2) A marker (or a\n non-static node) rendered outside an `<Island>`, or hole escaping that does not\n match React's (e.g. emitting `&#39;` instead of `&#x27;`, or using `setRaw`\n where the client would escape). Keep dynamic text in `<Hole>` / `setText` and\n client-only content in `<Island>`.\n\n- **Route renders client-side only even though `ssr = true`.** You forgot to\n `import './SsrHelloRender'` in `server/main.ts`, so `Ssr.register` never ran\n and `Ssr.dispatch` returns `null`. Add the import. (Plain render modules are\n not auto-discovered the way `@rest`/`@service` files are.)\n\n- **`setRaw` injecting unsanitized request data.** `setRaw` is verbatim, never\n pass it anything derived from request input you have not sanitized. Use\n `setText` for request-derived text.\n\n- **Empty `<Repeat>` sample at build.** The build captures the row sub-template\n from the **first** sample row. If your build-time `loader` returns an empty\n array for a `<Repeat>`, there is no row to capture. Give the build sample at\n least one representative row.\n</content>\n</invoke>\n",
15
- "rpc.md": "# RPC and the generated client\n\nThe server build with `--rpcModule shared/server.ts` scans your decorated\nsurface (`@data`, `@user`, `@service`/`@remote`, `@rest`) and emits one\nTypeScript module: a typed `Server` proxy, the `@data` codec classes, the REST\nfetch client, and the `getUser()` accessor. The client imports that file and\ncalls the server with full type-safety and editor autocomplete. The file is\nregenerated on every server build, so it never drifts from the server.\n\n```sh\ntoilscript --target release --rpcModule shared/server.ts\n```\n\n## `@service` and `@remote`\n\nA `@service` class exposes its `@remote` methods as callable RPC. A top-level\n`@remote` function is exposed directly.\n\n```ts\n@service\nclass Stats {\n @remote\n public playerCount(): i32 { return store.size; }\n}\n\n@remote\nfunction ping(n: i32): i32 { return n + 1; }\n```\n\nThe generated client surfaces these on the global `Server` proxy. A service is\nkeyed by its class name with the first letter lowercased (`Stats` `stats`):\n\n```ts\nawait Server.stats.playerCount(); // Promise<number>\nawait Server.ping(42); // Promise<number>\n```\n\nArguments and return values are `@data`-typed; scalars map to TS as below.\n\n## The generated `Server` surface\n\n`shared/server.ts` declares a global `Server` whose shape is, schematically:\n\n```ts\ndeclare global {\n const Server: {\n // top-level @remote functions\n ping(n: number): Promise<number>;\n\n // @service classes (keyed by lowercased name)\n readonly stats: {\n playerCount(): Promise<number>;\n };\n\n // @rest controllers, under REST\n readonly REST: {\n readonly players: {\n get(args: { params: { id: string | number | bigint }; query?: …; headers?: }): Promise<Response>;\n create(args: { body: NewPlayer; query?: …; headers?: }): Promise<Player>;\n };\n };\n };\n}\n```\n\n### Type mapping (ToilScript → TypeScript)\n\n| ToilScript | TypeScript |\n| --- | --- |\n| `u8`,`u16`,`u32`,`i8`,`i16`,`i32`,`f32`,`f64` | `number` |\n| `u64`,`i64`,`u128`,`i128`,`u256`,`i256` | `bigint` |\n| `bool` | `boolean` |\n| `string` | `string` |\n| a `@data` class `T` | `T` (the emitted class) |\n| `T[]` | `T[]` |\n\n64-bit-and-larger integers are `bigint` on the client and travel as decimal\nstrings on the JSON wire, so they are exact at any magnitude.\n\n### Emitted `@data` classes\n\nEach `@data` (and `@user`) class becomes an exported TS class with the fields, a\ndefaulted constructor, and the matching codec:\n\n```ts\nexport class Player {\n constructor(public username = '', public admin = false, public score = 0n) {}\n encodeInto(w: DataWriter): void { /* */ }\n encode(): Uint8Array { /* dataId prefix + fields */ }\n static decodeFrom(r: DataReader): Player { /* */ }\n static decode(buf: Uint8Array): Player { /* */ }\n static dataId(): number { /* FNV-1a of \"Player\" */ }\n static fromJSONValue(v: any): Player { /* revive, 64-bit from strings */ }\n toJSONValue(): any { /* 64-bit as decimal strings */ }\n}\n```\n\nThe codec is byte-compatible with the server's `@data` codec, so binary bodies\nround-trip exactly between client and wasm.\n\n## The REST fetch client\n\nEvery `@rest` route also gets a typed fetch wrapper under `Server.REST.<key>`,\nkeyed by the controller name lowercased. The call argument is an object:\n\n```ts\nServer.REST.players.create({\n body: new NewPlayer('alice'), // present iff the route takes a body\n // params: { id: 7 }, // present iff the path has :params\n query: { ref: 'home' }, // optional\n headers: { 'x-trace': traceId }, // optional\n});\n```\n\n- If the route has no params and no body, the whole argument is optional\n (`args?`).\n- The wrapper builds the URL (substituting `:params`, appending `query`),\n `fetch`es with `credentials` as configured, throws on a non-2xx status, and\n decodes the response into the route's return type.\n- A route declared to return `Response` resolves to the raw `fetch` `Response`,\n so you can stream or inspect headers yourself.\n\n```ts\nconst player = await Server.REST.players.create({ body: new NewPlayer('alice') });\n// ^? Player\n```\n\n## `getUser()`\n\nWhen the server declares a `@user` class, the generated module also exports a\ntyped, no-argument `getUser()` that reads the readable companion cookie and\ndecodes it with the generated codec:\n\n```ts\nimport { getUser } from './shared/server';\n\nconst user = getUser(); // Account | null, fully typed\n```\n\nThis is **display-only**: the server re-verifies the signed session on every\n`@auth` request. See [Auth](./auth.md) for the full picture.\n\n## Notes\n\n- `shared/server.ts` is generated; never edit it by hand. Re-run the server\n build (or `toiljs dev`, which does it on save) to refresh it.\n- The `Server` proxy is declared as an ambient global on the client; the runtime\n implementation is provided by toiljs. The REST client and `getUser` are real\n exported values in the generated module.\n",
16
- "tiers.md": "# Deployment tiers\n\nA Toil app's server runs across several deployment **tiers** from one source\ntree. Each tier has a different lifetime and placement on the edge, and compiles\ninto its own WebAssembly artifact. You write one project; `toiljs build` decides\nwhich entries belong to which tier and emits one `.wasm` per tier. You opt into a\ntier purely by adding its entry file and surface decorator; nothing else changes.\n\n## The tiers\n\n| Entry (`server/`) | Surface | Artifact | Tier | Lifetime / placement |\n| ----------------- | -------------------------------- | ---------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `main.ts` | `@rest` / `@service` / `@remote` | `build/server/release.wasm` | **L1** request | A fresh handler per request, anywhere on the edge. |\n| `main.stream.ts` | `@stream` | `build/server/release-stream.wasm` | **L2/L3** stream | One resident box per connection, pinned to a worker via QUIC connection-id steering; its state survives every event. See [Streams](./streams.md). |\n| `main.daemon.ts` | `@daemon` / `@scheduled` | `build/server/release-cold.wasm` | **L4** daemon | Exactly one leader-elected box per domain (warm standby, at-most-once failover) firing `@scheduled` tasks. See [Daemon](./daemon.md). |\n\nThe three tiers differ in how long a box lives and how many of it exist:\n\n- **L1 request** is stateless. A `@rest` handler's fields reset each request,\n because a fresh box serves each one, anywhere on the edge.\n- **L2/L3 stream** is resident per connection. A `@stream` box is created when a\n connection opens, lives for its lifetime, and is torn down on close, so its\n fields persist across every event.\n- **L4 daemon** is a single elected leader per domain - the global coordination\n tier - running recurring background work on a cadence.\n\n## How the build works\n\n`toiljs build` runs one toilscript pass per tier, handing each pass only the\nentries that belong to it. Tier membership is decided by the surface decorator or\nby the entry naming convention:\n\n- a runtime-export entry that is **not** `*.stream.ts` or `*.daemon.ts` is the\n **request** entry (`main.ts`), which compiles `@rest` / `@service` / `@remote`;\n- `*.stream.ts` is the **stream** entry, which compiles `@stream`;\n- `*.daemon.ts` is the **daemon** entry, which compiles `@daemon` / `@scheduled`.\n\nPlain `@data` and helper modules carry no tier of their own, so they are shared\ninto every artifact. Routing each entry to exactly one tier is what keeps\n`release.wasm` free of `stream_dispatch` and keeps the daemon artifact free of\nthe request `handle`.\n\nEach entry is a thin file that imports its tier's modules and re-exports the\nright runtime hooks. The stream and request entries re-export the request runtime\nexports; the daemon entry does not, because a cold artifact exposes\n`daemon_start` / `scheduled_tick`, not `handle`:\n\n```ts\n// server/main.stream.ts - the L2/L3 stream entry\nimport { revertOnError } from 'toiljs/server/runtime/abort/abort';\nimport './streams/Echo';\n\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\n```ts\n// server/main.daemon.ts - the L4 daemon entry\nimport { revertOnError } from 'toiljs/server/runtime/abort/abort';\nimport './daemon/Jobs';\n\n// NOTE: no `export *` from the request runtime - a cold artifact exposes\n// daemon_start/scheduled_tick, not the request `handle`.\nexport function abort(message: string, fileName: string, line: u32, column: u32): void {\n revertOnError(message, fileName, line, column);\n}\n```\n\nA single build produces the artifacts side by side:\n\n```sh\n$ ls build/server/*.wasm\nbuild/server/release.wasm # L1 request (exports: handle)\nbuild/server/release-stream.wasm # L2/L3 stream (exports: stream_dispatch)\nbuild/server/release-cold.wasm # L4 daemon (exports: daemon_start, scheduled_tick)\n```\n\n## Single-artifact default\n\nA project with no `@stream` and no `@daemon` surface keeps the default\nsingle-artifact build - just `build/server/release.wasm`. The stream and daemon\ntiers are opt-in: add `main.stream.ts` (and a `@stream` class) to get\n`release-stream.wasm`, add `main.daemon.ts` (and a `@daemon` class) to get\n`release-cold.wasm`. Existing request-only apps build exactly as before.\n\n## When to use each tier\n\n- **L1 request** for request/response and RPC: `@rest` controllers, `@service` /\n `@remote` callable surface. The default tier; most code lives here.\n- **L2/L3 stream** for stateful, long-lived connections where per-connection\n state must survive across events - the resident box is pinned to one worker for\n the connection's lifetime.\n- **L4 daemon** for scheduled and coordination work: rollups, cleanup, polling an\n upstream, anything that should run exactly once per domain on a cadence rather\n than per request.\n\n```ts\n// server/streams/Echo.ts - L2/L3: the box is resident, so `count` persists.\n@stream('echo')\nclass Echo {\n private count: i32 = 0;\n\n @connect onConnect(): void {\n this.count = 0;\n }\n @message onMessage(): void {\n this.count = this.count + 1;\n }\n @close onClose(): void {\n /* box torn down after this hook */\n }\n}\n```\n\n```ts\n// server/daemon/Jobs.ts - L4: one leader per domain runs this hourly.\n@daemon\nclass Jobs {\n @scheduled('1h')\n hourly(): void {\n // Recurring background work: rollups, cleanup, polling an upstream, ...\n }\n}\n```\n\n## See also\n\n- [Streams](./streams.md) - the `@stream` surface and the L2/L3 tier.\n- [Daemon](./daemon.md) - the `@daemon` surface and the L4 tier.\n- [Routing](./routing.md) - `@rest` controllers on the L1 request tier.\n- [RPC](./rpc.md) - `@service` / `@remote` and the generated client.\n",
17
- "streams.md": "# Streams\n\nA `@stream` declares a long-lived, stateful protocol handler over WebTransport -\nthe **L2/L3** (regional / continental) stream tier of the Toil edge. Unlike a\n`@rest` route, which is a fresh handler per request, a `@stream` is a **resident\nWebAssembly box per connection**: it is created when the connection opens, lives\nfor the whole connection, and is torn down on close. State stored on its fields\n**persists across events**, because it is the same box every time.\n\n```ts\n@stream('echo')\nclass Echo {\n private count: i32 = 0;\n\n @connect\n onConnect(): void {\n this.count = 0;\n }\n\n @message\n onMessage(): void {\n this.count = this.count + 1;\n }\n\n @close\n onClose(): void {}\n}\n```\n\n## Declaring a stream\n\n`@stream(name)` marks a class as a stream handler and mounts it at the given\nname/route. The class becomes a resident box; its fields are the connection's\nstate.\n\n```ts\n@stream('echo') // mounted at /echo\nclass Echo { /* ... */ }\n```\n\nA stream lives on the **L2/L3 stream tier** and its default scope is **Regional\n(L2)**. See [Tiers](./tiers.md) for the full tier model.\n\n## Lifecycle hooks\n\nA stream method is a lifecycle hook, chosen by its decorator. All hooks are\noptional - declare only the ones you need; a missing hook is a no-op.\n\n| Decorator | Fires when |\n| --- | --- |\n| `@connect` | the connection opens (the box has just been created). |\n| `@message` | an inbound frame arrives. |\n| `@close` | the connection closes gracefully (the box is torn down after this hook). |\n| `@disconnect` | the transport is lost abruptly. |\n\nThe `Echo` example above shows why state survives: `count` is set to `0` in\n`@connect`, incremented on every `@message`, and the increments **accumulate**.\nThat is only possible because the same resident box handles every event for the\nconnection. A `@rest` handler's fields would reset on each request, since a\nfresh handler is constructed per request.\n\nDistributed stream channels are not part of the live v1 ABI. The edge rejects\nstream artifacts that declare a channel hook until the channel fan-out runtime\nexists.\n\n## Placement\n\nA `@stream` is distributed across the eligible L2/L3 stream nodes and pinned to\n**ONE worker** for the connection's lifetime via QUIC connection-id steering. The\nconnection always lands on the same worker, so the box - and the state on its\nfields - survives every event. You do not manage placement; the edge steers each\nconnection to its resident box automatically.\n\n## The entry: `main.stream.ts`\n\nThe stream surface has its own entry, `server/main.stream.ts`, distinct from the\nrequest entry (`server/main.ts`). It re-exports the WASM runtime exports and\nimports the `@stream` classes, which pulls their compiler-generated\n`stream_dispatch` export into the artifact.\n\n```ts\nimport { revertOnError } from 'toiljs/server/runtime/abort/abort';\n\nimport './streams/Echo';\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\nThis entry compiles into its **own artifact**, `build/server/release-stream.wasm`\n- the resident stream box - separate from the request build,\n`build/server/release.wasm`. Add a stream as you grow by importing it here:\n\n```ts\nimport './streams/Echo';\n```\n\n## Build\n\n`toiljs build` produces `release-stream.wasm` automatically when the project\ndeclares a `@stream` surface. The single build runs one toilscript pass per tier,\nhanding each pass only the entries that belong to it, so `release.wasm` never\ncontains `stream_dispatch` and the stream artifact never contains the request\n`handle`. Plain `@data` and helper modules are shared into every artifact.\n\n```sh\n$ toiljs build\n$ ls build/server/*.wasm\nbuild/server/release.wasm # L1 request (exports: handle)\nbuild/server/release-stream.wasm # L2/L3 stream (exports: stream_dispatch)\nbuild/server/release-cold.wasm # L4 daemon (exports: daemon_start, scheduled_tick)\n```\n\nSee [Tiers](./tiers.md) for how the three artifacts map to the deployment tiers.\n\n## Reading and replying to messages\n\n`@message` receives the inbound frame as a `StreamPacket` and returns a\n`StreamOutbound`. `StreamPacket.bytes()` is the raw frame payload;\n`StreamOutbound.reply(bytes)` stages one frame back to the client (return an empty\n`StreamOutbound` to accept the frame without replying). The same resident box\nhandles every frame, so state on its fields persists across messages.\n\n```ts\n@message\nreply(packet: StreamPacket): StreamOutbound {\n return StreamOutbound.reply(packet.bytes()); // echo the bytes back\n}\n```\n\n## Typed messages\n\nBy default a `@message` payload is **raw bytes**. Opt into a decoded `@data` value\nwith `@stream({ message: T })`: the `@message` hook then receives the named `@data`\nclass, decoded from the frame for you. The reply stays raw (`StreamOutbound`).\n\n```ts\n@data\nclass ChatMsg { text: string = ''; }\n\n@stream({ message: ChatMsg })\nclass Chat {\n @message\n onMessage(msg: ChatMsg): StreamOutbound { // decoded @data, not raw bytes\n return StreamOutbound.reply(new TextEncoder().encode(msg.text));\n }\n}\n```\n\n## The client\n\nA `@stream` class is reachable from the browser as `Server.Stream.<ClassName>`. The\ntyped client is generated into `shared/server.ts` (the same place `Server.REST`\nlands), so no manual wiring is needed. `connect()` opens a WebSocket to the class's\nroute and resolves a channel:\n\n```ts\nconst chat = await Server.Stream.Chat.connect();\nchat.onMessage((bytes) => { /* a reply frame, always raw bytes */ });\nchat.send(new ChatMsg('hello')); // a typed stream: send() encodes the @data for you\nchat.onClose((code) => { /* a 0x02xx stream close code */ });\nchat.close();\n```\n\n- The channel key is the **class name** (`Server.Stream.Chat`); it connects to the\n class's mount route (`/Chat`).\n- A **raw** `@stream` channel sends `Uint8Array`; a **typed** `@stream({ message: T })`\n channel sends the `@data` class and encodes it on the wire for you.\n- The inbound reply is **always raw bytes** - the server's `StreamOutbound` is raw.\n- `connect()` resolves once the upgrade completes; a `@connect` reject (or any\n later server close) surfaces through `onClose(code)`.\n\n---\n\nSee also: [Tiers](./tiers.md), [Daemon](./daemon.md), [Routing](./routing.md).\n",
18
- "daemon.md": "# Daemon\n\n`@daemon` declares a single, leader-elected background worker for your domain -\nthe **L4** (global) coordination tier of the Toil edge. Where a `@rest` handler\nis a fresh instance per request and a `@stream` box is one instance per\nconnection, there is exactly **one** daemon per domain at a time. The edge keeps\na warm standby ready and fails over at-most-once, so the daemon is the right\nplace for work that must happen once globally rather than once per request.\n\n```ts\n@daemon\nclass Jobs {\n @scheduled('1h')\n hourly(): void {\n // Runs once an hour on the elected leader. Put recurring background work\n // here (rollups, cleanup, polling an upstream, ...).\n }\n}\n```\n\n## `@daemon` classes\n\n`@daemon` marks a class as the domain's background worker. The class is resident:\nit is created once on the elected leader and lives for as long as that leader\nholds the lease, so its fields persist across scheduled runs (a `@rest`\nhandler's fields would reset every request).\n\nExactly one daemon instance runs per domain at any moment. A second node stays a\nwarm standby and only becomes active if the current leader's lease lapses. You do\nnot start, stop, or place the daemon yourself - the edge elects the leader and\ndrives it.\n\n## `@scheduled`\n\nA `@scheduled` method declares a task that fires on a cadence, always on the\n**elected leader**. The single string argument is the cadence:\n\n```ts\n@scheduled('1h')\nhourly(): void { /* ... */ }\n```\n\n- **Interval strings** like `'1h'` fire on that fixed period.\n- **Cron expressions** are also supported when you need a wall-clock schedule\n rather than a fixed interval.\n\nA class can declare several `@scheduled` methods; each runs on its own cadence.\nBecause only the leader fires them, a task runs once per domain per tick, not\nonce per node.\n\n## The daemon entry\n\nThe daemon surface has its own entry module, `server/main.daemon.ts`. It imports\nthe `@daemon` classes so the compiler-generated `daemon_start` / `scheduled_tick`\nexports are pulled into the artifact:\n\n```ts\n// server/main.daemon.ts\nimport { revertOnError } from 'toiljs/server/runtime/abort/abort';\n\nimport './daemon/Jobs';\n\n// The abort hook (the daemon box reports a trap through it). NOTE: unlike main.ts /\n// main.stream.ts, the daemon entry does NOT re-export the request runtime - a cold\n// artifact exposes daemon_start/scheduled_tick, not the request `handle`.\nexport function abort(message: string, fileName: string, line: u32, column: u32): void {\n revertOnError(message, fileName, line, column);\n}\n```\n\nNote what is **not** here: unlike `main.ts` and `main.stream.ts`, the daemon\nentry does not `export * from 'toiljs/server/runtime/exports'`. A daemon (cold)\nartifact exposes `daemon_start` and `scheduled_tick`, not the request `handle`.\nAdd a daemon as you grow by importing its module here.\n\n## Build\n\n`toiljs build` runs one toilscript pass per tier and hands each pass only the\nentries that belong to it. When the project declares a `@daemon` / `@scheduled`\nsurface, the daemon pass compiles `server/main.daemon.ts` into its own artifact,\n`build/server/release-cold.wasm`:\n\n```sh\n$ ls build/server/*.wasm\nbuild/server/release.wasm # L1 request (exports: handle)\nbuild/server/release-stream.wasm # L2/L3 stream (exports: stream_dispatch)\nbuild/server/release-cold.wasm # L4 daemon (exports: daemon_start, scheduled_tick)\n```\n\nSo `release.wasm` never contains `scheduled_tick` and the daemon artifact never\ncontains the request `handle`. Plain `@data` and helper modules are shared into\nevery artifact. See [Tiers](./tiers.md) for how the three artifacts are produced\nfrom one source tree.\n\n## Use cases\n\nThe daemon is the once-per-domain tier, so it fits work you want to happen\nglobally on a cadence rather than per request:\n\n- **Periodic rollups** - aggregate counters or events into summaries.\n- **Cleanup** - expire stale rows, prune logs, reclaim resources.\n- **Polling an upstream** - pull from an external API on a schedule.\n- **Global coordination** - any task that must run exactly once across the\n domain, not once per node.\n\n## Failover\n\nScheduling is **at-most-once**. A `@scheduled` task fires on whichever node\ncurrently holds the leader lease. If that leader fails, the warm standby takes\nover and fires the **subsequent** runs; the edge does not retry or duplicate the\ntick that was in flight when the leader was lost. This trades exactly-once\ndelivery for the guarantee that two nodes never run the same scheduled tick at\nonce, so design tasks to be safe to skip an occasional run and to be idempotent\nwhere a missed run matters.\n\n---\n\nSee also:\n\n- [Tiers](./tiers.md) - the three deployment tiers and how one source tree\n compiles to a separate artifact per tier.\n- [Streams](./streams.md) - the L2/L3 `@stream` tier (one resident box per\n connection).\n",
19
- "data.md": "# Data codec (`@data`)\n\n`@data` turns a plain class into a typed, versionable value with a deterministic\nbinary codec and a JSON codec. It is the backbone of request/response bodies,\nRPC arguments, sessions, and anything you persist. The same class becomes a\nfully typed client type in the generated `shared/server.ts` (see\n[RPC](./rpc.md)).\n\n```ts\n@data\nclass Player {\n username: string = '';\n admin: bool = false;\n score: u64 = 0;\n}\n```\n\nFrom that the compiler synthesizes, on the class:\n\n- `encode(): Uint8Array` / `static decode(buf): T`, the binary codec (with a\n 4-byte type id prefix).\n- `encodeInto(w: DataWriter)` / `static decodeFrom(r: DataReader)`, the codec\n without the type-id frame, for nesting.\n- `toJSON()` / `static fromJSON(v)`, the JSON codec (64-bit-and-larger integers\n as decimal strings, so they survive `JSON.parse` exactly).\n- `static dataId(): u32`, a stable FNV-1a hash of the class name, written as the\n type-id prefix by `encode()`.\n\nFields may be scalars (`u8`..`u256`, `i8`..`i256`, `f32`, `f64`, `bool`),\n`string`, a nested `@data` class, or an array `T[]` of any of these. Give every\nfield a default; the generated decoder and the client constructor use them.\n\n## Using `@data` in routes\n\nIn a **JSON** route, a `@data` parameter is revived from the parsed body and a\n`@data` return value is serialized with `toJSON()`. In a **Binary** route, the\nparameter is `decode`d from the raw body and the return value is `encode`d. The\nroute's stream mode (see [Routing](./routing.md#data-streams)) picks which.\n\n```ts\n@post('/') // JSON route\npublic create(input: NewPlayer): Player { /* input from JSON, Player to JSON */ }\n\n@route({ method: Methods.POST, path: '/blob', stream: DataStream.Binary })\npublic blob(input: FileData): FileResult { /* input.decode, result.encode */ }\n```\n\n## The binary codec: `DataWriter` / `DataReader`\n\nWhen you need to lay out bytes yourself, custom bodies, session payloads,\nchallenge messages, use the codec directly. It lives in the `data` module:\n\n```ts\nimport { DataWriter, DataReader } from 'data';\n```\n\nThe codec has a byte-for-byte identical TypeScript implementation in\n`toiljs/io` (`src/io/codec.ts`), so the client can read and write the exact same\nwire format the wasm guest does.\n\n### `DataWriter`\n\nEvery writer method returns the writer for chaining.\n\n| Method | Signature | Wire format |\n| --- | --- | --- |\n| `writeU8` / `writeI8` | `(v): DataWriter` | 1 byte |\n| `writeU16` / `writeI16` | `(v): DataWriter` | 2 bytes, little-endian |\n| `writeU32` / `writeI32` | `(v): DataWriter` | 4 bytes, LE |\n| `writeU64` / `writeI64` | `(v): DataWriter` | 8 bytes, LE |\n| `writeF32` / `writeF64` | `(v): DataWriter` | 4 / 8 bytes, IEEE-754 LE |\n| `writeBool` | `(v): DataWriter` | 1 byte (`1`/`0`) |\n| `writeBytes` | `(b: Uint8Array): DataWriter` | `u32` length (LE) + raw bytes |\n| `writeString` | `(s: string): DataWriter` | `u32` length (LE) + UTF-8 bytes |\n| `writeU128` / `writeI128` | `(v): DataWriter` | two `u64` limbs (lo, hi) |\n| `writeU256` / `writeI256` | `(v): DataWriter` | four `u64` limbs (lo1, lo2, hi1, hi2) |\n| `length` | `(): i32` | bytes written so far |\n| `toBytes` | `(): Uint8Array` | an exact-length copy of the buffer |\n\n### `DataReader`\n\nReads are bounds-safe: an over-read never traps. It returns a zero/empty default\nand sets the public `ok` flag to `false`. Check `ok` after a sequence of reads\nto detect a truncated or malformed buffer.\n\n| Method | Signature | On over-read |\n| --- | --- | --- |\n| `readU8` / `readI8` | `(): u8 / i8` | `0` |\n| `readU16`..`readU64`, `readI16`..`readI64` | `(): integer` | `0` |\n| `readF32` / `readF64` | `(): f32 / f64` | `0` |\n| `readBool` | `(): bool` | `false` |\n| `readBytes` | `(): Uint8Array` | empty array |\n| `readString` | `(): string` | `\"\"` |\n| `readU128`/`readI128`/`readU256`/`readI256` | `(): bignum` | `0` |\n| `remaining` | `(): i32` | bytes left unread |\n| `ok` | `bool` (field) | `false` once any read over-ran |\n\n### Example\n\n```ts\nimport { DataWriter, DataReader } from 'data';\n\n// Write: u8 version, str name, u64 score, bytes blob\nconst out = new DataWriter()\n .writeU8(1)\n .writeString('alice')\n .writeU64(1234)\n .writeBytes(payload)\n .toBytes();\n\n// Read it back\nconst r = new DataReader(out);\nconst version = r.readU8();\nconst name = r.readString();\nconst score = r.readU64();\nconst blob = r.readBytes();\nif (!r.ok) return Response.badRequest('truncated');\n```\n\n## Notes\n\n- **Endianness.** The AS guest codec is little-endian. The TypeScript `toiljs/io`\n codec defaults to little-endian and also accepts a per-call `be` flag for\n big-endian network formats; keep both ends on the same setting.\n- **Field order is the format.** The binary layout is exactly the field\n declaration order. Reordering fields, or changing a type, is a breaking format\n change. Add new fields at the end and bump a leading version byte if you need\n to evolve a hand-rolled payload.\n- **`encode()` carries a type id.** The 4-byte `dataId()` prefix lets a decoder\n confirm it is reading the type it expects. `encodeInto`/`decodeFrom` skip the\n frame for nesting one `@data` value inside another.\n",
20
- "caching.md": "# Caching\n\ntoiljs can cache a response at the edge (shared, across users) and instruct the\nbrowser to cache it too. You opt in per route, either declaratively with the\n`@cache` decorator or imperatively with `Response.cache(...)`. The edge keys a\ncached entry by host, method, path, and body hash, and honors a per-entry TTL.\n\n## `@cache` decorator\n\nAnnotate a route method; the compiler appends the cache directive to whatever\n`Response` the route returns, so it composes with every return shape (a\n`Response`, a `void` 204, or an auto-encoded `@data` value).\n\n```ts\n@cache(60) // 60 minutes at the edge\n@cache(60, 300) // + 5 minutes (300s) in the browser\n@cache(60, 300, true) // + private scope (per-user caches only)\n@cache(60, 300, true, true) // + cache even for authenticated requests\n@get('/leaderboard')\npublic top(): Standings { /* */ }\n```\n\nArguments must be integer or boolean literals; a non-literal argument makes the\ndecorator degrade safely to \"not cached\" rather than miscompile.\n\n## `Response.cache(...)`\n\nThe same controls are available imperatively, which is what `@cache` lowers to:\n\n```ts\npublic cache(\n edgeTtlMinutes: u16,\n browserTtlSeconds: u32 = 0,\n privateScope: bool = false,\n allowAuth: bool = false,\n): Response\n```\n\n```ts\nreturn Response.json(body).cache(60, 300);\n```\n\n`cacheFor(minutes)` is the common shorthand for \"edge only, no browser caching\":\n\n```ts\nreturn Response.bytes(blob).cacheFor(5);\n```\n\n## Parameters\n\n| Parameter | Meaning |\n| --- | --- |\n| `edgeTtlMinutes` | How long the edge may serve the cached response. Clamped to a 24-hour maximum. |\n| `browserTtlSeconds` | `max-age` for the browser. `0` (default) means the browser does not cache. |\n| `privateScope` | Marks the response `private`: only per-user caches (the browser), never a shared edge/CDN cache. |\n| `allowAuth` | Permit caching a response to an authenticated request. Off by default (see safety rails). |\n\n## Safety rails\n\nThe cache layer refuses to store anything unsafe, regardless of the directive:\n\n- **5xx** responses are never cached, a server error is transient, and `@cache`\n wraps the whole route, so a `@cache`d route that hits a blip returns its 500\n carrying the directive; caching it would serve the failure for the full TTL.\n **2xx, 3xx, and 4xx are cacheable** (a redirect or a `404`/`410` is a\n deterministic function of the request key);\n- a response that sets a **`Set-Cookie`** is never cached;\n- a response to an **authenticated** request is not cached unless you pass\n `allowAuth = true`, this prevents one user's personalized response from being\n served to another;\n- the edge TTL is **clamped to 24 hours**.\n\nBecause `@auth` guards and body-decode run before the cache directive is applied,\nan unauthorized request is rejected with 401 before anything is cached, and a\ncached entry is only ever produced from a handler that actually ran.\n\nCaching is **always opt-in.** A response with no `Dacely-Cache-Control` directive\n(i.e. no `@cache` / `Response.cache(...)`) is never stored, there is no blind\n\"cache every GET\" mode, because an automatic window cannot tell a personalized\nresponse from a public one and would key it without a per-user component.\n\n## Memory bounds and disk spill\n\nThe edge cache is per-core and hard-capped so it can never exhaust node memory.\nIt has two tiers:\n\n- **RAM tier**, small, short-TTL responses. Bounded by a per-core byte budget\n (each core holds at most ~128 MB) plus an entry-count cap; an insert that would\n exceed the budget drops expired entries first, then evicts the soonest-to-expire\n ones. A response over ~256 KB does not go in the RAM tier.\n- **Disk tier (spill)**, when the operator enables `--spill-dir`, a **big**\n (over the ~256 KB RAM cap) or **long-TTL** (≥ 10 min) cacheable response is\n written to disk instead and served back zero-RAM via a memory map, the same way\n static files are served. This keeps the RAM tier for the hot working set while\n still caching large bodies and long-lived entries. Writes (and unlinks) are\n offloaded to a sibling thread so they never stall the request path; a separate\n per-core disk budget caps total spilled bytes, with the same expiry + eviction.\n If spill is not enabled, a big response is simply not cached (reported as not\n stored by the `Dacely-Cache` tag).\n\nFrom a tenant's point of view nothing changes: you still just set a\n`Dacely-Cache-Control` directive (via `@cache` / `Response.cache(...)`). The edge\ndecides RAM vs disk; both honor the same TTL and the same safety rails above.\nExpiry is enforced on read (a past-TTL entry is a miss) and reclaimed on the next\ninsert that needs room. Nothing persists across a process restart.\n\n## Choosing TTLs\n\n- Public, slow-changing data (a leaderboard, a catalog): a few minutes of edge\n TTL plus a short browser TTL removes most of the load.\n- Per-user data: set `privateScope` so it never lands in a shared cache, and\n prefer a small or zero edge TTL.\n- Anything with a `Set-Cookie` or behind `@auth`: leave it uncached unless you\n have thought through `allowAuth` and are certain the body is identical for\n every authorized caller.\n",
21
- "ratelimit.md": "# Rate limiting\n\nThe `@ratelimit` decorator throttles **any** `@rest` route, a login, a signup, a\npublic API, an email trigger, anything. It is enforced at the edge, before your\nhandler runs, and keyed by default on the connecting client's **unspoofable** IP,\nso it works as an abuse / brute-force control out of the box.\n\nIt composes with the other route decorators and is independent of email or auth.\n\n## Using `@ratelimit`\n\nAdd it to a route alongside the verb decorator:\n\n```ts\nimport { Response, RouteContext } from 'toiljs/server/runtime';\n\n@rest('auth')\nclass Auth {\n // At most 5 login attempts per 60 seconds per client IP.\n @ratelimit(RateLimit.SlidingWindow, 5, 60)\n @post('/login')\n login(ctx: RouteContext): Response {\n // ... only runs if under the limit ...\n return Response.text('ok\\n');\n }\n}\n```\n\n`@ratelimit(strategy, limit, window)`:\n\n- **`strategy`**, a `RateLimit` value (ambient global, no import):\n `RateLimit.FixedWindow`, `RateLimit.SlidingWindow`, or `RateLimit.TokenBucket`.\n- **`limit`** and **`window`**, integer literals whose meaning depends on the\n strategy (see below).\n\nWhen a request is over the limit the edge returns **`429 Too Many Requests`**\nwith a **`Retry-After`** header (whole seconds), and your handler never runs. The\nguard runs **before `@auth`**, so unauthenticated floods are limited too.\n\n> Both arguments must be **integer literals** and the strategy a `RateLimit`\n> member (or a bare integer tag). A malformed decorator emits no guard rather\n> than miscompiling, the same fail-safe rule as `@cache`.\n\n## Strategies\n\n| Strategy | `limit`, `window` mean | Behavior |\n| --- | --- | --- |\n| `FixedWindow` | `limit` events per `window` seconds | Cheapest. Counts in aligned wall-clock buckets; a caller hammering a boundary can briefly get up to ~2× `limit` across two adjacent windows. |\n| `SlidingWindow` | `limit` events per `window` seconds | Smooths the fixed-window boundary by weighting the previous window. Best general choice for \"N per period\". |\n| `TokenBucket` | `limit` = burst size, `window` = refill **per second** | Allows an initial burst of `limit`, then a steady `window` tokens/sec. Good for bursty-but-bounded APIs. |\n\nExamples:\n\n```ts\n// 100 requests / minute, smoothed:\n@ratelimit(RateLimit.SlidingWindow, 100, 60)\n\n// Burst of 20, then 5 per second sustained:\n@ratelimit(RateLimit.TokenBucket, 20, 5)\n\n// Exactly 3 per hour, cheapest:\n@ratelimit(RateLimit.FixedWindow, 3, 3600)\n```\n\n## How requests are keyed\n\nBy default the limiter keys on the **client IP**, specifically the TCP peer\naddress the edge observed (`ctx.clientIp()`), **not** a header like\n`X-Forwarded-For`, which a client can forge. That makes it a real abuse control:\na caller can't reset their bucket by spoofing a header.\n\nThe count is **exact across all 14 edge workers** (a given IP always maps to one\nauthoritative shard), so the limit is global per route, not per worker. Only\nroutes that opt in with `@ratelimit` ever pay anything, the lock-free fast path\nfor everything else is untouched.\n\n> Each rate-limited route has its own independent limiter, a limit on `/login`\n> does not consume the budget of `/signup`.\n\n## Notes and limits\n\n- **Route-level only.** Put `@ratelimit` on each route you want limited; there is\n no controller-wide form yet (unlike `@auth`).\n- **Keyed on IP.** The decorator keys on the peer IP today. (A per-user / custom\n key, limiting by account instead of IP, exists in the runtime but is not yet\n exposed through the decorator.)\n- **In dev.** `toiljs dev` runs a single-process mirror of the same three\n strategies, so a limited route behaves the same locally as on the edge.\n\n## See also\n\n- [Email](./email.md), `@ratelimit` pairs well with email triggers (verification\n codes, password resets) to blunt abuse.\n- [Auth, sessions, and `@user`](./auth.md), `@ratelimit` runs before the `@auth`\n guard, so it protects the login itself.\n",
22
- "auth.md": "# Auth, sessions, and `@user`\n\ntoiljs ships **Toil PQ-Auth**: a post-quantum password login where the password\nnever leaves the browser and the server stores only public verifier material.\nOn top of it sit HMAC-signed session cookies, a `@auth` route guard, and a\n`@user` type that makes the signed-in user available - fully typed, no type\nargument - on both the server (`AuthService.getUser()`) and the generated client\n(`getUser()`).\n\n> **Status.** PQ-Auth is a hybrid construction (see [What it is](#what-it-is)).\n> It is opt-in and **not hardened to production yet** - the example storage is a\n> dev stand-in, the secrets are dev placeholders, and the composition has not had\n> an external cryptographic review. See [`docs/auth-todo.md`](./auth-todo.md) for\n> the remaining work before it backs real credentials.\n\n`AuthService` is an ambient global (no import). The pieces:\n\n- **`@user`** - declares the authenticated user's shape and registers it as *the* user type.\n- **`@auth`** - guards a route (or a whole `@rest` class): a valid session is required or `401`.\n- **`AuthService`** - the server runtime: the PQ-Auth crypto, plus mint/read/clear a session and `getUser()`.\n- **client `Auth` + generated `getUser()`** - run the login from the browser and read the user for display.\n\n---\n\n## What it is\n\nA password is a weak, low-entropy secret. PQ-Auth turns it into a strong,\n**post-quantum** credential and proves possession to the server without the\nserver (or anyone on the wire, or a future quantum adversary) ever seeing\nanything they can replay. It is built from three independent ideas, each\ndefending a specific attack:\n\n| Layer | Primitive | Defends against |\n| --- | --- | --- |\n| **Keyed salt** | OPRF (RFC 9497, ristretto255-SHA512) | A breached server (or a passive observer) **precomputing** a password dictionary. |\n| **Credential** | Argon2id → ML-DSA-44 (FIPS 204) keypair | The password ever crossing the wire; a stolen verifier being usable without an expensive per-guess attack. |\n| **Mutual auth + key** | ML-KEM-768 (FIPS 203) | A phishing/MITM server impersonating the real one; a session with no key to bind to. |\n\nThe password is stretched into a signing keypair entirely client-side; only the\n**public** key is registered. Login is a challenge-response signature *plus* a\nkey encapsulation, so both parties authenticate each other. Authentication\n(ML-DSA) and key agreement (ML-KEM) are post-quantum; the keyed-salt OPRF is\nclassical ristretto255 (the one non-PQ layer - a quantum break of it degrades to\na post-breach offline attack, no worse than a plain salt, while defeating\nprecomputation for everyone else).\n\n### Why a keyed salt (the OPRF)\n\nA normal salted hash (`Argon2id(password, salt)`) lets anyone who learns the\nsalt - including a future attacker who simply asks the login endpoint for it -\n**precompute** a dictionary offline and crack the stored verifier the instant\nthey breach it. PQ-Auth replaces the salt with the output of a **server-keyed**\nOPRF:\n\n```\noprfOutput = OPRF_finalize(password, OPRF_evaluate(k_user, blind(password)))\nseed = Argon2id(oprfOutput, salt)\n```\n\nThe client **blinds** the password (so the server learns nothing about it),\nthe server **evaluates** the blinded element under a per-user key `k_user`\nderived from a server-secret master seed, and the client **unblinds** to recover\na deterministic, high-entropy `oprfOutput`. Because `k_user` is a server secret,\n**no offline work is possible until that secret leaks** - precomputation is\nimpossible, and even a passive observer who captures a login learns nothing.\nThe per-user key (`k_user = DeriveKeyPair(masterSeed, username)`) means two\naccounts with the same password get different outputs - no cross-account\npassword-equality leak.\n\n### Why a password-derived signing key\n\n`seed = Argon2id(oprfOutput, salt)` deterministically expands into an\n**ML-DSA-44 keypair**. The client registers only the 1312-byte **public** key;\nthe secret key and seed are zeroized the instant signing is done. The server\nstores the public key as a verifier and can only ever *verify* - it never holds\na secret (`crypto.mldsa_verify` is verify-only on the edge). A full server breach\nyields public keys, not passwords; recovering a password still requires an\noffline Argon2id dictionary attack **and** the leaked OPRF master seed.\n\n### Why ML-KEM (mutual auth + session key)\n\nA signature proves the *client* to the server, but nothing proves the *server*\nto the client. PQ-Auth pins the server's static **ML-KEM-768 public key** in the\nclient. At login the client **encapsulates** a shared secret to that key; only\nthe genuine server (holding the matching secret key) can **decapsulate** it. Both\nsides derive the same session key and the server returns a confirmation tag the\nclient checks - so a phishing/MITM server that lacks the secret key cannot\ncomplete the handshake.\n\n---\n\n## Flow at a glance\n\n```mermaid\nsequenceDiagram\n autonumber\n actor U as User\n participant C as Browser\n participant S as Edge wasm\n participant DB as Your store\n\n rect rgb(14, 21, 32)\n Note over U,DB: Register, password never leaves the browser\n U->>C: Auth.register(username, password)\n C->>S: POST /auth/register/start (username, blinded)\n S->>S: OPRF-evaluate under k_user, issue salt and KDF params\n S-->>C: salt, params, evaluated\n C->>C: finalize OPRF, Argon2id, ML-DSA-44 keypair, sign PoP\n C->>S: POST /auth/register/finish (username, publicKey, regProof)\n S->>S: verifyRegister(publicKey, PoP)\n S->>DB: store Account (username, salt, params, publicKey)\n S-->>C: ok\n end\n\n rect rgb(22, 15, 31)\n Note over U,DB: Login, mutual authentication\n U->>C: Auth.login(username, password)\n C->>S: POST /auth/login/start (username, blinded)\n S->>DB: store challenge (cid, nonce, iat, exp)\n S-->>C: cid, aud, salt, params, nonce, iat, exp, evaluated\n C->>C: finalize OPRF, derive seed, ML-DSA keypair, ML-KEM encapsulate, sign M\n C->>S: POST /auth/login/finish (cid, ct, signature)\n S->>DB: atomic consume challenge(cid)\n S->>S: rebuild M, verifyLogin, decapsulate, derive K, build confirm\n alt signature valid\n S-->>C: ok, sessionToken, serverConfirm, Set-Cookie\n C->>C: re-derive K, check serverConfirm, server authenticated\n else invalid or unknown user\n S-->>C: 401 generic, anti-enumeration\n end\n end\n\n rect rgb(13, 25, 18)\n Note over U,DB: Guarded request, the @auth guard\n U->>C: open or call an @auth route\n C->>S: request, cookies sent automatically\n S->>S: @auth verifies HMAC and expiry on __Host-toil_sess\n alt valid session\n S->>S: handler runs, AuthService.getUser() returns the @user\n S-->>C: 200\n else missing or invalid\n S-->>C: 401 before handler and body-decode\n end\n end\n```\n\n### The signed transcript\n\nThe login message `M` the client signs (and the server rebuilds from its own\nstored values) is a single fixed binary layout - no JSON, no version negotiation:\n\n```\nu8 tag = 1\nstr sub (username)\nstr aud (service audience; server constant)\nbytes cid (challenge id)\nbytes nonce (32 random bytes, server-issued)\nu64 iat, u64 exp (challenge validity window)\nbytes ct (ML-KEM ciphertext)\nu32 memKiB, iterations, parallelism (Argon2id params)\nbytes serverKemKeyId (SHA-256 of the server KEM public key)\n```\n\nSigning over all of this binds the login to: the exact challenge (so it can't be\nreplayed - and `cid` is consumed atomically), the **ciphertext** (so a MITM can't\nswap the key encapsulation), the **KDF params** (so a downgrade can't be slipped\npast the signature), and the **server key identity** (so it commits to which\nserver key was used). The mutual-auth tag is then:\n\n```\nK = HMAC-SHA256(sharedSecret, \"toil-session-key-v1\" || SHA-256(M))\nconfirm = HMAC-SHA256(K, \"toil-server-confirm-v1\" || SHA-256(M))\n```\n\n`K` is the authenticated session key, derived from the KEM shared secret and\nbound to the transcript. Only a server that decapsulated correctly derives the\nsame `K`, so the client checking `confirm` proves the server's identity. (`K` is\nthe handle for future channel binding; binding the session *cookie* to the\ntransport needs the TLS exporter, which the wasm guest can't see - a follow-up.)\n\n### Anti-enumeration\n\n`login/start` returns a fully-formed response for **every** username: it always\nOPRF-evaluates (a real `k_user` for known users, a deterministic decoy key for\nunknown ones) and returns a **deterministic per-user salt** and constant params.\nKnown and unknown users are byte-indistinguishable, and the eventual signature\nsimply fails for a non-account. Failures return one generic `401`.\n\n---\n\n## `@user`\n\nMark one class per program as the user type. It becomes a `@data` codec (so it\nserializes into the session) and the return type of `getUser()` everywhere.\n\n```ts\n@user\nclass Account {\n username: string = '';\n admin: bool = false;\n score: u64 = 0;\n}\n```\n\nThere is exactly one `@user` per program; a second is a compile error.\n\n## `@auth`\n\nPut `@auth` on a route, or on the `@rest` class to guard every route in it. The\ngenerated dispatcher checks for a valid, unexpired session **before** the handler\nruns (and before any body-decode or cache write); without one it returns `401`.\n\n```ts\n@rest('session')\nclass Session {\n @auth\n @get('/me')\n public me(): Response {\n const u = AuthService.getUser(); // Account | null, auto-typed\n if (u == null) return Response.text('no session\\n', 401);\n return Response.bytes(new DataWriter()\n .writeString(u.username).writeBool(u.admin).writeU64(u.score).toBytes());\n }\n}\n```\n\n`@auth` on the class form guards all routes in it.\n\n## `AuthService` (server)\n\nA global namespace. Session methods read the ambient request\n(`Server.currentRequest`), so `getUser()`/`hasSession()` take no argument and are\nonly meaningful during a dispatch.\n\n### PQ-Auth crypto\n\nStartup config (call once in `main.ts`; identical on every edge instance; never\nin a client bundle):\n\n| Member | Notes |\n| --- | --- |\n| `setSecret(secret)` | HMAC secret for session cookies. |\n| `setOprfSeed(seed)` | 32-byte OPRF master seed; per-user keys derive from this + the username. |\n| `setServerKemSecretKey(sk)` | Server static ML-KEM-768 secret key (2400 B) used to decapsulate. |\n| `setServerKemPublicKey(pk)` | The matching public key (1184 B) for `serverKemKeyId`; it is embedded in `sk` at bytes `[1152, 2336)`, so you can pass `sk.slice(1152, 2336)`. |\n\nPer-request building blocks:\n\n| Member | Notes |\n| --- | --- |\n| `oprfEvaluate(username, blinded)` | OPRF server step: blind-evaluate under `k_user` derived from the seed + username. Returns the 32-byte evaluated element. |\n| `mlkemDecapsulate(ct)` | Recover the 32-byte shared secret from the client ciphertext with the server secret key. |\n| `buildLoginMessage(sub, aud, cid, nonce, iat, exp, ct, memKiB, iterations, parallelism, serverKemKeyId)` | The canonical login message `M`. Call it with the server's **own** stored values, never client-echoed fields. |\n| `verifyLogin(publicKey, message, signature)` | Verify the ML-DSA login signature under `LOGIN_CONTEXT`. |\n| `serverKemKeyId()` | `SHA-256(serverKemPublicKey)` - the key id bound into `M`. |\n| `sha256(data)` | SHA-256, for the transcript hash. |\n| `deriveSessionKey(sharedSecret, transcriptHash)` | `K = HMAC(sharedSecret, SESSION_KEY_LABEL || transcriptHash)`. |\n| `serverConfirmTag(sessionKey, transcriptHash)` | The mutual-auth tag `HMAC(K, SERVER_CONFIRM_LABEL || transcriptHash)`. |\n| `buildRegisterMessage(username, publicKey)` / `verifyRegister(...)` | Registration proof-of-possession (under `REGISTER_CONTEXT`). |\n| `LOGIN_CONTEXT` / `REGISTER_CONTEXT` | `qauth:login:v1` / `qauth:register:v1` - FIPS 204 signing contexts. |\n| `PUBLIC_KEY_LEN` `SIGNATURE_LEN` `KEM_*` `SHARED_SECRET_LEN` `OPRF_*` | Fixed sizes. |\n\nThe full register/login orchestration (the four binary endpoints, the\nanti-enumeration decoy, the atomic challenge-consume) is in\n`examples/basic/server/routes/Auth.ts`. **Storage is the app's** - a tenant's\nwasm memory is wiped per request, so accounts and challenges live in an external\nstore, and challenge-consume **must** be an atomic fetch-and-delete (a\nread-then-delete race makes a captured login replayable). The example uses a\n**dev-only** KV for this; production wires toildb (see `docs/auth-todo.md`).\n\n### Sessions\n\n| Member | Signature | Notes |\n| --- | --- | --- |\n| `getUser()` | `(): AuthUser \\| null` | The signed-in user, decoded from the verified session, auto-typed to your `@user`. |\n| `hasSession()` | `(): bool` | Whether the request carries a valid, unexpired session. What `@auth` calls. |\n| `mintSession(userData, ttlSecs?)` | `(Uint8Array, u64=86400): Cookie` | Signed `__Host-toil_sess` cookie carrying `user.encode()`. HttpOnly, Secure, SameSite=Lax. |\n| `clearSession()` / `userCookie(...)` / `clearUserCookie()` | | Logout; the readable `__Secure-toil_user` companion (display-only); clear it. |\n\nThe session payload is `u8 version || u64 iat || u64 exp || bytes userData`, sealed\nwith HMAC-SHA256. The HttpOnly `__Host-toil_sess` is the **only** cookie the\nserver trusts; the readable `__Secure-toil_user` exists solely so the client\n`getUser()` can show a name without a round-trip and must never gate anything.\n\n## The client half\n\n```ts\nimport { Auth } from 'toiljs/client';\n\nawait Auth.register(username, password); // OPRF + Argon2id + ML-DSA keypair, send only the public key + PoP\nawait Auth.login(username, password); // + ML-KEM encapsulate; resolves only if the server's confirm tag verifies\n```\n\n`login` resolves **only after** the client verifies the server's confirmation tag\n- so a resolved `login` means mutual authentication succeeded. The secret key,\nseed, and shared secret are zeroized as soon as they are used. There is no\nrecovery: the password *is* the key (see `docs/auth-todo.md` for the recovery\nwork).\n\nThe generated `shared/server.ts` also exports a typed, no-argument client\n`getUser()` that reads the readable companion cookie. It is **display-only and\nuntrusted** - a client can forge it, fooling only its own UI. The server\nre-verifies the signed session on every `@auth` request, so authorization never\ndepends on the readable cookie.\n\n## Security checklist\n\n- Set real secrets in `main.ts`: `setSecret`, `setOprfSeed`, and the server KEM\n keypair - per-deployment, identical on every instance, never in a client\n bundle. The defaults are insecure DEV placeholders.\n- Pin **your** server KEM public key in the client and rotate it; the example\n ships a throwaway dev keypair.\n- Use a production Argon2id cost (≥ 256 MiB, ≥ 3 iterations); the demo is tuned\n for browser responsiveness.\n- Back accounts/challenges with a shared store and make challenge-consume atomic.\n- Rate-limit `register` and `login` (online guessing is not stopped by the\n offline-attack resistance).\n- Always verify server-side. The server `getUser()` decodes a verified,\n expiry-checked session; the client `getUser()` does not and must not gate\n anything.\n- This is an unreviewed hybrid composition - get a cryptographic review before it\n backs real credentials. Tracked in [`docs/auth-todo.md`](./auth-todo.md).\n",
23
- "environment.md": "# Environment variables & secrets\n\n`Environment` gives a tenant **per-app environment variables and secrets**, set\nout of band (a dashboard, like GitHub Actions) so the deployed `.wasm` carries\n**no credentials**. It is read-only from app code, there is no `set`; values are\nconfigured on the deployment side, never from the module.\n\n```ts\nimport { Response, RouteContext } from 'toiljs/server/runtime';\n\n@rest('cfg')\nclass Cfg {\n @get('/')\n show(ctx: RouteContext): Response {\n const base = Environment.get('PUBLIC_API_BASE'); // plain var, or null\n const key = Environment.getSecure('STRIPE_KEY'); // secret, or null\n // Use `key` to call a third party; never log it or return it to a client.\n return Response.text(base != null ? base : 'unset');\n }\n}\n```\n\n`Environment` is a global, no import needed (like `EmailService` / `AuthService`).\n\n## Two disjoint buckets\n\nJust like GitHub Actions' `vars` vs `secrets`:\n\n- **`Environment.get(key)`** reads **plain vars**, non-sensitive config (a public\n API base URL, a feature flag, a region). Returns the string, or `null`.\n- **`Environment.getSecure(key)`** reads **secrets**, sensitive values (a\n third-party API key). Returns the string, or `null`.\n\nThe buckets are **disjoint**: a secret is **never** returned by `get()`, and a\nplain var is never returned by `getSecure()`. That keeps a secret from leaking\nthrough a code path that logs the result of a `get()`. Keys are case-sensitive,\nexact-match.\n\n> Secrets you read with `getSecure` are plaintext in your module at runtime\n> (that's the point, you need them to call out). Don't log them, don't put them\n> in a response, and don't copy them into a client bundle.\n\n## What is NOT here\n\nFramework-reserved namespaces (today: **email** provider config) are **host-only**,\nresolved and used in Rust where the framework needs them, and **never exposed to\nthe `.wasm`**. There is no `Environment.email`; you configure email in the\n`[email]` block of the same env file and the platform uses it for you (see\n[Email](./email.md)). The env imports only ever see your own `vars` / `secrets`.\n\n## Where values live\n\nVars and secrets live in **two separate dotenv (`.env`) files**, so the disjoint\nsplit is structural and the secrets file can be locked down on its own. On the\nedge they are per host, **out of `hosts/`** (so the config watcher never sees a\ncredential), the dashboard / edge database replaces them later:\n\n```bash\n# $TOIL_ENV_DIR/<host>.env (default dir /run/toil/env)\nPUBLIC_API_BASE=https://api.example.com # -> Environment.get(\"PUBLIC_API_BASE\")\nREGION=eu\n\n# $TOIL_ENV_DIR/<host>.env.secrets (mode 0600)\nSTRIPE_KEY=sk_live_xxx # -> Environment.getSecure(\"STRIPE_KEY\")\n\n# host-only email config, reserved TOIL_EMAIL_* keys, NEVER exposed to the .wasm\nTOIL_EMAIL_ENABLED=true\nTOIL_EMAIL_PROVIDER=resend\nTOIL_EMAIL_FROM=noreply@example.com\nTOIL_EMAIL_API_KEY=re_xxx\n```\n\nEach file is plain dotenv: `KEY=value` per line, `#` comments, optional `export`,\noptional quotes. Keys with the reserved **`TOIL_`** prefix are framework/host-only\nand are stripped from BOTH guest buckets, a tenant can never read them via\n`get`/`getSecure` (see [Email](./email.md) for `TOIL_EMAIL_*`).\n\nOn the edge, env is loaded **lazily** (the first time your code reads it) into a\n**bounded, shared, read-only cache** with idle eviction: the data lives in one\nplace and is never copied per request, a host that never reads env costs nothing,\nsecrets are wiped when a host goes cold, and a flood of requests to many distinct\nhosts can never grow memory without bound.\n\n## In dev\n\n`toiljs dev` reads `.env` (vars) and `.env.secrets` (secrets) at your project\nroot, and overlays `process.env` as plain vars for convenience. Both are\ngitignored by the scaffold. The ABI is identical to the edge, so code that runs\nin dev runs on the edge.\n\n```bash\n# .env (vars)\nPUBLIC_API_BASE=http://localhost:4000\n\n# .env.secrets (secrets; 0600; gitignored)\nSTRIPE_KEY=sk_test_xxx\n```\n",
24
- "email.md": "# Email\n\ntoiljs can send transactional email from a route handler. A handler calls\n`EmailService.send(...)` (or a typed `EmailTemplate` / `Emails.*` from the\n`emails/` folder, or the stateless `TwoFactor` helper); the edge hands the\nmessage to a single off-core mailer thread that talks to the provider over the\nkernel network (never the worker cores), and **suspends** the wasm call until the\nprovider responds, so a slow send never blocks the worker.\n\nEverything here is an ambient **global** (no import), like `crypto` and\n`AuthService`. A tenant that never sends email pulls none of it into its build.\n\n- **`EmailService`**, send one email.\n- **`EmailTemplate`**, a reusable template with `{{placeholder}}` substitution\n (plain text and/or HTML).\n- **`emails/*.tsx`**, author emails as React components; the build renders them\n to static HTML and generates a typed `Emails.<Name>.send(...)`.\n- **`TwoFactor`**, stateless email verification codes (2FA / confirm), no DB.\n\n> **The one rule of HTML email:** email clients run **no JavaScript** and strip\n> `<style>`/external CSS. So HTML email is a static, inline-styled string, and\n> any \"rendering\" (React, CSS files) happens at **build time**, not at send time.\n> See [React email templates](#react-email-templates).\n\n## Configure email\n\nEmail is a **framework-reserved namespace of the tenant's environment**, the\nsame out-of-band [Environment](./environment.md) store that backs\n`Environment.get` / `getSecure`, but the `[email]` block is **host-only**: it is\nread and used in Rust (the off-core mailer) and is **never exposed to the\n`.wasm`**. The provider key never lives in the deployed module or in the\ninotify-watched `hosts/<host>.toml`.\n\nOn the edge today it lives in the tenant's env secrets file,\n`$TOIL_ENV_DIR/<host>.env.secrets` (default dir `/run/toil/env`), kept `0600` and\n**out of `hosts/`** so the config watcher never sees a credential (the dashboard /\nedge DB replaces this file later). Email config is a set of **reserved\n`TOIL_EMAIL_*` keys**, host-only, stripped from the guest buckets, so a tenant\ncan never read them via `Environment.getSecure`:\n\n```bash\n# $TOIL_ENV_DIR/<host>.env.secrets (mode 0600; NOT under hosts/, NOT in the .wasm)\n\nTOIL_EMAIL_ENABLED=true\nTOIL_EMAIL_FROM=you@example.com # validated; single address, no CRLF\nTOIL_EMAIL_PROVIDER=resend # resend | gmail | smtp\nTOIL_EMAIL_API_KEY=re_xxxxxxxxxxxx # the provider credential\nTOIL_EMAIL_MAX_PER_MIN=60 # per-host send budget: rolling 1-minute cap\nTOIL_EMAIL_MAX_PER_DAY=0 # per-host send budget: rolling 24-hour cap (0 = unlimited)\nTOIL_EMAIL_MAX_PER_RECIPIENT_PER_HOUR=5 # anti-abuse cap per recipient\n```\n\nThe same file also carries the tenant's own secrets (and `<host>.env` their plain\nvars; see [Environment](./environment.md)); the `TOIL_EMAIL_*` keys are just the\nreserved namespace the framework consumes.\n\nWhen `enabled` is `false` (the default) the host has no email capability and\n`EmailService.send` returns `Disabled`. The env is loaded **lazily** (on the\nfirst send) and the `api_key` is materialized into a zeroizing secret in host\nmemory, never written to logs or `/_admin`. A malformed `[email]` block is\ntreated as \"no email\" (the host returns `Disabled`); validate config on the\ndashboard before deploying.\n\n### Providers\n\n**Resend** (`provider = \"resend\"`), a JSON API; `api_key` holds the API key.\n\n**Gmail** (`TOIL_EMAIL_PROVIDER=gmail`), SMTP with Gmail defaults:\n`smtp.gmail.com`, port `587` (STARTTLS), username = `from`. `TOIL_EMAIL_API_KEY`\nholds a Gmail **App Password** (create one at\n<https://myaccount.google.com/apppasswords>; the account needs 2-Step\nVerification). No extra keys needed:\n\n```bash\nTOIL_EMAIL_ENABLED=true\nTOIL_EMAIL_FROM=you@gmail.com\nTOIL_EMAIL_PROVIDER=gmail\nTOIL_EMAIL_API_KEY=abcd efgh ijkl mnop\n```\n\n**Generic SMTP** (`TOIL_EMAIL_PROVIDER=smtp`), any submission server (Outlook,\nSendGrid/Mailgun SMTP, your own). Requires `TOIL_EMAIL_SMTP_HOST`; port defaults\nto `587` (STARTTLS), or set `465` for implicit TLS. `TOIL_EMAIL_SMTP_USER`\ndefaults to `from`.\n\n```bash\nTOIL_EMAIL_ENABLED=true\nTOIL_EMAIL_FROM=noreply@example.com\nTOIL_EMAIL_PROVIDER=smtp\nTOIL_EMAIL_API_KEY=the-smtp-password\nTOIL_EMAIL_SMTP_HOST=smtp.example.com\nTOIL_EMAIL_SMTP_PORT=587\nTOIL_EMAIL_SMTP_USER=noreply@example.com\n```\n\n### In dev\n\n`toiljs dev` runs the **full email pipeline** in Node, recipient validation,\ndedup, and the per-minute / per-day / per-recipient caps all behave exactly like\nthe edge, and **really sends** once you configure a provider. Configure it in\n`toil.config.ts` (non-secret) with the API key in `.env.secrets`:\n\n```ts\n// toil.config.ts\nimport { defineConfig } from 'toiljs/compiler';\nexport default defineConfig({\n server: {\n email: { provider: 'resend', from: 'you@example.com', maxPerMin: 60 },\n },\n});\n```\n\n```bash\n# .env.secrets (gitignored)\nTOIL_EMAIL_API_KEY=re_xxxxxxxxxxxx\n```\n\n`TOIL_EMAIL_*` env vars override the config file (so the same `.env.secrets` the\nedge uses works in dev too). Supports `resend` and `gmail`/`smtp` (SMTP via\nnodemailer). **Not configured?** `EmailService.send` stays a log-only mock and\nreturns `Sent`, so a flow that sends email still works without setup.\n\n> Because the dev server runs the guest **synchronously**, the actual network\n> send is fire-and-forget: validation + caps return their exact status\n> immediately, but a `Sent` is optimistic and the real delivery outcome (or\n> `ProviderError`) is logged, not returned. The ABI is identical to the edge, so\n> code that runs in dev runs on the edge.\n\n## Sending email\n\n```ts\nimport { Response, RouteContext } from 'toiljs/server/runtime';\n\n@rest('notify')\nclass Notify {\n @post('/welcome')\n welcome(ctx: RouteContext): Response {\n const status = EmailService.send(\n 'alice@example.com',\n 'Welcome!',\n 'Thanks for signing up.', // plain-text body\n 'welcome', // purpose tag (dedup / abuse keying)\n '<h1>Thanks for signing up.</h1>', // optional HTML body\n );\n return status == EmailStatus.Sent\n ? Response.text('sent\\n')\n : Response.text('could not send\\n', 503);\n }\n}\n```\n\n`send(to, subject, body, purpose = 'tx', html = '')` returns an **`EmailStatus`**:\n\n| Status | Meaning | Retry? |\n| ----------------- | ----------------------------------------------------------- | ---------------- |\n| `Sent` | Accepted by the provider |, |\n| `Deduped` | An identical recent `(recipient, purpose)` was collapsed | treat as sent |\n| `Budget` | The host's per-minute budget is exhausted | yes, later |\n| `TryLater` | The mailer was saturated / a queue was full | yes, back off |\n| `RecipientCapped` | The per-recipient hourly cap was hit | no (this window) |\n| `BadRecipient` | The address failed validation (CRLF, multiple addresses) | no |\n| `Disabled` | This host has no `[email]` capability | no |\n| `ProviderError` | The provider rejected it, or transport failed after retries | no |\n\n`purpose` is a short, non-PII tag (`\"welcome\"`, `\"reset\"`, …). The mailer folds\nit into the **dedup** key (identical `(host, recipient, purpose)` within ~30s is\ncollapsed to one send) and the abuse counters. It is never logged in the clear.\n\nThe recipient is validated host-side (exactly one address, no CR/LF/`<>`), so a\nguest can never smuggle a second envelope recipient or a header into the send.\n\n## Templates\n\n`EmailTemplate` is a reusable message with `{{placeholder}}` substitution, for\nwhen the same email is sent with different values:\n\n```ts\nconst welcome = new EmailTemplate(\n 'Welcome, {{name}}!', // subject\n 'Hi {{name}}, your code is {{code}}.', // plain-text body\n '<h1>Welcome, {{name}}</h1><p>Code: <b>{{code}}</b></p>', // html (optional)\n);\n\nconst vars = new Map<string, string>();\nvars.set('name', 'Alice');\nvars.set('code', '123456');\nconst status = welcome.send('alice@example.com', vars, 'welcome');\n```\n\n- `{{ name }}` (with surrounding spaces) is accepted; an unknown placeholder\n renders to the empty string.\n- Omit the `html` argument for a plain-text template.\n- `template.render(vars)` returns the rendered `{ subject, body, html }` without\n sending (useful for preview/testing).\n\nFor anything richer than `{{token}}` substitution, real layout, CSS, brand,\nauthor the email as a React component instead.\n\n## React email templates\n\nWrite emails as React components in an **`emails/`** folder. At `toiljs build`\neach one is rendered **once, at build time**, to static inline-CSS HTML (because\nthe inbox runs no JS), with the component's props turned into `{{token}}` holes;\nthe build then generates a typed `Emails.<Name>.send(...)` your server calls.\n\n```tsx\n// emails/Welcome.tsx\nexport const subject = 'Welcome, {{name}}!';\n\nexport default function Welcome({ name, code }: { name: string; code: string }) {\n return (\n <table\n width=\"100%\"\n style={{ fontFamily: 'Arial, sans-serif' }}>\n <tbody>\n <tr>\n <td style={{ padding: '24px' }}>\n <h1 style={{ color: '#111' }}>Welcome, {name}!</h1>\n <p>\n Your code is <b>{code}</b>.\n </p>\n </td>\n </tr>\n </tbody>\n </table>\n );\n}\n```\n\nThe generated `Emails.Welcome.send(...)` takes the recipient, then one argument\nper `{{token}}` **in alphabetical order**, then an optional `purpose`:\n\n```ts\n// emails/Welcome.tsx uses {{code}} and {{name}} -> params are (code, name)\nconst status = Emails.Welcome.send('alice@example.com', '123456', 'Alice');\n```\n\nAuthoring notes:\n\n- **Styles must end up inline.** Email clients strip `<style>`/external CSS, so\n write inline `style={{ ... }}`, or import a stylesheet and its rules are\n inlined into element `style=\"…\"` for you at build (a bare CSS import has no\n effect on its own under SSR). Keep email-only styles next to the email, e.g.\n `import './styles/email.css'`, or **reuse existing project CSS** with `import\n'client/styles/…'` (the `client/*` alias points at your client source).\n- **Optional exports:** `export const subject` (a token template; defaults to the\n email name), `export const text` (a plain-text alternative; otherwise derived\n from the HTML), `export const purpose`.\n- **Build-time, field substitution only.** Because the component renders once at\n build, per-send data is `{{token}}` substitution, a runtime `{items.map(...)}`\n or conditional bakes in at build, it does not re-run per recipient. That covers\n transactional / 2FA / confirmation email; dynamic lists need a different\n approach.\n- The generated `server/_emails.ts` is regenerated on `build`/`dev` and should be\n gitignored.\n\n### Preview while you author\n\nWhile `toiljs dev` runs, open **`/__toil/emails`** (the dev banner prints the\nlink). It lists every `emails/*.tsx`, renders the selected one exactly as the\nbuild does (imported `client/*` CSS inlined), lets you fill each `{{token}}` to\nsee the result, toggle the HTML and plain-text parts, and open the file in your\neditor. It refreshes live as you edit the template or its CSS.\n\n## Email verification codes (`TwoFactor`)\n\n`TwoFactor` is a **stateless** email-code primitive (2FA, email confirmation,\nmagic codes), no database. It emails a random code and returns a signed\n**token** that commits to the code via HMAC, without putting the code in the\ntoken (the code is only in the email). Verification recomputes the HMAC from the\ntoken plus the user-entered code, so a valid `(token, code)` pair can only come\nfrom someone who both received the email and holds the token.\n\n```ts\n// 1. Issue + email a code; hand `token` to the client (a cookie or hidden field).\nconst challenge = TwoFactor.send('alice@example.com', 'login'); // emails the code\n// challenge.token -> give to the client\n// challenge.status -> the EmailStatus of the send\n\n// 2. Later, verify what the user typed.\nconst ok: bool = TwoFactor.verify(challenge.token, 'alice@example.com', userEntered);\n```\n\n- **`send(recipient, purpose, ttlSecs = 600, digits = 6)`**, issues a code,\n emails it with a built-in template, returns `{ token, status }`.\n- **`issue(recipient, purpose, ttlSecs, digits)`**, returns `{ code, token }`\n **without** sending, so you can email `code` with your own `EmailTemplate` /\n `Emails.*` for a branded message.\n- **`verify(token, recipient, code)`**, `true` only for a code issued for that\n recipient that hasn't expired. Constant-time compare.\n- **`TwoFactor.setSecret(secret)`**, the HMAC secret for the tokens. Call once\n at startup in `main.ts`; it must be identical on every edge instance and out of\n any client bundle. (This is separate from the provider `api_key`.)\n\n**Limitation:** this gives integrity + expiry but **not single-use**, a valid\ncode verifies repeatedly within its TTL, because there is no server state to burn\nit. Keep the TTL short; for true single-use, store a per-recipient\nlast-verified-at and reject at or before it.\n\n## Limits and abuse controls\n\nAll enforced authoritatively in the single mailer (so the counts are exact across\nall workers):\n\n- **Per-host budget**, two rolling windows, both enforced: a 1-minute cap\n (`max_per_min`) and a 24-hour cap (`max_per_day`, `0` = unlimited). Over either\n one → `Budget`. Each host's caps, in-window sends, and reject counts are visible\n per host at `GET /_admin/email`.\n- **Per-recipient cap**, `max_per_recipient_per_hour`. Over it →\n `RecipientCapped`.\n- **Dedup**, identical `(host, recipient, purpose)` within ~30s → `Deduped`.\n\nEditing these in the host config takes effect on the next send (no restart).\n\n## Observability\n\n`GET /_admin/email` returns process-wide counters by reason (JSON), e.g.\n`submitted`, `sent`, `deduped`, `budget`, `recipient_capped`, `try_later`,\n`bad_recipient`, `provider_error`. It exposes **counts only**, never a\nrecipient, code, subject, body, or secret.\n\n## See also\n\n- [Rate limiting](./ratelimit.md), protect your routes (including any email\n trigger) with `@ratelimit`.\n- [Web Crypto](./crypto.md), the `crypto` global `TwoFactor` builds on.\n",
25
- "cookies.md": "# Cookies\n\nA complete HTTP cookie layer for the toiljs server runtime, covering the full\nRFC 6265bis surface (including `SameSite`, the `Partitioned`/CHIPS attribute, and\nthe `__Host-` / `__Secure-` prefixes) plus cryptographic signing and encryption.\n\n`Cookie`, `Cookies`, `CookieMap`, `SecureCookies`, and the `SameSite` /\n`CookieEncoding` / `CookiePrefix` enums are **ambient globals**: a handler uses\nthem with **no import**, exactly like `crypto`. They are also exported from\n`toiljs/server/runtime` for anyone who prefers an explicit import.\n\n- [How \"global, no import\" works](#how-global-no-import-works)\n- [Quick start](#quick-start)\n- [The `Cookie` builder](#the-cookie-builder)\n- [The `Cookies` parser and codec](#the-cookies-parser-and-codec)\n- [`CookieMap`](#cookiemap)\n- [`SecureCookies` signing and encryption](#securecookies-signing-and-encryption)\n- [`Request` and `Response` integration](#request-and-response-integration)\n- [`base64url` helpers](#base64url-helpers)\n- [Encoding vs encryption](#encoding-vs-encryption)\n- [Security notes](#security-notes)\n- [Spec compliance](#spec-compliance)\n- [Testing](#testing)\n- [API reference](#api-reference)\n\n---\n\n## How \"global, no import\" works\n\nThe cookie types are declared with ToilScript's `@global` decorator and pulled\ninto every server build (re-exported from `toiljs/server/runtime` and\nside-effect-imported by `toiljs/server/runtime/exports`, which every `main.ts`\nre-exports). At compile time the symbols register in the global scope, so a\nhandler can write `Cookie.create(...)` or `req.cookie(...)` without importing\nanything.\n\nFor the editor, `toiljs create` scaffolds `server/toil-server-env.d.ts` with\nambient `declare`s for these globals (the toilscript compiler ignores `.d.ts`;\nit only feeds the language service). If you would rather import them:\n\n```ts\nimport { Cookie, Cookies, SecureCookies, SameSite } from 'toiljs/server/runtime';\n```\n\n---\n\n## Quick start\n\n```ts\nimport { ToilHandler, Request, Response } from 'toiljs/server/runtime';\n\nexport class AppHandler extends ToilHandler {\n public handle(req: Request): Response {\n // Read (no import needed for Cookie / Cookies / SameSite, they are global).\n const sid = req.cookie('sid'); // string | null\n\n // Write a hardened session cookie.\n return Response.json('{\"ok\":true}').setCookie(\n Cookie.create('sid', 'abc123')\n .httpOnly()\n .secure()\n .sameSite(SameSite.Lax)\n .maxAge(3600)\n .asHostPrefixed(), // forces Secure + Path=/ + no Domain\n );\n }\n}\n```\n\n---\n\n## The `Cookie` builder\n\nA fluent builder that serializes to one `Set-Cookie` field value. Every setter\nreturns the cookie, so calls chain.\n\n```ts\nconst c = Cookie.create('id', 'abc123')\n .domain('example.com')\n .path('/app')\n .maxAge(3600)\n .secure()\n .httpOnly()\n .sameSite(SameSite.Lax);\n\nc.serialize();\n// \"id=abc123; Domain=example.com; Path=/app; Max-Age=3600; SameSite=Lax; Secure; HttpOnly\"\n```\n\n### Fields\n\n| Field | Type | Notes |\n| --- | --- | --- |\n| `name` | `string` | The cookie name (a token; never encoded). |\n| `value` | `string` | The logical value (encoded per `encoding` on serialize). |\n| `encoding` | `CookieEncoding` | Wire encoding for the value. Default `Percent`. |\n\n### Construction\n\n- `new Cookie(name, value)`\n- `Cookie.create(name, value): Cookie`, a builder-style alias.\n\n### Attribute setters\n\n| Method | Attribute |\n| --- | --- |\n| `domain(v: string)` | `Domain` |\n| `path(v: string)` | `Path` (must begin with `/`) |\n| `maxAge(seconds: i64)` | `Max-Age` (`0` / negative expire immediately) |\n| `expires(epochSeconds: i64)` | `Expires`, formatted as an IMF-fixdate (`Sun, 06 Nov 1994 08:49:37 GMT`) |\n| `expiresRaw(date: string)` | `Expires` verbatim (escape hatch) |\n| `secure(on: bool = true)` | `Secure` |\n| `httpOnly(on: bool = true)` | `HttpOnly` |\n| `sameSite(s: SameSite)` | `SameSite` |\n| `partitioned(on: bool = true)` | `Partitioned` (CHIPS) |\n| `priority(p: string)` | `Priority` (`Low` / `Medium` / `High`) |\n| `extension(av: string)` | An arbitrary extension attribute, appended verbatim |\n| `withEncoding(e: CookieEncoding)` | Choose the value wire encoding |\n\n### Prefixes\n\n- `asSecurePrefixed(): Cookie`, prepends `__Secure-` and forces `Secure`.\n- `asHostPrefixed(): Cookie`, prepends `__Host-` and forces `Secure`, `Path=/`, and no `Domain`.\n- `detectedPrefix(): CookiePrefix`, the prefix detected on the current name (case-insensitive).\n\n### Output\n\n- `serialize(strict: bool = false): string`, returns the `Set-Cookie` value. Lenient by\n default (always returns a best-effort cookie); pass `strict = true` to throw on\n a hard validation failure. `Secure` is added automatically when `SameSite=None`\n or `Partitioned` is set; `Max-Age` is clamped to the 400-day cap; control\n characters are stripped from the name, value, and attributes.\n- `toString(): string`, alias for `serialize()`.\n- `encodedValue(): string`, the value transformed per `encoding`.\n\n### Validation\n\n`validate(): CookieValidation` checks the cookie against RFC 6265bis and returns\na structured result:\n\n```ts\nclass CookieValidation {\n valid: bool;\n errors: Array<string>;\n}\n```\n\nIt flags: a non-token name, name+value over 4096 bytes, a `Domain`/`Path` over\n1024 bytes, a `Path` not starting with `/`, a `Raw` value outside `cookie-octet`,\nthe `__Host-` / `__Secure-` prefix requirements, `SameSite=None` or `Partitioned`\nwithout `Secure`, and a `Max-Age` beyond the 400-day cap.\n\n### Attribute serialization order\n\n`name=value` then, when set: `Domain`, `Path`, `Expires`, `Max-Age`, `SameSite`,\n`Secure`, `HttpOnly`, `Partitioned`, `Priority`, then any `extension(...)` values.\n(Attribute order is not significant to user agents; the order is stable so output\nis predictable.)\n\n### Enums\n\n```ts\nenum SameSite { Default, None, Lax, Strict } // Default omits the attribute\nenum CookieEncoding { Percent, Raw, Base64Url } // value wire encoding\nenum CookiePrefix { None, Secure, Host }\n```\n\n---\n\n## The `Cookies` parser and codec\n\nStatic helpers for the read side and a one-shot serializer.\n\n| Method | Description |\n| --- | --- |\n| `Cookies.parse(cookieHeader: string): CookieMap` | Parse a request `Cookie` header (`a=1; b=2`). Values are percent-decoded; one layer of surrounding quotes is stripped; malformed pairs and empty names are skipped. On a duplicate name the first wins. |\n| `Cookies.get(cookieHeader: string, name: string): string \\| null` | Parse and return one value. |\n| `Cookies.serialize(name: string, value: string): string` | One-shot `name=value` with no attributes (percent-encoded). For attributes, build a `Cookie`. |\n| `Cookies.parseSetCookie(setCookie: string): Cookie` | Parse a `Set-Cookie` line back into a `Cookie` (for clients, tests, proxies). Kept verbatim (`CookieEncoding.Raw`) so re-serializing reproduces the wire form. |\n| `Cookies.encodeValue(raw: string): string` | Percent-encode a value (the default `Cookie` encoding). |\n| `Cookies.decodeValue(enc: string): string` | Percent-decode a value (the inverse). |\n\n```ts\nconst jar = Cookies.parse('sid=abc123; theme=dark');\njar.get('sid'); // \"abc123\"\n\nCookies.serialize('sid', 'a b'); // \"sid=a%20b\"\n```\n\n---\n\n## `CookieMap`\n\nThe ordered name to value view returned by `Cookies.parse` and `Request.cookies()`.\nBacked by parallel arrays (a request carries a handful of cookies, so a linear\nscan beats hashing and keeps the runtime small).\n\n| Member | Description |\n| --- | --- |\n| `get(name: string): string \\| null` | The value, or `null`. |\n| `has(name: string): bool` | Whether the cookie is present. |\n| `names(): Array<string>` | A copy of the names, in encounter order. |\n| `size: i32` | The number of cookies. |\n| `set(name: string, value: string): void` | Insert unless present (keep-first). Used by `parse`; rarely called directly. |\n\n---\n\n## `SecureCookies` signing and encryption\n\nTamper-proof and confidential cookie values, built on the `crypto` global (no new\nhost functions).\n\n- **`SecureCookies.signed(key)`**: HMAC-SHA256. The value stays readable but is\n bound to the cookie name, so it cannot be tampered with or moved to another\n cookie. Sealed form: `base64url(value) \".\" base64url(mac)`.\n- **`SecureCookies.encrypted(key)`**: AES-256-GCM (or AES-128-GCM) with a random\n 96-bit IV and the cookie name as additional authenticated data. The value is\n confidential and authenticated. Sealed form: `base64url(iv ‖ ciphertext ‖ tag)`.\n\nKeys are caller-supplied raw bytes:\n\n- HMAC: any length (32+ bytes recommended).\n- AES: exactly 16 or 32 bytes (enforced up front; a wrong length is rejected by\n the factory).\n\n```ts\n// A real app loads a long random secret from config; never hard-code one.\nconst key = Uint8Array.wrap(String.UTF8.encode('0123456789abcdef0123456789abcdef'));\n\n// Signed (readable, tamper-proof)\nconst signer = SecureCookies.signed(key);\nconst sealed = signer.sign('session', 'user-42');\nconst user = signer.unsign('session', sealed); // \"user-42\", or null if tampered\n\n// Encrypted (confidential + authenticated)\nconst box = SecureCookies.encrypted(key);\nresp.setCookie(box.seal(Cookie.create('secret', 'top-secret').httpOnly()));\nconst secret = box.open(req.cookies(), 'secret'); // \"top-secret\", or null\n```\n\n| Method | Description |\n| --- | --- |\n| `SecureCookies.signed(key: Uint8Array)` | HMAC-SHA256 signer/verifier. |\n| `SecureCookies.encrypted(key: Uint8Array)` | AES-GCM (16- or 32-byte key). |\n| `addKey(key: Uint8Array): SecureCookies` | Add a fallback key for rotation: seal with the first, open with any. |\n| `sign(name, value): string` | Sealed signed value. |\n| `unsign(name, sealed): string \\| null` | Verify and recover, or `null`. |\n| `encrypt(name, value): string` | Sealed encrypted value. |\n| `decrypt(name, sealed): string \\| null` | Decrypt, or `null`. |\n| `seal(cookie: Cookie): Cookie` | Seal a cookie's value in place (sign or encrypt per the instance mode) and mark it `Raw`. Returns the same cookie. |\n| `open(jar: CookieMap, name): string \\| null` | Read and open cookie `name` from a parsed jar. |\n\n**Key rotation:** seal with `keys[0]`; `unsign` / `decrypt` try every key in turn,\nso you can add a new key as the first and keep an old one as a fallback while\nexisting cookies age out.\n\n```ts\nconst signer = SecureCookies.signed(newKey).addKey(oldKey);\n```\n\n---\n\n## `Request` and `Response` integration\n\nBecause every handler already has a `Request` and returns a `Response`, the most\ncommon operations live there directly.\n\n**Read (`Request`):**\n\n| Method | Description |\n| --- | --- |\n| `req.cookies(): CookieMap` | All cookies, parsed from the `Cookie` header (cached for the request). |\n| `req.cookie(name: string): string \\| null` | One cookie value. |\n\n**Write (`Response`, builder-style):**\n\n| Method | Description |\n| --- | --- |\n| `resp.setCookie(cookie: Cookie): Response` | Append a `Set-Cookie`. Each call adds its own header (cookies are never folded). |\n| `resp.setCookieKV(name, value): Response` | Shorthand for `setCookie(new Cookie(name, value))`. |\n| `resp.clearCookie(name, path = '/', domain = ''): Response` | Append a deletion cookie (empty value, `Max-Age=0`, epoch `Expires`). `path` / `domain` must match the original. |\n\n---\n\n## `base64url` helpers\n\nUnpadded base64url (RFC 4648 §5), used internally by `SecureCookies` and exported\nfor convenience. Its alphabet (`A-Z a-z 0-9 - _`) is within the `cookie-octet`\ngrammar and invariant under percent-encoding, so encoded values round-trip\ncleanly through the default cookie codec.\n\n| Function | Description |\n| --- | --- |\n| `base64UrlEncode(data: Uint8Array): string` | Encode bytes as unpadded base64url. |\n| `base64UrlDecode(s: string): Uint8Array \\| null` | Decode base64url/base64 (padding and whitespace tolerated); `null` on an invalid character or length. |\n\n---\n\n## Encoding vs encryption\n\nTwo independent layers, easy to mix up:\n\n- **Encoding** (`CookieEncoding`) is transport-only and reversible by anyone. It\n keeps an arbitrary value inside the `cookie-octet` grammar.\n - `Percent` (default): `encodeURIComponent`-style; arbitrary UTF-8 is safe.\n - `Base64Url`: UTF-8 then base64url.\n - `Raw`: no transformation (the value must already be valid `cookie-octet`).\n- **Signing / encryption** (`SecureCookies`) is cryptographic. Signing keeps the\n value readable but tamper-proof; encryption makes it unreadable and\n authenticated. Both require a secret key.\n\n`SecureCookies.seal` sets the value to its sealed (base64url) form and marks the\ncookie `Raw`, so it passes through the default parse path untouched.\n\n---\n\n## Security notes\n\n- **Panic-free verification.** `unsign` and `decrypt` return `null` on a tampered,\n truncated, or wrong-key value, never a trap. (`decrypt` reads the host return\n code directly instead of letting the underlying crypto throw, because the\n server runs with exceptions disabled.) This makes them safe to call on\n attacker-controlled input.\n- **Name-binding.** Signing MACs `name + \"=\" + value`; encryption uses the name as\n AAD. A sealed value made for one cookie name will not verify or decrypt under\n another.\n- **Control characters are stripped** from the name, value, and attribute values\n on serialize, as a defense-in-depth guard against header injection (CR/LF).\n Control characters are invalid in all of these per the grammar, so nothing\n legitimate is lost. The default value encoding already neutralizes CR/LF.\n- **Prefixes.** `asHostPrefixed()` / `asSecurePrefixed()` apply and enforce the\n browser-recognized guarantees; `validate()` reports a name that carries a prefix\n without satisfying its requirements.\n- **`SameSite=None` and `Partitioned` imply `Secure`** and are emitted with it\n automatically.\n- **Lifetime is clamped** to the RFC 400-day cap on serialize; sizes are checked by\n `validate()`.\n- **Local development.** Browsers treat `http://localhost` as a secure context, so\n `Secure` and `__Host-` cookies work under `toiljs dev` over plain HTTP.\n\nWhen putting untrusted input into a cookie **name** or **attribute** (rather than\nthe value, which is encoded by default), check `validate()` or use\n`serialize(true)`.\n\n---\n\n## Spec compliance\n\nImplements RFC 6265bis (HTTP State Management) and the `Partitioned` (CHIPS)\ncompanion: the `cookie-name` token and `cookie-value` `cookie-octet` grammars,\nthe `Expires` / `Max-Age` / `Domain` / `Path` / `Secure` / `HttpOnly` /\n`SameSite` / `Partitioned` attributes plus `Priority` and arbitrary extensions,\nthe `__Host-` / `__Secure-` prefixes (matched case-insensitively), the 4096-byte\nname+value and 1024-byte attribute limits, the 400-day lifetime cap, the\n`SameSite=None` ⇒ `Secure` rule, and the requirement that each cookie occupy its\nown `Set-Cookie` header (never folded).\n\n---\n\n## Testing\n\n- Pure cookie logic (builder, parser, codec, validation, `Request` / `Response`\n integration) is unit-tested with as-pect in `test/assembly/cookie.spec.ts`\n (`npm run test:server`).\n- `SecureCookies` is exercised end-to-end against the real toilscript-compiled\n wasm with the Node-backed crypto host in `test/devserver.test.ts`\n (`npm test`). It is tested there rather than under as-pect because the as-pect\n compiler does not ship the toilscript crypto standard library.\n\nA live demo (every attribute's serialized output, set/inspect/clear, and an\ninteractive sign/encrypt) is in the example app: run `toiljs dev` in\n`examples/basic` and open `/cookies`. The backend lives in\n`examples/basic/server/core/AppHandler.ts`.\n\n---\n\n## API reference\n\n```ts\n// Globals (also exported from 'toiljs/server/runtime')\n\nenum SameSite { Default, None, Lax, Strict }\nenum CookieEncoding { Percent, Raw, Base64Url }\nenum CookiePrefix { None, Secure, Host }\n\nclass CookieValidation {\n valid: bool;\n errors: Array<string>;\n}\n\nclass Cookie {\n name: string;\n value: string;\n encoding: CookieEncoding;\n static create(name: string, value: string): Cookie;\n domain(v: string): Cookie;\n path(v: string): Cookie;\n maxAge(seconds: i64): Cookie;\n expires(epochSeconds: i64): Cookie;\n expiresRaw(date: string): Cookie;\n secure(on?: bool): Cookie;\n httpOnly(on?: bool): Cookie;\n sameSite(s: SameSite): Cookie;\n partitioned(on?: bool): Cookie;\n priority(p: string): Cookie;\n extension(av: string): Cookie;\n withEncoding(e: CookieEncoding): Cookie;\n asSecurePrefixed(): Cookie;\n asHostPrefixed(): Cookie;\n detectedPrefix(): CookiePrefix;\n encodedValue(): string;\n validate(): CookieValidation;\n serialize(strict?: bool): string;\n toString(): string;\n}\n\nclass CookieMap {\n get(name: string): string | null;\n has(name: string): bool;\n names(): Array<string>;\n size: i32;\n set(name: string, value: string): void;\n}\n\nclass Cookies {\n static parse(cookieHeader: string): CookieMap;\n static get(cookieHeader: string, name: string): string | null;\n static serialize(name: string, value: string): string;\n static parseSetCookie(setCookie: string): Cookie;\n static encodeValue(raw: string): string;\n static decodeValue(enc: string): string;\n}\n\nclass SecureCookies {\n static signed(key: Uint8Array): SecureCookies;\n static encrypted(key: Uint8Array): SecureCookies;\n addKey(key: Uint8Array): SecureCookies;\n sign(name: string, value: string): string;\n unsign(name: string, sealed: string): string | null;\n encrypt(name: string, value: string): string;\n decrypt(name: string, sealed: string): string | null;\n seal(cookie: Cookie): Cookie;\n open(jar: CookieMap, name: string): string | null;\n}\n\nfunction base64UrlEncode(data: Uint8Array): string;\nfunction base64UrlDecode(s: string): Uint8Array | null;\n\n// On Request\nreq.cookies(): CookieMap;\nreq.cookie(name: string): string | null;\n\n// On Response (builder-style)\nresp.setCookie(cookie: Cookie): Response;\nresp.setCookieKV(name: string, value: string): Response;\nresp.clearCookie(name: string, path?: string, domain?: string): Response;\n```\n",
26
- "crypto.md": "# Web Crypto\n\nThe guest gets a synchronous Web Crypto surface through the ambient `crypto`\nglobal, backed by host functions. It mirrors the browser `crypto` /\n`crypto.subtle` API but **without Promises**, ToilScript has no `async`, so\nevery call returns its result directly. Keys are opaque per-request handles in a\nhost keystore; a `CryptoKey` is valid only for the request that created it.\n\n```ts\nconst mac = crypto.hmacSha256(key, message); // Uint8Array\nconst id = crypto.randomUUID(); // string\n```\n\nThis is also what [`SecureCookies`](./cookies.md) and\n[`AuthService`](./auth.md) are built on, so most apps use crypto indirectly.\n\n## `crypto` namespace\n\nConvenience helpers (all synchronous):\n\n| Function | Signature | Notes |\n| --- | --- | --- |\n| `getRandomValues` | `(array: Uint8Array): void` | Fill with CSPRNG bytes. |\n| `randomUUID` | `(): string` | RFC 4122 v4 UUID. |\n| `sha1` / `sha256` / `sha384` / `sha512` | `(data: Uint8Array): Uint8Array` | One-shot digests. |\n| `sha1Text` … `sha512Text` | `(s: string): Uint8Array` | UTF-8 encode then digest. |\n| `hmacSha256` | `(key: Uint8Array, msg: Uint8Array): Uint8Array` | One-shot HMAC-SHA256. |\n| `hmacSha256Text` | `(key: Uint8Array, msg: string): Uint8Array` | HMAC-SHA256 over a UTF-8 string. |\n| `toHex` | `(bytes: Uint8Array): string` | Lowercase hex. |\n| `subtle` | `SubtleCrypto` | The full primitive surface (below). |\n\n## `crypto.subtle`\n\n| Method | Signature |\n| --- | --- |\n| `digest` | `digest(algorithm: string, data: Uint8Array): Uint8Array` |\n| `importKey` | `importKey(format: string, keyData: Uint8Array, algorithm: AlgorithmParams, extractable: bool, usages: i32): CryptoKey` |\n| `exportKey` | `exportKey(format: string, key: CryptoKey): Uint8Array` |\n| `encrypt` | `encrypt(algorithm: AlgorithmParams, key: CryptoKey, data: Uint8Array): Uint8Array` |\n| `decrypt` | `decrypt(algorithm: AlgorithmParams, key: CryptoKey, data: Uint8Array): Uint8Array` |\n| `sign` | `sign(algorithm: AlgorithmParams, key: CryptoKey, data: Uint8Array): Uint8Array` |\n| `verify` | `verify(algorithm: AlgorithmParams, key: CryptoKey, signature: Uint8Array, data: Uint8Array): bool` |\n| `deriveBits` | `deriveBits(algorithm: AlgorithmParams, baseKey: CryptoKey, length: i32): Uint8Array` |\n| `deriveKey` | `deriveKey(algorithm, baseKey, lengthBits, derivedKeyAlgorithm, extractable, usages): CryptoKey` |\n\n`digest` takes a named algorithm string (`\"SHA-1\"`, `\"SHA-256\"`, `\"SHA-384\"`,\n`\"SHA-512\"`, `\"SHA3-256\"`, `\"SHA3-384\"`, `\"SHA3-512\"`). `verify` returns a bool\n(it does not throw on a mismatch). Formats are `raw`, `pkcs8`, `spki`; **`jwk`\nis not supported**.\n\n### Algorithm parameter classes\n\n`crypto` and `crypto.subtle` are ambient globals (no import). The params classes\nand the `ALG_*` / `USAGE_*` / `FMT_*` / `CURVE_*` constants and the `CryptoKey`\ntype are imported from the `'crypto'` module:\n\n```ts\nimport { AesGcmParams, HmacImportParams, ALG_SHA_256, USAGE_SIGN } from 'crypto';\n```\n\nEach algorithm has a small params class you pass to `importKey`/`sign`/etc.:\n\n| Class | Constructor |\n| --- | --- |\n| `AesGcmParams` | `(iv, additionalData?, tagLength = 128)` |\n| `AesCbcParams` | `(iv)` |\n| `AesCtrParams` | `(counter, length = 128)` |\n| `HmacImportParams` | `(hash)` |\n| `HmacParams` | `()` |\n| `Pbkdf2Params` | `(hash, salt, iterations)` |\n| `HkdfParams` | `(hash, salt, info?)` |\n| `EcdsaParams` | `(hash)` |\n| `EcKeyImportParams` | `(alg, namedCurve)` |\n| `Ed25519Params` | `()` |\n| `X25519ImportParams` | `()` |\n| `EcdhParams` | `(alg, publicKeyHandle)` |\n\n### Constants\n\n- **Hashes / algorithms:** `ALG_SHA_1`, `ALG_SHA_256`, `ALG_SHA_384`,\n `ALG_SHA_512`, `ALG_SHA3_256/384/512`, `ALG_AES_GCM`, `ALG_AES_CBC`,\n `ALG_AES_CTR`, `ALG_HMAC`, `ALG_ECDSA`, `ALG_ED25519`, `ALG_ECDH`, `ALG_HKDF`,\n `ALG_PBKDF2`.\n- **Key formats:** `FMT_RAW`, `FMT_PKCS8`, `FMT_SPKI` (`FMT_JWK` is rejected).\n- **Usages (bitmask):** `USAGE_ENCRYPT`, `USAGE_DECRYPT`, `USAGE_SIGN`,\n `USAGE_VERIFY`, `USAGE_DERIVE_KEY`, `USAGE_DERIVE_BITS`, `USAGE_WRAP_KEY`,\n `USAGE_UNWRAP_KEY`, OR them together.\n- **Named curves:** `CURVE_P256`, `CURVE_P384` (`CURVE_P521` is not supported).\n\n### `CryptoKey`\n\nAn opaque handle plus metadata: `handle: i32`, `type: string`\n(`secret`/`public`/`private`), `extractable: bool`, `algorithm: i32`,\n`usages: i32`, with `algorithmName()` and `hasUsage(u)`. A key is valid only for\nthe request that imported it.\n\n## Examples\n\nHMAC-SHA256 (one-shot):\n\n```ts\nconst mac = crypto.hmacSha256(key, message);\nconst hex = crypto.toHex(mac);\n```\n\nAES-256-GCM via `subtle`:\n\n```ts\nconst key = new Uint8Array(32); crypto.getRandomValues(key);\nconst iv = new Uint8Array(12); crypto.getRandomValues(iv);\n\nconst k = crypto.subtle.importKey('raw', key, new AesGcmParams(iv, aad, 128), false, USAGE_ENCRYPT);\nconst ct = crypto.subtle.encrypt(new AesGcmParams(iv, aad, 128), k, plaintext);\n```\n\n## Post-quantum verify\n\nThe host also exposes ML-DSA-44 (FIPS 204) signature verification as\n`crypto.mldsa_verify`. It is verify-only, the host never holds a secret key, and\nunderpins the [auth primitive](./auth.md). Most code reaches it through\n`AuthService.verifyLogin(publicKey, message, signature)` rather than calling the\nimport directly. Public key is 1312 bytes, signature 2420 bytes, with a FIPS 204\ndomain-separation context.\n\n## Limitations\n\n- **No Promises**, every call is synchronous.\n- **No RSA** and **no JWK** key format.\n- **P-521** is not supported (P-256 and P-384 are).\n- Signature *generation* for ML-DSA is client-side only; the server verifies.\n",
27
- "time.md": "# Time\n\n`Time` is the guest's wall-clock. It is the toiljs-blessed way to read the\ncurrent time, backed by the host's `Date.now()` binding (`env.Date.now`). Both\nthe edge and the dev server provide that binding, so time behaves identically in\n`toiljs dev` and in production.\n\nIt is available as an ambient global (`@global`, no import) and is also exported\nfrom `toiljs/server/runtime`.\n\n```ts\nimport { Time } from 'toiljs/server/runtime'; // optional; Time is also a global\n\nconst ms = Time.nowMillis(); // u64 milliseconds since the Unix epoch\nconst s = Time.nowSeconds(); // u64 whole seconds since the Unix epoch\n```\n\n## API\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `Time.nowMillis()` | `static nowMillis(): u64` | Milliseconds since the Unix epoch (the raw host `Date.now()` value). |\n| `Time.nowSeconds()` | `static nowSeconds(): u64` | Whole seconds since the epoch (`nowMillis() / 1000`). The unit used by sessions and login challenges. |\n\n## Semantics\n\n`Time` is **wall-clock, not monotonic**, exactly like browser `Date.now()`. It\ntracks the system clock and can step backward across an NTP correction.\n\n- Use it to stamp and compare absolute instants: session `iat`/`exp`, login\n challenge expiry, cache ages.\n- Do **not** use it to measure elapsed time or as a high-resolution timer; a\n backward step would produce a negative or zero interval.\n\n## Relationship to `Date.now()`\n\nToilScript's `Date.now()` lowers to the same `env.Date.now` host import, so you\n*can* call it directly. Prefer `Time`: it makes the host boundary (and the\nsingle millisecond unit) explicit and easy to find, and it gives you\n`nowSeconds()` without an open-coded `/ 1000` cast at every call site.\n\n`AuthService` uses `Time.nowSeconds()` internally for session `iat`/`exp`, so\nsession timing and any timing you do in a handler share one clock.\n",
28
- "cli.md": "# CLI\n\n- `toiljs create [name]`, scaffold a project. Flags: `--template app|minimal`,\n `--style css|sass|less|stylus`, `--tailwind`, `--no-ai`, `-y`/`--yes`.\n- `toiljs dev`, dev server with HMR (`--port`, `--root`). With a `toilconfig.json` it builds\n the server first, then rebuilds it whenever a `server/` file changes (regenerating\n `shared/server.ts`, which Vite HMRs into the client); client-only edits just HMR the client.\n- `toiljs build`, production build. With a `toilconfig.json` it builds the server (toilscript,\n regenerating `shared/server.ts`) first, then the client `build/client`. `--server` builds\n only the server. Every `server/` file declaring a surface (`@data`/`@rest`/...) is compiled.\n- `toiljs start`, self-host the built app with production hyper-express/uWS static workers,\n SSR/wasm dispatch, daemon support, and a `/_toil` WebSocket channel. Use `--threads <n>`\n (or `server.threads`) to set the worker count; `1` disables the pool.\n- `toiljs configure`, toggle styling features on an existing project (see [styling.md](./styling.md)).\n- `toiljs doctor`, diagnose project setup (`--json` for CI). `--fix` auto-wires the typed-RPC\n setup (build scripts, tsconfig `shared` + alias, `.gitignore`, toilscript version, and the\n toilscript prettier plugin) so an existing project upgrades in one command.\n",
29
- "derive.md": "# Derive (materialized views)\n\n`@derive` precomputes a read-optimized **view** from your data so reads stay\nfast and never scan. A request handler (`@get` runs as a *query*, `@post`/`@put`/\n`@delete` as an *action*) is not allowed to scan, reading \"the latest N events\"\nor \"every member of a set\" could fan out across unbounded rows, so those scans\nare barred on the request path. A `@derive` does the scan **off** the request\npath: it folds your event log / counters into a `View`, and your route serves\nthat view with a single keyed read.\n\n```ts\n@database\nclass GuestbookDb {\n @collection static entries: Events<GuestKey, GuestEntry>;\n @collection static totals: Counter<GuestKey>;\n @collection static book: View<GuestKey, GuestbookView>;\n\n // Recompute the view from the sources. Runs after a signature is written\n // (and when a box first loads). A derive MAY scan + 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); // counter read\n view.entries = GuestbookDb.entries.latest(key, 10); // scan, allowed here\n GuestbookDb.book.publish(key, view); // publish the materialized view\n }\n}\n```\n\n## Why a derive\n\nToilDB gates every data op by the *function kind* it runs under:\n\n- **query** (`@get`/`@head`) and **action** (`@post`/`@put`/`@patch`/`@delete`)\n may do keyed reads and (actions only) writes, but **not scans**\n (`events.latest`, `membership.list`).\n- **derive** may do everything a read can, **plus** scans, plus\n `view.publish`/`append`/`counter.add`.\n\nSo if a page needs \"the 10 newest entries\" or \"the leaderboard\", you cannot read\nthat directly in the `@get`. Instead a `@derive` builds it once into a `View`,\nand the `@get` reads the view by key, which is not a scan.\n\n## Declaring a derive\n\nA derive is a method on your `@database` class, alongside the collections it\nreads 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 is run\n independently.\n- The view value (`HomePage` above) and the key are ordinary `@data` types, so\n they round-trip through the codec like any other stored value.\n\n## `View<K, V>`\n\nA `View` is a published, read-optimized projection. Its API:\n\n```ts\nview.get(key) // V | null - the published view, or null if none yet\nview.require(key) // V - like get, but traps if nothing is published\nview.publish(key, value) // void - overwrite the view (derive/job only)\n```\n\n`publish` is only allowed from a `@derive` (or a `@job`); the host assigns the\nversion so a later publish always supersedes an earlier one. `get`/`require` are\nplain keyed reads, allowed from any handler, including a `@get` route.\n\n## When derives run\n\nYou never call a derive yourself. The runtime runs it for you:\n\n- **After a write to a source.** When a request writes one of a database's\n source collections (an `events.append`/`append_once`, a `counter.add`, or a\n record `create`/`patch`), that database's derives run right after the response\n is produced, so the view reflects the new data on the next read. Many writes to\n one database in a single request coalesce into one recompute.\n- **On box load.** When a server box starts or hot-reloads (or the underlying\n source data changed out of band), the views are rebuilt from their sources\n before the first read is served. This is also where a value type's `@migrate`\n runs against old stored events, as the derive re-reads and republishes them.\n\nA derive's own writes (its `view.publish`) never re-trigger it.\n\nThe same code runs under `toiljs dev` (the in-process emulator) and on the\nproduction edge, no flags or wiring to change.\n\n## Reading a view from a route\n\nThe route just reads the view by key, which is a non-scan read and so is legal in\na `@get`:\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);\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 the\n // entries list is served by GET. The action just acks with the new total\n // (a counter read is allowed here; a scan is not).\n const view = new GuestbookView();\n view.total = GuestbookDb.totals.get(key);\n return view;\n }\n}\n```\n\n## How it fits together (the guestbook)\n\nThe `examples/basic` guestbook is the end-to-end demo:\n\n1. `POST /guestbook` (an action) appends the signature to an `Events` stream and\n bumps a `Counter`. It returns the running total, but it does **not** read the\n entry list (that would be a scan).\n2. The runtime then runs `@derive recompute()` under the derive kind: it scans\n `entries.latest(...)`, reads the `totals` counter, and `publish`es a fresh\n `GuestbookView`.\n3. `GET /guestbook` (a query) reads `book.get(...)`, a single keyed read, and\n returns the precomputed total + newest entries.\n\nSign twice and the total climbs across requests, because the data lives in\nToilDB (and its view), not in module memory.\n\n## Notes\n\n- A derive **recomputes** the view from whatever its method reads (here, the\n latest 10 events). It is a fresh recompute on each trigger, so it suits views\n built from a bounded read (latest N, a counter total, a small set). Folding an\n unbounded full event log incrementally is a separate, more advanced pattern.\n- Because publishes are last-writer-wins and a derive recomputes from the source\n of truth, a view always converges to a correct snapshot of its sources.\n- See also: [`data.md`](data.md) for `@data` value types, and the ToilDB host\n ABI for the exact `derive_run` / `toildb.derives` contract.\n",
8
+ "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 [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).\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\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- [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",
9
+ "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## 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",
10
+ "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",
11
+ "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",
12
+ "auth/README.md": "# Authentication\n\nToil ships a complete, **post-quantum password login** you turn on with one line. No passwords on your\nserver, no third-party identity provider, no hand-written crypto: enable it and you get a `/auth/*` API,\nsigned sessions, `@auth`-guarded routes, and a stable per-user identity.\n\n```ts\n// toil.config.ts\nimport { defineConfig } from 'toiljs/compiler';\n\nexport default defineConfig({\n server: { auth: true }, // ← mounts the full /auth/* API + sessions\n});\n```\n\nThat is the whole setup. The build appends the framework's auth controller to your server, so `/auth/*`\nis live and `@auth` works everywhere.\n\n## What you get\n\n| Endpoint | Purpose |\n| --- | --- |\n| `POST /auth/register/start`, `/register/finish` | Create an account (password never leaves the browser) |\n| `POST /auth/login/start`, `/login/finish` | Log in; sets a signed session cookie |\n| `GET /auth/me` *(`@auth`)* | The current user (`toilUserId` + `username`) |\n| `POST /auth/logout` *(`@auth`)* | Clear the session |\n\nPlus, in your own code:\n\n- **`@auth`** on any route (or a whole `@rest` class) → 401 unless there's a valid session.\n- **`AuthService.getUser()`** → the typed logged-in user.\n- **`AuthService.userId()`** → the stable [`ToilUserId`](./extending.md#toiluserid) (a 256-bit id you can\n key your data on).\n- **The client**: `import { Auth } from 'toiljs/client'`, then `Auth.register(username, password)` /\n `Auth.login(username, password)`. It does all the browser-side crypto and talks to `/auth/*` for you.\n\n## A login page in full\n\n```tsx\n// client/routes/login.tsx\nimport { useState } from 'react';\nimport { Auth } from 'toiljs/client';\n\nexport default function Login() {\n const [u, setU] = useState('');\n const [p, setP] = useState('');\n const [msg, setMsg] = useState('');\n\n const register = async () => {\n try { await Auth.register(u, p); setMsg('registered, now log in'); }\n catch (e) { setMsg(parseError(e)); }\n };\n const login = async () => {\n try { await Auth.login(u, p); setMsg('logged in'); } // sets the session cookie\n catch (e) { setMsg(parseError(e)); }\n };\n\n return (\n <main>\n <input value={u} onChange={(e) => setU(e.currentTarget.value)} placeholder=\"username\" />\n <input value={p} type=\"password\" onChange={(e) => setP(e.currentTarget.value)} placeholder=\"password\" />\n <button onClick={register}>Register</button>\n <button onClick={login}>Log in</button>\n <p>{msg}</p>\n </main>\n );\n}\n```\n\n```ts\n// server/routes/Secret.ts, a route only a logged-in user can reach\nimport { Response } from 'toiljs/server/runtime';\n\n@rest('secret')\nclass Secret {\n @auth // 401 without a valid session\n @get('/')\n public secret(): Response {\n const user = AuthService.getUser()!; // typed: { toilUserId, username }\n return Response.text('hello ' + user.username + '\\n');\n }\n}\n```\n\nThat's a real, production-grade auth system, the password is stretched with Argon2id in the browser into\nan ML-DSA-44 key pair, your server only ever stores a public key, and login is a mutually-authenticated\nML-KEM-768 challenge. You didn't write any of it.\n\n## Where to go next\n\n- **[How it works](./how-it-works.md)**: the protocol (OPRF + Argon2id + ML-DSA + ML-KEM), sessions,\n cookies, and the `ToilUserId`, with sequence diagrams.\n- **[Usage](./usage.md)**: enabling it, the client API, guarding routes, reading the user, the full wire\n contract of each endpoint.\n- **[Configuration](./configuration.md)**: the secrets a deployment MUST set, the audience/domain, tuning\n Argon2id, and the deploy checklist.\n- **[Extending & integrating](./extending.md)**: `ToilUserId`, keying your own data on a user, a custom\n user shape / opting out, and the `AuthService` primitive reference.\n\n> **One rule before you ship:** built-in auth runs with **insecure DEV fallback secrets** so it Just Works\n> locally. A deployment MUST set `AUTH_SESSION_SECRET`, `AUTH_OPRF_SEED`, and `AUTH_KEM_SK`. See\n> [Configuration](./configuration.md).\n",
13
+ "auth/usage.md": "# Using auth\n\n## 1. Enable it\n\nEither the config flag (canonical) or a one-line import, both do the same thing (the build appends the\nframework's auth controller + user shape to your server as compiled entries, so `/auth/*` self-mounts):\n\n```ts\n// toil.config.ts, canonical\nexport default defineConfig({ server: { auth: true } });\n```\n\n```ts\n// server/main.ts, escape hatch (equivalent)\nimport 'toiljs/server/auth';\n```\n\nThere is also an `AuthService.enable()` no-op you can call for discoverability, but enabling is done by the\nflag/import above (it happens at build time). Turn it off by removing the flag/import, with neither, no\n`/auth` routes and no accounts collection are generated. It is strictly opt-in.\n\n## 2. The client\n\n`toiljs/client` ships the browser half, it runs the OPRF blinding, Argon2id, ML-DSA keygen, and ML-KEM\nencapsulation, and talks to `/auth/*`. You never touch the crypto.\n\n```ts\nimport { Auth } from 'toiljs/client';\n\n// Create an account. Throws on a taken username or a transport error.\nawait Auth.register(username, password);\n\n// Log in. On success the browser holds the signed session cookie; subsequent\n// requests to @auth routes are authorized automatically.\nawait Auth.login(username, password);\n```\n\nOptions (second argument, `AuthOptions`):\n\n```ts\nawait Auth.login(username, password, {\n baseUrl: '/auth', // default; change if you mount elsewhere\n serverKemPublicKey: MY_KEM_PUB, // REQUIRED in production, pin your deployment's KEM key\n});\n```\n\n> **Production:** the client ships with the DEV KEM public key pinned. A real deployment MUST pass its own\n> `serverKemPublicKey` (derived from `AUTH_KEM_SK`). See [Configuration](./configuration.md).\n\n## 2b. Client-side: who's logged in, and protecting pages\n\n`register`/`login` set the session cookies; to *render* login state on the client, use the generated\n`getUser()` (emitted from the built-in `@user`). It reads the **readable** `__Secure-toil_user` companion\ncookie and returns the typed user or `null`, instant, no network call. It is **display-only**; the server\nstill enforces access via `@auth`, so never trust it for authorization.\n\n```tsx\nimport { getUser } from 'shared/server'; // generated; typed to the built-in @user\n\nfunction Nav() {\n const user = getUser(); // { toilUserId, username } | null\n return user\n ? <span>Hi {user.username} <button onClick={logout}>Log out</button></span>\n : <a href=\"/login\">Log in</a>;\n}\n\nasync function logout() {\n await fetch('/auth/logout', { method: 'POST' });\n location.href = '/login'; // cookies are cleared; bounce to login\n}\n```\n\nGate a whole client route by redirecting when there's no session:\n\n```tsx\nexport default function Dashboard() {\n const user = getUser();\n if (user == null) { location.href = '/login'; return null; } // not logged in -> to /login\n return <main>Welcome, {user.username}</main>;\n}\n```\n\nThe authoritative check is still the server: any data this page fetches should come from an `@auth` route,\nso a user who forged/deleted the readable cookie sees the redirect OR a `401`, never real data.\n\n## 2c. Handling errors\n\n`Auth.register` / `Auth.login` reject with a message you can show. The important distinguishable cases:\n\n```ts\ntry {\n await Auth.register(username, password);\n} catch (e) {\n const m = String(e);\n if (m.includes('already registered')) setError('That username is taken, log in instead.');\n else setError('Could not register, try again.');\n}\n\ntry {\n await Auth.login(username, password);\n} catch {\n // Wrong password OR unknown user both fail generically (anti-enumeration), one message.\n setError('Incorrect username or password.');\n}\n```\n\n- **Username taken** `register` throws `auth: username already registered (log in instead)` (a\n distinguishable case so you can guide the user).\n- **Wrong password / unknown user** → `login` throws generically (`auth: request failed`): by design,\n the two are indistinguishable, so use ONE \"incorrect username or password\" message.\n- **Rate limited** → after 5 attempts / 60s a `429` surfaces as the same generic throw; back off and tell\n the user to wait.\n\n## 3. Guard your routes: `@auth`\n\nPut `@auth` on a route method or a whole `@rest` class. The generated dispatcher checks for a valid signed\nsession **before** your handler runs and returns `401` otherwise. `@auth` is unchanged by built-in auth,\nit's the same decorator you'd use with hand-written auth.\n\n```ts\n@rest('account')\nclass AccountApi {\n @auth // this route needs a session\n @get('/settings')\n public settings(): Response { /* … */ }\n\n @get('/public') // this one is open\n public open(): Response { /* … */ }\n}\n\n@auth // …or guard the ENTIRE class\n@rest('admin')\nclass AdminApi { /* every route requires a session */ }\n```\n\n## 4. Read the current user\n\nInside any handler:\n\n```ts\n// The typed logged-in user (the built-in `@user`: toilUserId + username), or null.\nconst user = AuthService.getUser();\nif (user != null) {\n user.username; // string\n user.toilUserId; // Uint8Array(32), the stable id bytes\n}\n\n// The stable identity as a ToilUserId (gate on hasSession() first, see note).\nif (AuthService.hasSession()) {\n const id = AuthService.userId()!; // ToilUserId\n // key your own data on id (see Extending)\n}\n```\n\n> `ToilUserId` overloads `==`, so `AuthService.userId() == null` does not type-check. Gate with\n> `AuthService.hasSession()` and then use `userId()!`, or use `getUser()` and null-check that.\n\n## 5. The endpoint wire contract\n\nYou normally use the `Auth` client, but the raw endpoints are binary (`DataWriter`/`DataReader`, never\nJSON):\n\n| Route | Request body | Response |\n| --- | --- | --- |\n| `POST /auth/register/start` | `str(username) bytes(blinded)` | `u8(0) u32(mem) u32(iters) u32(par) bytes(salt) bytes(evaluated)` |\n| `POST /auth/register/finish` | `str(username) bytes(pubkey) bytes(proof)` | `u8(status)`, `0` ok, `1` username taken |\n| `POST /auth/login/start` | `str(username) bytes(blinded)` | `bytes(cid) str(aud) u32(mem) u32(iters) u32(par) bytes(salt) bytes(nonce) u64(iat) u64(exp) bytes(evaluated)` |\n| `POST /auth/login/finish` | `bytes(cid) bytes(ct) bytes(sig)` | `u8(0) bytes(sessionToken) bytes(serverConfirm)` + `Set-Cookie`, or `u8(≠0)` on failure |\n| `GET /auth/me` *(`@auth`)* | (none) | `bytes(toilUserId) str(username)` |\n| `POST /auth/logout` *(`@auth`)* | (none) | `200` + cookie-clearing `Set-Cookie` |\n\nRate limiting: every register/login POST carries `@ratelimit(SlidingWindow, 5, 60)` (5 requests / 60s per\nclient) out of the box, so brute-force is throttled before it reaches the crypto.\n\n## 6. Under `toiljs dev`\n\nEverything runs locally with **zero setup**: the dev server emulates the ToilDB account/challenge storage\nand the ML-DSA/ML-KEM/OPRF host functions in process, and the auth secrets fall back to insecure DEV\nvalues. Register and login span requests (the accounts persist for the dev session). You'll see a warning\nthat `AUTH_SESSION_SECRET` is unset, that's expected in dev; set it before you deploy (see\n[Configuration](./configuration.md)).\n\n## 7. `Server.REST.auth.*`\n\nBecause the controller is a normal `@rest('auth')` class, a typed `Server.REST.auth.*` fetch client is\ngenerated for free (`me`, `logout`, and the register/login methods). Use it for `/me` and `/logout`; but\n**drive register/login through `toiljs/client` `Auth`**, not the generated client, only the `Auth` helper\nruns the required browser-side OPRF/Argon2id/ML-DSA/ML-KEM crypto.\n",
14
+ "backend/data.md": "# Data types (`@data`)\n\n`@data` turns a plain class into a typed value that can travel safely between your frontend and your WASM backend, and in and out of the database, with both a binary and a JSON codec generated for you.\n\n## What `@data` is\n\nA **codec** is a pair of functions: one that turns a value into bytes (encode), and one that turns those bytes back into a value (decode). Any time data crosses a boundary (browser to server, server to database, one WASM call to another) it has to become bytes and back. Writing that by hand is tedious and easy to get wrong.\n\n`@data` writes it for you. You declare a class with typed fields, tag it `@data`, and the compiler synthesizes a deterministic binary codec and a JSON codec on the class. The exact same class is also generated into your `shared/server.ts`, so the browser and the WASM backend agree on the format down to the byte.\n\n```ts\n@data\nclass Player {\n username: string = '';\n admin: bool = false;\n score: u64 = 0;\n}\n```\n\nThat is a complete, serializable type. Note every field has a **default value**; that is required (the generated decoder and the client constructor use it).\n\n## Why and when\n\n`@data` is the backbone of almost everything that moves in a toiljs app:\n\n- **Request and response bodies** for [`@rest`](./rest.md) routes.\n- **Arguments and return values** for [`@service` / `@remote`](./rpc.md) calls.\n- **Values stored in [ToilDB](../database/README.md)**.\n- **Sessions**, [stream](../realtime/README.md) messages, and any custom payload you design.\n\nWhenever a route, an RPC method, or the database needs a structured value, that value is a `@data` class. You will define these constantly.\n\n```mermaid\nflowchart LR\n D[\"@data class<br/>(one definition)\"] --> R[\"REST route body\"]\n D --> P[\"RPC argument / result\"]\n D --> B[(\"ToilDB value\")]\n D --> S[\"Stream message\"]\n```\n\n## What the compiler generates\n\nFrom your `@data` class, the compiler adds these members:\n\n| Member | What it does |\n| --- | --- |\n| `encode(): Uint8Array` | Serialize to bytes, with a 4-byte type-id prefix. |\n| `static decode(buf): T` | Rebuild a value from bytes produced by `encode()`. |\n| `encodeInto(w: DataWriter)` | Serialize without the type-id frame, for nesting inside another value. |\n| `static decodeFrom(r: DataReader): T` | The matching read, for nested values. |\n| `toJSON()` / `static fromJSON(v)` | The JSON codec (large integers become decimal strings, so `JSON.parse` keeps them exact). |\n| `static dataId(): u32` | A stable hash (FNV-1a) of the class name, written as the type-id prefix. |\n\nYou mostly do not call these directly; routes, RPC, and the database call them for you. But they are there when you need them (for example `value.toJSON().toString()` to build a `Response.json`).\n\n## Supported field types\n\nA `@data` field may be:\n\n- a scalar: `u8` through `u256`, `i8` through `i256`, `f32`, `f64`, `bool`;\n- a `string`;\n- a `Uint8Array` (a raw byte buffer);\n- a nested `@data` class;\n- an array `T[]` of any of the above.\n\nFor the number types (why `u64` is a `bigint` on the client, when to reach for `u256`, and so on), see [Types](../concepts/types.md).\n\nGive every field a default. The layout is exactly the field declaration order (this matters, see [gotchas](#gotchas)).\n\n## Nested `@data`, arrays, and bytes\n\n`@data` classes compose. A field can be another `@data` class, an array, or a byte buffer, and it all encodes and decodes as one value:\n\n```ts\n@data\nclass Tag {\n label: string = '';\n weight: f32 = 0;\n}\n\n@data\nclass Document {\n id: u64 = 0;\n title: string = '';\n tags: Tag[] = []; // array of nested @data\n authors: string[] = []; // array of strings\n thumbnail: Uint8Array = new Uint8Array(0); // raw bytes\n}\n```\n\n`Document.encode()` walks the whole tree: it writes `id`, `title`, each `Tag` (via `encodeInto`), each author string, and the raw `thumbnail` bytes, in field order. `Document.decode(bytes)` reads them back in the same order. On the JSON side, a `Uint8Array` field becomes a JSON array of byte numbers, and nested `@data` fields become nested JSON objects.\n\n## Using `@data` in a route\n\nA route's body parameter and its return value are `@data` values. Which codec runs depends on the route's stream mode (see [REST bodies](./rest.md#request-and-response-bodies)):\n\n```ts\n// JSON route (the default): body from JSON.parse, result via toJSON()\n@post('/')\npublic create(input: NewPlayer): Player { /* ... */ }\n\n// Binary route: body via decode(), result via encode()\n@route({ method: Methods.POST, path: '/blob', stream: DataStream.Binary })\npublic blob(input: FileData): FileResult { /* ... */ }\n```\n\n- In a **JSON** route, the incoming body is `JSON.parse`d and revived with the type's `fromJSON`, and the returned value is serialized with `toJSON()`.\n- In a **Binary** route, the incoming body is `decode`d and the returned value is `encode`d.\n\nYou do not call the codec yourself in either case; you just declare the types.\n\n## The raw codec: `DataWriter` and `DataReader`\n\nSometimes you want to lay out bytes by hand: a custom body, a session token, a challenge message, a wire format someone else defined. For that, use the codec directly. It lives in the `data` module:\n\n```ts\nimport { DataWriter, DataReader } from 'data';\n```\n\nThis is the same codec `@data` classes are built from, and it has a byte-for-byte identical TypeScript version in `toiljs/io` (`src/io/codec.ts`), so your browser code can read and write the exact same bytes the WASM backend does.\n\n### `DataWriter`\n\nEvery write method returns the writer, so calls chain.\n\n| Method | Signature | Wire format |\n| --- | --- | --- |\n| `writeU8` / `writeI8` | `(v): DataWriter` | 1 byte |\n| `writeU16` / `writeI16` | `(v): DataWriter` | 2 bytes, little-endian |\n| `writeU32` / `writeI32` | `(v): DataWriter` | 4 bytes, LE |\n| `writeU64` / `writeI64` | `(v): DataWriter` | 8 bytes, LE |\n| `writeF32` / `writeF64` | `(v): DataWriter` | 4 / 8 bytes, IEEE-754 LE |\n| `writeBool` | `(v): DataWriter` | 1 byte (`1` / `0`) |\n| `writeBytes` | `(b: Uint8Array): DataWriter` | `u32` length (LE) + the raw bytes |\n| `writeString` | `(s: string): DataWriter` | `u32` length (LE) + UTF-8 bytes |\n| `writeU128` / `writeI128` | `(v): DataWriter` | two `u64` limbs (lo, hi) |\n| `writeU256` / `writeI256` | `(v): DataWriter` | four `u64` limbs |\n| `length` | `(): i32` | bytes written so far |\n| `toBytes` | `(): Uint8Array` | an exact-length copy of the buffer |\n\n### `DataReader`\n\nReads are **bounds-safe**: reading past the end of the buffer never crashes. Instead the read returns a zero or empty default and flips the reader's public `ok` flag to `false`. Check `ok` after a sequence of reads to catch a truncated or malformed buffer.\n\n| Method | Signature | On over-read |\n| --- | --- | --- |\n| `readU8` / `readI8` | `(): integer` | `0` |\n| `readU16`..`readU64`, `readI16`..`readI64` | `(): integer` | `0` |\n| `readF32` / `readF64` | `(): float` | `0` |\n| `readBool` | `(): bool` | `false` |\n| `readBytes` | `(): Uint8Array` | empty array |\n| `readString` | `(): string` | `\"\"` |\n| `readU128` / `readI128` / `readU256` / `readI256` | `(): bignum` | `0` |\n| `remaining` | `(): i32` | bytes left unread |\n| `ok` | `bool` (field) | `false` once any read over-ran |\n\n### Encode and decode, both directions\n\n```ts\nimport { DataWriter, DataReader } from 'data';\n\n// Encode: a version byte, a name, a score, then a blob.\nconst out = new DataWriter()\n .writeU8(1)\n .writeString('alice')\n .writeU64(1234)\n .writeBytes(payload)\n .toBytes();\n\n// Decode: read the fields back in the exact same order.\nconst r = new DataReader(out);\nconst version = r.readU8();\nconst name = r.readString();\nconst score = r.readU64();\nconst blob = r.readBytes();\nif (!r.ok) return Response.badRequest('truncated');\n```\n\nThe order of reads must match the order of writes exactly. The layout is the format.\n\n## JSON vs binary: which to use\n\nYou usually pick this at the route level (see [REST bodies](./rest.md#request-and-response-bodies)), but the trade-off is the same everywhere:\n\n- **JSON** is human-readable and understood by every tool. Pick it for endpoints a browser or a third party calls directly. Large integers ride as decimal strings so they stay exact.\n- **Binary** is smaller, faster, and lossless for big numbers. Pick it for app-to-app traffic and anything performance sensitive. RPC always uses binary under the hood.\n\n## Evolving a `@data` format: `@migrate`\n\nOnce records are stored, changing a `@data` **value** type is a problem: as the [gotchas](#gotchas) explain, the binary layout *is* the field order, so adding or changing a field means the bytes already on disk no longer match the new shape and would fail to decode. `@migrate` is how you evolve a stored type without a backfill and without downtime.\n\n**What it is.** `@migrate` marks a plain (free) function that upgrades one record written under an **old** version of a value type into the **current** shape. The compiler weaves that function into the value type's decoder as a version dispatch: every stored record carries the schema version it was written under, so when a read hits an old record, the decoder decodes it as its old shape and runs your `@migrate` forward to today's shape. This happens **lazily**, per row, only when a record is actually read (nothing rewrites your whole collection up front).\n\n**When to use it.** Any time you change a `@data` type that is already stored in ToilDB: you added a field, renamed one, changed a type. New writes use the new layout; old rows keep working because the migration upgrades them on the way out.\n\nHere is the whole story. You shipped this value type:\n\n```ts\n// server/models/GuestEntry.ts (version 1: what you shipped first)\n@data\nexport class GuestEntry {\n author: string = '';\n message: string = '';\n}\n```\n\nLater you add an `at` timestamp:\n\n```ts\n// server/models/GuestEntry.ts (version 2: gained an `at` field)\n@data\nexport class GuestEntry {\n author: string = '';\n message: string = '';\n at: u64 = 0; // NEW field\n}\n```\n\nEvery entry already stored was written without `at`, so its bytes cannot decode as the new `GuestEntry`. You add a migration file to bridge them:\n\n```ts\n// server/migrations/GuestEntry.migration.ts\nimport { GuestEntry } from '../models/GuestEntry';\n\n// Keep the ORIGINAL layout as its own class so old rows still decode.\n// One kept class per past version.\n@data\nexport class GuestEntryV1 {\n author: string = '';\n message: string = '';\n}\n\n// Upgrade a v1 row to the current GuestEntry. This is the DELTA form:\n// (old, into). The compiler pre-copies the fields the two layouts share\n// (author, message), so your body fills only what is new.\n@migrate\nexport function up(old: GuestEntryV1, into: GuestEntry): void {\n into.at = 0; // unknown for entries written before the timestamp existed\n}\n```\n\nThat is it. Old entries now surface as fully-formed `GuestEntry` values with `at = 0`; new entries carry a real timestamp; the same read path serves both.\n\nA `@migrate` function comes in two shapes, and you pick whichever reads better:\n\n- **Delta form** `up(old: OldType, into: NewType): void` (used above). The compiler copies every field the two layouts share by name and type, and your body fills only the changed or new fields. Least to write when most fields carry over.\n- **Full form** `up(old: OldType): NewType`. Your body builds and returns the whole new value itself. Use it when the transform is not a simple field-for-field copy.\n\n```ts\n// The same migration written in the full form.\n@migrate\nexport function up(old: GuestEntryV1): GuestEntry {\n const e = new GuestEntry();\n e.author = old.author;\n e.message = old.message;\n e.at = 0;\n return e;\n}\n```\n\nGotchas specific to `@migrate`:\n\n- **Location is enforced.** Every `@migrate` must live in a `migrations/<Type>.migration.ts` file (a `*.migration.ts` file under a `migrations/` folder). The build auto-discovers it (nothing imports it), and a `@migrate` placed anywhere else is a hard compile error, because it would silently never run.\n- **A migration is a pure value transform.** It may not touch the database or any host service; it only turns old fields into new ones. Trying to read or write ToilDB from inside a `@migrate` is a compile error.\n- **Migrations chain.** If you evolve a type more than once, keep one old class and one `@migrate` per step (`V0 -> V1`, `V1 -> V2`). The compiler walks the chain, so a row written under the oldest layout is carried all the way forward to the current shape, shortest path first.\n- **When it actually runs.** On any read that decodes an old row (`get`, `getMany`, a view read, events `latest`). It also runs when a [`@derive`](../background/derive.md) rebuilds a view on box load and re-reads old stored events (see [Views](../database/views.md)).\n\n## Dynamic JSON on the server: the `JSON` value tree\n\n`@data` is for shapes you know ahead of time. Sometimes you do not: a webhook whose body varies, a third-party payload with optional fields, or a response you assemble on the fly. For those, toiljs gives you a `JSON` **value tree**: an in-memory value that can be a null, a bool, a number, a string, an array, or an object, which you read and build at runtime.\n\n**What it is.** `JSON` is an ambient global class (no import needed). It represents one dynamic JSON value. `JSON.parse(text)` turns JSON text into one of these trees, and a `@data` class's `toJSON()` actually returns one too. It is the **untyped** counterpart to `@data`'s typed, fixed-shape codec: reach for `@data` when the shape is known and you want type safety, and for `JSON` when the shape is dynamic.\n\nThe statics that make a value:\n\n| Static | What it does |\n| --- | --- |\n| `JSON.parse(text: string): JSON` | Parse JSON text into a value tree (returns an error value on malformed input). |\n| `JSON.obj(): JSON` | A new empty object; fill it with `.set(key, value)`. |\n| `JSON.arr(): JSON` | A new empty array; fill it with `.push(value)`. |\n| `JSON.of<T>(value: T): JSON` | Wrap a scalar, string, bool, or array as a JSON value. |\n| `JSON.nul(): JSON` | A JSON null. |\n| `JSON.stringify<T>(value: T): string` | Serialize a scalar / string / bool / array value straight to a JSON string. |\n\nThe instance methods that read and build a value:\n\n| Method | What it does |\n| --- | --- |\n| `.isObject()` / `.isArray()` / `.isString()` / `.isNumber()` / `.isBool()` / `.isNull(): bool` | Test the value's type before you read it. |\n| `.has(key: string): bool` | Whether an object has `key`. |\n| `.get(key: string): JSON` | The value for `key` on an object. |\n| `.objectKeys(): Array<string>` | The keys of an object. |\n| `.at(index: i32): JSON` | The element at `index` of an array. |\n| `.length(): i32` | The element count of an array (0 otherwise). |\n| `.asString(): string` | Read the value as a string. |\n| `.asF64(): f64` / `.asI64(): i64` / `.asU64(): u64` | Read the value as a number. |\n| `.asBool(): bool` | Read the value as a bool. |\n| `.set(key: string, value: JSON): JSON` | Set a key on an object; returns `this`, so calls chain. |\n| `.push(value: JSON): JSON` | Append to an array; returns `this`, so calls chain. |\n| `.toString(): string` | Serialize this tree back to a JSON string. |\n\nReading an untyped body and building a reply, together:\n\n```ts\nimport { Response, RouteContext } from 'toiljs/server/runtime';\n\n@rest('echo')\nclass Echo {\n // POST /echo with an arbitrary JSON body we do not model as a @data class.\n @post('/')\n public handle(ctx: RouteContext): Response {\n const body = JSON.parse(ctx.text()); // ctx.text() is the raw body as text\n if (!body.isObject() || !body.has('name')) {\n return Response.badRequest('expected an object with a \"name\" field');\n }\n\n // Read fields out of the tree, guarding types and optional keys.\n const name = body.get('name').asString();\n const age = body.has('age') ? body.get('age').asI64() : 0;\n\n // Build a fresh JSON object to send back (chaining .set).\n const out = JSON.obj()\n .set('greeting', JSON.of<string>('hello, ' + name))\n .set('age', JSON.of<i64>(age));\n return Response.json(out.toString());\n }\n}\n```\n\nGotchas specific to `JSON`:\n\n- **Check the type before you read.** `JSON.parse` never throws; a malformed input or a wrong-typed field yields an error or default value rather than a crash. Use `.isObject()` / `.has(...)` / `.isString()` and friends to validate untrusted input before you trust it.\n- **It is a value tree, not a typed struct.** You get no compile-time field checking. When a shape is stable, prefer a `@data` class so the compiler catches typos and the client gets a typed type for free.\n- **Big-integer care still applies.** As with any JSON, integers above 2^53 are best carried as strings; read them with `.asString()` when exactness matters.\n\n## Gotchas\n\n- **Field order is the format.** The binary layout is exactly your field declaration order. Reordering fields, or changing a field's type, is a breaking change: old bytes will decode wrong. To evolve a format safely, add new fields at the **end** (and, for hand-rolled payloads, bump a leading version byte).\n- **Every field needs a default.** The generated decoder and the client constructor rely on it. A field with no default will not compile as `@data`.\n- **`encode()` carries a type id; `encodeInto` does not.** The 4-byte `dataId()` prefix lets a decoder confirm it is reading the type it expected. When nesting one `@data` inside another, `encodeInto` / `decodeFrom` skip that frame (the outer type already identifies the whole value).\n- **Endianness.** The WASM codec is little-endian. The `toiljs/io` codec defaults to little-endian too, and also accepts a per-call big-endian flag for network formats. Keep both ends on the same setting.\n- **Plain JSON numbers lose precision above 2^53.** That is why `@data` sends 64-bit-and-larger integers as decimal strings over JSON. If you hand-build JSON, do the same, or use the binary codec.\n- **`DataReader` never throws; check `ok`.** An over-read returns a default and sets `ok = false`. Always check `ok` after decoding untrusted bytes.\n\n## Related\n\n- [Types](../concepts/types.md): `u64`, `u256`, `f64`, and how each maps to `number` or `bigint`.\n- [HTTP routes (`@rest`)](./rest.md): where `@data` bodies and return values are used, and the JSON vs binary route modes.\n- [Typed RPC](./rpc.md): `@data` as RPC arguments and results, and the generated client classes.\n- [The database](../database/README.md): storing `@data` values in ToilDB.\n- [Backend overview](./README.md): where `@data` fits in the request lifecycle.\n",
15
+ "backend/README.md": "# Backend overview\n\nYour backend is TypeScript that toilscript compiles into a small, sandboxed WebAssembly program, which runs on the Dacely edge and answers every request.\n\n## What the backend is\n\nIn a toiljs project, everything under `server/` is your backend. You write it in TypeScript, the same language as your frontend. But it does not run in a browser and it does not run in Node. Instead, a compiler called **toilscript** turns it into **WebAssembly** (often shortened to **WASM**): a compact, fast, portable binary format that many kinds of servers can run safely.\n\nThat compiled program is then deployed to the **Dacely edge**. Two pieces of jargon to unpack there:\n\n- **The edge** means a fleet of servers spread across many cities around the world. When a user makes a request, it is served by the edge node physically closest to them. Close means fast: less distance for the data to travel, so lower latency. You do not pick a region or manage servers; your one compiled backend runs everywhere at once.\n- **Sandboxed** means your WASM program runs inside a locked box. It cannot open files, reach the operating system, or make raw network connections on its own. The only way it can touch the outside world is through a small, fixed set of **host functions** that toiljs provides (read the request, build a response, query the database, send an email, and so on). Every one of those calls is metered and bounded, so a buggy or hostile backend cannot crash the node or read another app's data. This is what makes it safe to run thousands of different apps on the same shared edge.\n\nYou never call the WASM boundary by hand. You write normal TypeScript classes and functions, tag them with decorators (like `@rest` or `@service`), and the compiler wires everything up.\n\n> New to decorators? A **decorator** is the `@name` you write just above a class or method. It attaches meaning to that code without changing what the code does line by line. toiljs uses decorators to say \"this class is an HTTP controller\" or \"this method is callable from the browser.\" See [Decorators](../concepts/decorators.md).\n\n## The request lifecycle\n\nHere is what happens, end to end, when a browser talks to your backend.\n\n```mermaid\nflowchart TD\n U[\"User's browser\"] -->|\"HTTP request\"| E[\"Nearest Dacely edge node\"]\n E --> S{\"Static file<br/>for this path?\"}\n S -->|\"Yes (GET/HEAD)\"| F[\"Serve the file<br/>(HTML, JS, images)\"]\n S -->|\"No\"| W[\"Your compiled backend<br/>(server.wasm handle)\"]\n W --> H[\"Your handler runs<br/>and returns a Response\"]\n H --> E\n F --> E\n E -->|\"HTTP response\"| U\n```\n\nStep by step:\n\n1. The request lands on the closest edge node.\n2. The edge first checks whether the path is a **static file** it can serve directly (your built frontend: HTML pages, JavaScript bundles, images). If so, it serves the file and never wakes your code. This is fast and free.\n3. Otherwise the edge hands the request to your compiled backend by calling its single WASM export, `handle`.\n4. Inside, toiljs decodes the raw bytes into a friendly [`Request`](./rest.md#the-request-object) object and calls your handler's `handle(req)` method.\n5. Your handler returns a [`Response`](./rest.md#building-a-response). toiljs encodes it back into bytes.\n6. The edge sends that response to the browser.\n\nThe key mental model: your backend is a pure function of the request. Bytes in, bytes out, one request at a time.\n\n## Stateless by default\n\nA **fresh copy** of your handler serves each request. Any fields you set on a controller do not survive to the next request, and the request might even be served by a different edge node on the other side of the world. This is called being **stateless**.\n\nThat is a feature, not a limitation: it is what lets your backend scale to the whole planet with no coordination. When you need data to persist between requests (a user account, a counter, a list of posts), you store it in the built-in global database, **ToilDB**. See [the database section](../database/README.md).\n\n```mermaid\nflowchart LR\n R1[\"Request 1\"] --> B1[\"Handler copy A<br/>(fields reset)\"]\n R2[\"Request 2\"] --> B2[\"Handler copy B<br/>(fields reset)\"]\n B1 --> DB[(\"ToilDB<br/>(shared, persistent)\")]\n B2 --> DB\n```\n\n## The three surfaces\n\nYour backend can expose three different kinds of endpoint. Each is opted into with a decorator, and each has its own page:\n\n| Surface | Decorator | What it is | When to use it |\n| --- | --- | --- | --- |\n| **HTTP REST** | `@rest` + `@get`/`@post`/... | Plain HTTP routes with paths, methods, and status codes. | A public API, webhooks, anything a browser, `curl`, or a third party calls directly. See [REST](./rest.md). |\n| **Typed RPC** | `@service` / `@remote` | Server functions your own frontend calls like local async functions, fully type-checked end to end. | Talking from your own React app to your own backend. See [RPC](./rpc.md). |\n| **Realtime** | `@stream` | A long-lived connection where the server keeps state per connected client. | Chat, live cursors, notifications, anything push-based. See [Realtime](../realtime/README.md). |\n\nREST and RPC are the everyday tools. Most apps use both: REST for anything the outside world calls, RPC for your own frontend. They are not exclusive; you can use all three in one project.\n\nAll of these share the same building block for their data: **`@data` classes**, which are typed structs that travel safely between the browser and your WASM backend. See [Data types](./data.md).\n\n## Where your handler lives\n\nREST, RPC, and streams self-register: you tag a class and the compiler adds it to the right dispatcher. A tiny amount of glue lives in `server/main.ts`, which imports your route files and names your top-level handler class. In a typical project you rarely touch `main.ts`; you add route and service files and they are discovered automatically.\n\nYour handler class extends `ToilHandler` and overrides `handle`. The common pattern is to try the REST dispatcher first, then fall back to your own logic:\n\n```ts\nimport { Method, Request, Response, Rest, ToilHandler } from 'toiljs/server/runtime';\n\nexport class AppHandler extends ToilHandler {\n public handle(req: Request): Response {\n // Try every @rest controller. Returns the first match, or null.\n const hit = Rest.dispatch(req);\n if (hit != null) return hit;\n\n // Your own hand-written endpoints can go here.\n if (req.path == '/health') return Response.text('ok\\n');\n\n // \"I have no answer for this path\": let the edge serve it (a static\n // file, the client app) instead of returning a hard 404.\n return Response.unhandled();\n }\n}\n```\n\nRPC calls (to the reserved path `/__toil_rpc`) are handled by the framework before your `handle` runs, so you do not dispatch them yourself.\n\n### Per-request hooks: `onRequestStarted` and `onRequestCompleted`\n\n`ToilHandler` gives you two optional hooks that run around every request, so you can add cross-cutting logic (logging, timing, metrics) once instead of repeating it in every route, and without re-implementing `handle`:\n\n- **`onRequestStarted(req: Request): void`** runs just **before** `handle(req)`. Override it for per-request setup: start a timer, log the incoming method and path, read a header you need everywhere.\n- **`onRequestCompleted(req: Request, resp: Response): void`** runs just **after** `handle` returns, and it is handed the `Response` that is about to go out. It **also runs when your handler throws**, after the runtime has turned that throw into a `500`, so it is the right place for teardown you always want to happen (record the outcome, stop the timer, emit a metric).\n\nBoth are empty by default, so override only the one you need. The framework calls them around every request; you never call them yourself.\n\n```ts\nimport { Request, Response, Rest, ToilHandler } from 'toiljs/server/runtime';\n\nexport class AppHandler extends ToilHandler {\n public handle(req: Request): Response {\n const hit = Rest.dispatch(req);\n return hit != null ? hit : Response.unhandled();\n }\n\n // Runs before every handle().\n public onRequestStarted(req: Request): void {\n // per-request setup, e.g. note the path you are about to serve\n }\n\n // Runs after every handle(), including after a throw became a 500.\n public onRequestCompleted(req: Request, resp: Response): void {\n // always-run teardown: record the outcome, stop a timer, emit a metric\n }\n}\n```\n\nIf your project is REST-only, you do not even need a custom handler; toiljs ships a ready-made one. See [REST](./rest.md#dispatch-and-the-404-fallback).\n\n## Compute tiers\n\nThe request/response backend described here is the default and most common tier, called **L1**. toiljs also has higher tiers for long-lived connections (streams) and scheduled background work (daemons), each compiled into its own WASM artifact from the same project. You opt into a tier just by adding its entry file and surface decorator. For the full picture, see [Compute tiers](../concepts/tiers.md).\n\n## Related\n\n- [HTTP routes (`@rest`)](./rest.md): paths, methods, params, and responses.\n- [Typed RPC (`@service`/`@remote`)](./rpc.md): calling the server from your frontend with end-to-end types.\n- [Data types (`@data`)](./data.md): the serializable structs everything uses.\n- [The database (ToilDB)](../database/README.md): where persistent state lives.\n- [Compute tiers](../concepts/tiers.md): L1 request, L2/L3 stream, L4 daemon.\n- [Realtime streams](../realtime/README.md): the `@stream` surface.\n",
16
+ "backend/rest.md": "# HTTP routes (`@rest`)\n\nExpose a real HTTP API by tagging a class `@rest` and its methods with an HTTP verb; toiljs generates the router for you.\n\n## Why and when\n\nUse `@rest` when something outside your own frontend needs to call your backend over plain HTTP: a webhook from another service, a public API, a mobile app, a `curl` script, or a browser hitting a URL directly. REST endpoints are ordinary URLs with ordinary HTTP methods and status codes, so any HTTP client understands them.\n\nIf instead you are calling your own backend from your own React frontend, [typed RPC](./rpc.md) is usually nicer (you get end-to-end types and skip URL wrangling). The two live happily side by side.\n\n## The shape of a route\n\nA REST route is a method on a class. The class decorator says where the class is mounted; the method decorator says which HTTP method and path it answers.\n\n```ts\nimport { Response, RouteContext } from 'toiljs/server/runtime';\n\n@rest('players') // this controller is mounted at /players\nclass Players {\n @get('/:id') // answers GET /players/:id\n public get(ctx: RouteContext): Response {\n const id = ctx.param('id');\n return Response.json(`{\"id\":\"${id}\"}`);\n }\n}\n```\n\nSave that file, import it once (see [How routes are discovered](#how-routes-are-discovered)), and `GET /players/42` returns `{\"id\":\"42\"}`. That is a working endpoint.\n\n## `@rest`: mounting a controller\n\n`@rest` marks a class as a route **controller** (a group of related routes) and mounts it at a URL prefix.\n\n```ts\n@rest('api') // mounted at /api\n@rest('/') // or @rest('') : mounted at the root\n@rest({ stream: DataStream.Binary }) // root mount, binary body codec by default\n```\n\n- The string is the mount prefix. `\"api\"`, `\"/api\"`, and `\"api/\"` all normalize to `/api`. `\"\"` and `\"/\"` mean the root.\n- The object form sets class-wide defaults. `stream: DataStream.Binary` makes every route in the class use the binary body codec instead of JSON (more on that in [Request and response bodies](#request-and-response-bodies)).\n\nThe full URL of a route is the controller prefix joined with the method path. With `@rest('api')` and `@get('/todos/:id')`, the route is `GET /api/todos/:id`.\n\n## Verb decorators\n\nEach HTTP method has a decorator that takes the route path:\n\n```ts\n@get('/path') @post('/path') @put('/path') @patch('/path')\n@del('/path') @head('/path') @options('/path')\n```\n\nNote the delete decorator is `@del`, not `@delete` (the word `delete` is reserved in TypeScript, so it cannot be a decorator name).\n\n### `@route`: the explicit form\n\n`@route` is the general form. Reach for it when you want to set the body codec per route, or you just prefer an object:\n\n```ts\n@route({ method: Methods.POST, path: '/upload', stream: DataStream.Binary })\npublic upload(body: FileData): FileResult { /* ... */ }\n```\n\n`method` (from the `Methods` enum) and `path` are required; `stream` is optional and overrides the controller default. `Methods` and `DataStream` are global enums (like the decorators themselves, they need no import).\n\n## Path parameters\n\nA path segment written as `:name` is a **path parameter**: it captures whatever the request has in that position. Read it with `ctx.param('name')`.\n\n```ts\n@get('/todos/:id/items/:itemId')\npublic getItem(ctx: RouteContext): Response {\n const id = ctx.param('id'); // \"42\" for /todos/42/items/9\n const itemId = ctx.param('itemId'); // \"9\"\n return Response.json(`{\"todo\":\"${id}\",\"item\":\"${itemId}\"}`);\n}\n```\n\nMatching is **segment-exact**: the request path must have the same number of `/`-separated segments, the static segments must match literally, and each `:param` captures one segment. The query string is removed before matching. A captured param is always a string; convert it yourself if you need a number (for example `u64.parse(ctx.param('id'))`).\n\n## Method parameters: reading the request\n\nA route method may declare zero, one, or two parameters. toiljs looks at their types to decide what to pass:\n\n- a parameter typed `RouteContext` receives the [request context](#the-routecontext-object) (path params, query, headers, raw body);\n- any other type is treated as the **request body**, decoded into that [`@data`](./data.md) type.\n\n```ts\n@get('/status')\npublic status(): StatusResponse { /* no body, no context */ }\n\n@get('/user/:id')\npublic getUser(ctx: RouteContext): User { /* context only */ }\n\n@post('/create')\npublic create(input: NewTodo): Todo { /* body only */ }\n\n@post('/user/:id/score')\npublic addScore(input: ScoreDelta, ctx: RouteContext): Player {\n const id = ctx.param('id'); // body AND context\n /* ... */\n}\n```\n\nThe order of the two parameters does not matter; toiljs classifies them by type.\n\n## Return types\n\nYou return one of two things from a route, and toiljs encodes it for you:\n\n| You return | toiljs sends |\n| --- | --- |\n| a [`@data`](./data.md) value | The value serialized (JSON by default, binary if the route is in binary mode). Status `200`. |\n| a `Response` | Exactly that response: your status, headers, cookies, and body, untouched. |\n| `void` (nothing) | `204 No Content`. |\n\nReturning a `@data` value is the short path when you just want to send the data. Returning a `Response` gives you full control: a custom status, extra headers, cookies, caching. Use whichever fits.\n\n```ts\n// Short path: return the data, let toiljs serialize it as JSON.\n@get('/me')\npublic me(): Player { return currentPlayer(); }\n\n// Full control: custom 404, a header, and the body serialized by hand.\n@get('/:id')\npublic get(ctx: RouteContext): Response {\n const id = u64.parse(ctx.param('id'));\n if (!store.has(id)) return Response.notFound();\n return Response.json(store.get(id).toJSON().toString())\n .setHeader('cache-control', 'no-store');\n}\n```\n\n## Request and response bodies\n\nEvery route is either a **JSON route** (the default) or a **Binary route**. This decides how request bodies are decoded and response values are encoded:\n\n- **JSON**: the request body is parsed as JSON and revived into your `@data` type; the response value is serialized to JSON. Best for endpoints a browser or a third party calls directly, because JSON is universally readable.\n- **Binary**: the request body and response use toiljs's compact binary codec (`DataWriter`/`DataReader`). Smaller and faster, and exact for very large integers. Best for app-to-app traffic and anything performance sensitive.\n\nSet the mode on the whole controller with `@rest({ stream: DataStream.Binary })`, or per route with `@route({ ..., stream: DataStream.Binary })`. Full detail on both codecs is in [Data types](./data.md).\n\n> Large integers and JSON: numbers of 64 bits or more (`u64`, `i64`, `u128`, `u256`, and friends) are sent over JSON as decimal strings, so they stay exact at any size (plain JSON numbers would lose precision). The generated client turns them back into `bigint`. See [Types](../concepts/types.md).\n\n## The `RouteContext` object\n\n`RouteContext` is your window into the incoming request. toiljs builds one and passes it to any route method that declares a `RouteContext` parameter.\n\n| Member | Signature | What it gives you |\n| --- | --- | --- |\n| `request` | `Request` | The raw request (method, path, headers, body). |\n| `param` | `param(name: string): string` | A captured path param, or `\"\"` if absent. |\n| `query` | `query(name: string): string` | A query-string value (`?q=hi`), or `\"\"` if absent. Not URL-decoded in v1. |\n| `header` | `header(name: string): string \\| null` | A request header, case-insensitive, or `null`. |\n| `text` | `text(): string` | The raw request body decoded as UTF-8 text. |\n| `clientIp` | `clientIp(): string` | The connecting client's IP, or `\"\"` if unavailable. |\n\n`clientIp()` is the real socket address the edge observed, not a forgeable header like `X-Forwarded-For`, so it is safe to key rate limits, geo lookups, or audit logs on it.\n\n## The `Request` object\n\nThe raw request. You get it as `ctx.request`, or directly as the argument to a hand-written `handle(req)`.\n\n**Fields:**\n\n| Field | Type | Notes |\n| --- | --- | --- |\n| `method` | `Method` | `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, or `UNKNOWN`. |\n| `path` | `string` | The path, including the query string. |\n| `headers` | `Array<Header>` | Ordered list; a `Header` is `{ name, value }`. |\n| `body` | `Uint8Array` | The raw request body bytes. |\n\n**Methods:**\n\n| Method | Signature | Notes |\n| --- | --- | --- |\n| `header` | `header(name: string): string \\| null` | Case-insensitive lookup, `null` if absent. |\n| `cookies` | `cookies(): CookieMap` | Parses the `Cookie` header (values percent-decoded); cached per request. |\n| `cookie` | `cookie(name: string): string \\| null` | A single cookie value, or `null`. |\n\nThe `Method` enum and the `Header` class are exported from `toiljs/server/runtime`.\n\n## Building a response\n\n`Response` is what you return. Create one with a static factory, then chain instance methods to add headers, cookies, and caching. Every instance method returns the same `Response`, so calls chain.\n\n### Static factories\n\n| Factory | Signature | Status | Content-Type |\n| --- | --- | --- | --- |\n| `Response.text` | `text(body: string, status = 200)` | 200 | `text/plain; charset=utf-8` |\n| `Response.html` | `html(body: string, status = 200)` | 200 | `text/html; charset=utf-8` |\n| `Response.json` | `json(body: string, status = 200)` | 200 | `application/json; charset=utf-8` |\n| `Response.bytes` | `bytes(body: Uint8Array, status = 200)` | 200 | `application/octet-stream` |\n| `Response.empty` | `empty(status)` | custom | (none) |\n| `Response.notFound` | `notFound()` | 404 | text |\n| `Response.badRequest` | `badRequest(msg = 'bad request')` | 400 | text |\n| `Response.internalError` | `internalError(msg = 'internal error')` | 500 | text |\n| `Response.unhandled` | `unhandled()` | 404 | text, plus a marker header (see below) |\n\n`Response.json` takes an **already-serialized** string, not an object. Build it from a `@data` value with `value.toJSON().toString()`, or return the `@data` value directly and let toiljs serialize it (usually simpler).\n\n### Instance methods\n\n| Method | Signature | What it does |\n| --- | --- | --- |\n| `setHeader` | `setHeader(name, value): Response` | Appends a header (call again to add more). |\n| `setCookie` | `setCookie(cookie: Cookie): Response` | Appends a `Set-Cookie`. |\n| `setCookieKV` | `setCookieKV(name, value): Response` | Shorthand for a cookie with no attributes. |\n| `clearCookie` | `clearCookie(name, path = '/', domain = ''): Response` | Emits a deletion cookie (empty value, `Max-Age=0`). |\n| `cache` | `cache(edgeTtlMinutes, browserTtlSeconds = 0, privateScope = false, allowAuth = false): Response` | Marks the response cacheable. See [Caching](../services/caching.md). |\n| `cacheFor` | `cacheFor(minutes): Response` | Shorthand for edge-caching for N minutes. |\n\n```ts\nreturn Response.json('{\"id\":42}')\n .setHeader('x-trace', traceId)\n .setCookieKV('seen', '1')\n .cacheFor(5);\n```\n\nSee [Cookies](../services/cookies.md) for the full cookie builder and [Caching](../services/caching.md) for the caching rules.\n\n## How dispatch works\n\nEvery `@rest` controller registers itself into a global `Rest` registry when its module loads. At request time, `Rest.dispatch(req)` walks the controllers and tries their routes.\n\n```mermaid\nflowchart TD\n A[\"Request arrives\"] --> B[\"Rest.dispatch(req)\"]\n B --> C{\"Any controller<br/>route match<br/>the method + path?\"}\n C -->|\"Yes\"| D[\"Run that method\"]\n D --> E[\"Return its Response\"]\n C -->|\"No\"| F[\"dispatch returns null\"]\n F --> G[\"Your fallback logic<br/>(or Response.unhandled)\"]\n```\n\nMatching order is deterministic: controllers are tried in the order their modules load, and routes within a controller in declaration order. The **first match wins**, so put more specific routes before catch-all ones.\n\n## How routes are discovered\n\n`toiljs build` scans every file under `server/` and finds your decorated classes on its own, so a route file is picked up as soon as it exists. To keep a plain `toilscript` run (and your editor) seeing the same set, projects also `import` each route file once in `server/main.ts`:\n\n```ts\n// server/main.ts (excerpt)\nimport './routes/Players';\nimport './routes/Todos';\n```\n\nAdding a new controller is two steps: write the file, add the one-line import.\n\n## Dispatch and the 404 fallback\n\nInside your handler, the usual pattern is: try REST first, then fall through to anything else.\n\n```ts\nconst hit = Rest.dispatch(req); // Response, or null if nothing matched\nif (hit != null) return hit;\nreturn Response.unhandled(); // no route matched here\n```\n\nThere are two different 404s, and the difference matters:\n\n- **`Response.notFound()`** means \"I looked, and that resource does not exist.\" It is sent to the client as a plain `404`.\n- **`Response.unhandled()`** means \"this server has no route for that path.\" It is a `404` carrying a marker header (`x-toil-unhandled`). The edge (and the dev server) reads that marker and tries to serve the path another way: a static file, or the client-side app. The marker is stripped before anything reaches the browser.\n\nRule of thumb: return `unhandled()` when a path is simply not yours to handle, and `notFound()` when the path is yours but the specific thing is missing.\n\nIf your project is REST-only, you do not need a custom handler at all. toiljs ships `RestHandler`, which does exactly the dispatch-then-`unhandled` dance:\n\n```ts\nimport { Server, RestHandler } from 'toiljs/server/runtime';\nServer.handler = () => new RestHandler();\n```\n\n## Guards: auth, rate limits, caching\n\nYou can stack extra decorators on a route (or a whole controller) to protect or cache it. They compose with the verb decorators:\n\n```ts\n@rest('admin')\nclass Admin {\n @get('/stats')\n @auth // reject with 401 if there is no valid session\n @ratelimit(RateLimit.SlidingWindow, 30, 60) // at most 30 requests per 60 seconds\n public stats(): Stats { /* ... */ }\n}\n```\n\n- `@auth` requires a valid signed session, else the request is rejected with `401` before your method runs. See the [Auth guide](../auth/README.md).\n- `@ratelimit` caps how often a caller may hit the route. See [Rate limiting](../services/ratelimit.md).\n- Response caching is opt-in per response with `.cache(...)` / `.cacheFor(...)`. See [Caching](../services/caching.md).\n\n## A complete CRUD example\n\nA small in-memory players API showing create, read, update, and delete. In a real app you would store players in [ToilDB](../database/README.md) instead of a local map, because each request gets a fresh handler (see [statelessness](./README.md#stateless-by-default)); this example keeps it in memory to stay focused on routing.\n\n```ts\n// server/models/NewPlayer.ts\n@data\nexport class NewPlayer {\n name: string = '';\n}\n```\n\n```ts\n// server/models/Player.ts\n@data\nexport class Player {\n id: u64 = 0;\n name: string = '';\n score: i64 = 0;\n}\n```\n\n```ts\n// server/routes/Players.ts\nimport { Response, RouteContext } from 'toiljs/server/runtime';\nimport { NewPlayer } from '../models/NewPlayer';\nimport { Player } from '../models/Player';\n\n// A stand-in store. Real apps use ToilDB (see the database section).\nconst store = new Map<u64, Player>();\nlet nextId: u64 = 1;\n\n@rest('players')\nclass Players {\n // CREATE: POST /players (JSON body -> Player)\n @post('/')\n public create(input: NewPlayer): Player {\n const p = new Player();\n p.id = nextId++;\n p.name = input.name;\n p.score = 0;\n store.set(p.id, p);\n return p; // 200 with the player as JSON\n }\n\n // READ: GET /players/:id\n @get('/:id')\n public get(ctx: RouteContext): Response {\n const id = u64.parse(ctx.param('id'));\n if (!store.has(id)) return Response.notFound();\n return Response.json(store.get(id).toJSON().toString());\n }\n\n // UPDATE: PUT /players/:id (JSON body -> Player)\n @put('/:id')\n public update(input: NewPlayer, ctx: RouteContext): Response {\n const id = u64.parse(ctx.param('id'));\n if (!store.has(id)) return Response.notFound();\n const p = store.get(id);\n p.name = input.name;\n return Response.json(p.toJSON().toString());\n }\n\n // DELETE: DELETE /players/:id\n @del('/:id')\n public remove(ctx: RouteContext): Response {\n const id = u64.parse(ctx.param('id'));\n if (!store.has(id)) return Response.notFound();\n store.delete(id);\n return Response.empty(204); // 204 No Content\n }\n}\n```\n\nCalling it from anywhere:\n\n```sh\ncurl -X POST localhost:5173/players -d '{\"name\":\"Ada\"}'\ncurl localhost:5173/players/1\ncurl -X PUT localhost:5173/players/1 -d '{\"name\":\"Ada Lovelace\"}'\ncurl -X DELETE localhost:5173/players/1\n```\n\nBecause `@rest` also generates a typed fetch client, your React frontend can call the same routes without writing URLs:\n\n```ts\nawait Server.REST.players.create({ body: new NewPlayer('Ada') });\nawait Server.REST.players.get({ params: { id: 1 } });\n```\n\nSee [RPC and the generated client](./rpc.md#the-rest-fetch-client) for that client.\n\n## Gotchas\n\n- **Fields do not persist.** A fresh controller instance serves each request, so instance fields reset every time and are never shared between requests or edge nodes. Persist to [ToilDB](../database/README.md).\n- **`Response.json` wants a string.** Pass an already-serialized JSON string (or return the `@data` value directly). Passing an object will not do what you expect.\n- **`@del`, not `@delete`.** `delete` is a reserved word, so the decorator is `@del`.\n- **Query values are not URL-decoded in v1.** `ctx.query('q')` returns the raw value; decode it yourself if it may contain percent-encoding.\n- **Path matching is exact on segment count.** `/todos/:id` does not match `/todos/1/extra`. Add a route for the longer path.\n- **`notFound()` vs `unhandled()`.** Returning `notFound()` from your top handler stops the edge from falling through to static files or the client app. Use `unhandled()` for \"not my path.\"\n\n## Related\n\n- [Data types (`@data`)](./data.md): the request and response body structs, and the JSON vs binary codecs.\n- [Typed RPC](./rpc.md): call your backend from your frontend with end-to-end types (and the generated REST fetch client).\n- [Backend overview](./README.md): the request lifecycle and handler model.\n- [Cookies](../services/cookies.md), [Caching](../services/caching.md), [Rate limiting](../services/ratelimit.md): response helpers and guards.\n- [Auth](../auth/README.md): protecting routes with `@auth`.\n- [The database](../database/README.md): persisting data between requests.\n",
17
+ "backend/rpc.md": "# Typed RPC (`@service` / `@remote`)\n\nWrite a server function, tag it `@remote`, and call it from your React frontend like a local async function, with the argument and return types checked end to end.\n\n## What RPC is\n\n**RPC** stands for Remote Procedure Call. The idea is old and simple: call a function that actually runs somewhere else (here, on the edge) as if it were a normal function in your own code. You write a method on the server, and on the client you `await` it. No URLs, no `fetch`, no manual JSON. Just a function call.\n\ntoiljs makes that call **fully typed**. When you build the server, it generates a TypeScript file (`shared/server.ts`) describing every callable function, its arguments, and its return type. Your frontend imports nothing extra: a global object called `Server` is available with all of it typed. If you change a server function's signature and rebuild, your frontend code stops type-checking until you fix the call. The client can never drift from the server.\n\n## Why and when\n\nUse RPC when **your own frontend** talks to **your own backend**. It is the most ergonomic and safest way to do that:\n\n- End-to-end types: rename a field on the server, and the client sees it immediately.\n- No plumbing: no route paths to invent, no request or response shapes to hand-write.\n- Exact numbers: 64-bit-and-larger integers arrive as `bigint`, never rounded.\n\nUse [`@rest`](./rest.md) instead when the caller is **not** your frontend: a webhook, a third-party integration, a public API, a mobile client, or anything that speaks in plain URLs and HTTP methods. RPC uses one internal endpoint and a binary wire format, so it is not meant to be called by hand.\n\nYou can use both in the same project. A common split: RPC for your app's own screens, REST for everything public.\n\n## Declaring callable functions\n\nTwo decorators expose server code to the client:\n\n- **`@remote`** on a top-level function makes it directly callable.\n- **`@service`** on a class groups related `@remote` methods under a namespace.\n\n```ts\n// server/services/Stats.ts\nimport { store } from '../core/store';\n\n@service\nclass Stats {\n @remote\n public playerCount(): i32 {\n return store.size;\n }\n}\n```\n\n```ts\n// server/services/remotes.ts\n@remote\nfunction ping(n: i32): i32 {\n return n + 1;\n}\n```\n\nThat is the whole server side. Build the server and the client can call them.\n\n## Calling from the frontend\n\nThe generated client surfaces everything on a global `Server`. A `@service` becomes a namespace keyed by the class name with a lowercase first letter (`Stats` becomes `stats`). A free `@remote` sits directly on `Server`. Every call returns a `Promise`.\n\n```ts\n// anywhere in your React app, no import needed\nconst count = await Server.stats.playerCount(); // number\nconst next = await Server.ping(41); // number -> 42\n```\n\nAutocomplete, argument checking, and the return type all come from `shared/server.ts`, which the build regenerates every time (and `toiljs dev` regenerates on save), so it is always in sync.\n\n## The round trip\n\nHere is what actually happens under that innocent-looking `await`.\n\n```mermaid\nsequenceDiagram\n participant C as Browser (Server.stats.playerCount)\n participant E as Dacely edge\n participant W as Your backend (WASM)\n C->>C: encode args with DataWriter\n C->>E: POST /__toil_rpc<br/>header dacely-rpc = method id<br/>body = encoded args\n E->>W: dispatch to the matching @remote\n W->>W: run playerCount(), encode the result\n W->>E: encoded result bytes\n E->>C: 200 application/octet-stream\n C->>C: decode into the typed value\n```\n\nA few facts worth knowing:\n\n- Every callable has a stable numeric **method id** (a hash of `\"Service.method\"` or the function name). The client sends it in the `dacely-rpc` header; the server dispatches on it.\n- Arguments and results travel in the compact **binary `@data` codec** (see [Data types](./data.md)), so large integers are exact and payloads are small.\n- The endpoint is a single reserved path, `/__toil_rpc`. You never route it yourself; the framework handles it before your `handle` runs.\n\n## RPC is stateless too\n\nJust like a REST controller, a fresh service instance serves each call. Fields you set on a `@service` class do not survive between calls. If two calls need to share data, that data lives in [ToilDB](../database/README.md), not in an instance field. See [statelessness](./README.md#stateless-by-default).\n\n## Argument and return types\n\nArguments and return values may be scalars, arrays, or [`@data`](./data.md) classes, in both directions. Here is how toilscript's types map to what you see on the TypeScript client:\n\n| ToilScript type | TypeScript type |\n| --- | --- |\n| `u8`, `u16`, `u32`, `i8`, `i16`, `i32`, `f32`, `f64` | `number` |\n| `u64`, `i64`, `u128`, `i128`, `u256`, `i256` | `bigint` |\n| `bool` | `boolean` |\n| `string` | `string` |\n| a `@data` class `T` | `T` (the generated class) |\n| `T[]` | `T[]` |\n\nIntegers of 64 bits or more become `bigint` on the client, so they are exact at any magnitude. See [Types](../concepts/types.md) for the full number story.\n\nPassing a `@data` value is just as easy as a scalar. Construct it on the client and pass it in:\n\n```ts\nimport { NewPlayer } from './shared/server';\n\nconst created = await Server.roster.add(new NewPlayer('Ada')); // returns a typed Player\n```\n\nThe `@data` classes in `shared/server.ts` share a byte-for-byte identical codec with the server, so values round-trip exactly.\n\n## Reading and writing the database from a `@remote`\n\nA `@remote` can use the database, but with a safety default: **a plain `@remote` is read-only.** If it tries to write to ToilDB, the compiler rejects it. To let a `@remote` write, add `@action`:\n\n```ts\n@service\nclass Roster {\n @remote\n public count(): i32 { // read-only: fine to just read\n return db.players.count();\n }\n\n @remote\n @action // opts into writes\n public add(input: NewPlayer): Player {\n return db.players.create(/* ... */);\n }\n}\n```\n\n`@query` is the explicit opposite of `@action`: it marks a function read-only on purpose (the default for a `@remote`, so you rarely need to write it). These are ToilDB **function kinds**; the full rules, including what each kind may and may not do, are in the [database docs](../database/README.md). The takeaway for RPC: reads work out of the box, and a write needs `@action`, so a read-only endpoint can never silently mutate your data.\n\n## Guarding a `@remote`\n\nGuards stack on a `@remote` exactly as they do on a REST route:\n\n```ts\n@service\nclass Stats {\n @remote\n @auth // reject with 401 when there is no valid session\n public secretCount(): i32 {\n return store.size;\n }\n}\n```\n\nThe RPC dispatcher enforces `@auth` (and `@ratelimit`) the same way the REST router does: the guard runs first, and an unauthenticated call gets a `401` before your method body executes. See the [Auth guide](../auth/README.md) and [Rate limiting](../services/ratelimit.md).\n\n## The generated `Server` surface\n\n`shared/server.ts` declares `Server` as a global with a shape like this (schematic):\n\n```ts\ndeclare global {\n const Server: {\n // free @remote functions\n ping(n: number): Promise<number>;\n\n // @service classes, keyed by lowercased name\n readonly stats: {\n playerCount(): Promise<number>;\n secretCount(): Promise<number>;\n };\n\n // @rest controllers get a fetch client under REST (see below)\n readonly REST: { /* ... */ };\n\n // @stream classes get a client under Stream (see the realtime docs)\n readonly Stream: { /* ... */ };\n };\n}\n```\n\nYou never edit this file; the build regenerates it. If a `Server` method throws that it is \"unavailable,\" the generated client has not loaded yet: run the server build (or `toiljs dev`, which does it on save).\n\n## The REST fetch client\n\n`@rest` controllers also get a typed client, under `Server.REST.<controller>.<route>`. It is real `fetch` code (because REST is just HTTP), and it is handy when you want your frontend to call a route you also expose publicly:\n\n```ts\n// controller @rest('players') with a create route taking a NewPlayer body\nconst player = await Server.REST.players.create({\n body: new NewPlayer('Ada'), // present only if the route takes a body\n // params: { id: 7 }, // present only if the path has :params\n query: { ref: 'home' }, // optional\n headers: { 'x-trace': id }, // optional\n});\n```\n\nThe wrapper builds the URL, substitutes `:params`, appends `query`, sends the request, throws on a non-2xx status, and decodes the response into the route's return type. A route declared to return `Response` resolves to the raw `fetch` `Response` so you can inspect headers or stream it yourself. See [REST](./rest.md) for the routes themselves.\n\n## RPC vs REST at a glance\n\n| | RPC (`@service` / `@remote`) | REST (`@rest`) |\n| --- | --- | --- |\n| Caller | Your own frontend | Anyone (browser, webhook, third party, `curl`) |\n| Client | `Server.svc.method(args)` | `Server.REST.ctrl.route(args)`, or plain `fetch` |\n| URL shape | One internal endpoint | Real paths and HTTP methods you design |\n| Wire format | Compact binary `@data` | JSON (or binary), your choice |\n| Types | End to end, automatic | End to end for the generated client |\n| Best for | App-internal calls | Public APIs, integrations, webhooks |\n\n## Gotchas\n\n- **RPC is not a public API.** It uses one reserved endpoint and a binary format meant for the generated client. If an outside system needs to call in, expose a [`@rest`](./rest.md) route.\n- **Instance fields do not persist.** A fresh service instance serves every call. Shared state belongs in [ToilDB](../database/README.md).\n- **Writes need `@action`.** A plain `@remote` is read-only; the compiler rejects a database write unless the method is `@action`.\n- **Rebuild after signature changes.** `shared/server.ts` is generated. If autocomplete looks stale, rebuild the server (`toiljs dev` does this on save).\n- **`bigint`, not `number`, for 64-bit values.** A `u64`/`i64`/`u256` argument or return is a `bigint` on the client. Pass `10n`, not `10`.\n\n## Related\n\n- [Data types (`@data`)](./data.md): the structs your RPC arguments and results are made of, and the binary codec they travel in.\n- [HTTP routes (`@rest`)](./rest.md): the public-facing alternative, and the `Server.REST` fetch client.\n- [Types](../concepts/types.md): `u64`, `u256`, and how they map to `number` / `bigint`.\n- [The database](../database/README.md): `@action` vs `@query`, and persisting state.\n- [Fetching data on the frontend](../frontend/data-fetching.md): using `Server.*` from your React components.\n- [Auth](../auth/README.md): guarding a `@remote` with `@auth`.\n",
18
+ "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### `http_call` is not callable from your code yet\n\nAn honest caveat, because it is easy to assume otherwise: **there is no guest-callable `http_call` API in toiljs today.** The capability itself is fully built on the *host* side (the edge implements the leader-only, SSRF-bounded, metered outbound call as a low-level host import, `daemon.http_call(reqPtr, reqLen, outPtr, outCap) -> i64`, and the dev emulator reserves the same name), but toiljs does **not** ship a friendly TypeScript wrapper you can call from a `@scheduled` method. There is no `daemon.httpCall(...)` (or similarly named) function in the standard library, so your daemon code cannot make an outbound HTTP request at the moment.\n\nTreat outbound HTTP from a daemon as **planned, not yet available**. When the guest binding lands it will behave exactly as described above (leader-fenced, SSRF-bounded, and metered, and its usage will show up in the `Analytics` counters `daemonHttpCallAttempts` / `daemonHttpCallFailures`). Until then, if a daemon needs data from an outside service, have that service write into [ToilDB](../database/README.md) some other way, and let the daemon read it from there.\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",
19
+ "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",
20
+ "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",
21
+ "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, TypeScript, and so on) are installed and new enough, 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` only touches a server project (one with a `toilconfig.json`), and it repairs 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\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",
22
+ "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",
23
+ "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",
24
+ "concepts/security.md": "# Security\n\nSecurity in toil is not a single feature you switch on. It is a set of defaults that are already on, spread across the whole stack, so that the safe path is also the default path.\n\n## The model\n\nToil is built for **defense in depth**: many independent layers, so that one weak spot is not one breach. The goal is the top of our internal security bar, the Resilience and Scale Grade (see [../../RSG.md](../../RSG.md)), where each of these holds at the same time:\n\n- All traffic is encrypted in transit with modern TLS (Transport Layer Security, the encryption every `https://` connection uses).\n- The login design never lets the server see a password it could reuse.\n- Secrets never live in your code.\n- The browser verifies every asset it loads before running it.\n- There is no implicit trust between the parts of the system (zero trust): each request is checked on its own merits.\n\nThe rest of this page walks through each layer and points to where it is documented in full.\n\n## The backend is a sandbox\n\nEvery site (every tenant) on toil runs inside a **sandbox**: a walled-off compartment that can only touch what the host explicitly hands it, and can never reach the host itself or another tenant. Your server code is compiled to **WebAssembly** (wasm, a portable binary instruction format that runs in a tightly controlled virtual machine), and the edge loads each tenant as its own isolated wasm module.\n\nThat isolation is backed by **hard per-host limits**, enforced by the host and not by your code:\n\n- **A committed-memory cap.** Each tenant may commit only a bounded amount of real memory (on the order of tens of MiB). A tenant cannot grow without bound and starve its neighbors.\n- **A compute cap.** Each request gets a bounded compute budget, and when a request exceeds it, the host stops it. A tenant cannot spin forever on the request path.\n- **Rate limits.** How often one client may call a route is capped (see [Transport and rate limiting](#transport-and-rate-limiting) below).\n- **Hostile-wasm containment.** The runtime is hardened to run untrusted, even deliberately malicious, wasm without letting it escape.\n\nThe point of all of this: a buggy or malicious tenant can crash or hang only itself. It cannot escape its sandbox, exhaust the host, or read or corrupt another tenant's data.\n\n## Passwords: post-quantum login\n\nToil ships a complete login system you turn on with one line, and it is built so the server never handles a usable password. See [../auth/README.md](../auth/README.md) for the overview and [../auth/how-it-works.md](../auth/how-it-works.md) for the mechanism.\n\nHere is why that matters. The pattern almost the whole internet uses is \"send the password over TLS, then hash it on the server.\" That is the accepted baseline, but it caps security below the top grade, because for a moment the server holds a live, replayable credential. Anything that reads server memory or a stray log line at the wrong instant walks away with real passwords, and that is one of the most common breach paths in practice.\n\nToil designs that surface away. The proof that you know your password happens without the password ever crossing the wire in a form anyone can replay, so a fully breached server still yields no usable credentials.\n\n## Subresource Integrity (SRI) on the client bundle\n\nThis is the layer that protects the code running in your users' browsers.\n\n### The risk\n\nYour app's JavaScript and CSS are static files served over the network, often through a CDN (Content Delivery Network, the fleet of edge caches that sits between your server and the browser). If any point in that path is compromised (the host, a cache, or the network itself), an attacker can serve **tampered** JavaScript or CSS: a modified bundle that steals sessions, skims form input, or rewrites the page. The browser, on its own, has no way to know the bytes it received are not the bytes you shipped.\n\n### What toil does at build time\n\n**Subresource Integrity (SRI)** is a browser feature that closes this gap. You attach a cryptographic fingerprint of a file to the tag that loads it, and the browser refuses to use the file unless its bytes match that fingerprint.\n\nAt build time (verified in [../../src/compiler/sri.ts](../../src/compiler/sri.ts)), toil adds that fingerprint to every **local** asset tag in the built HTML:\n\n- every `<script src=...>`,\n- every `<link rel=\"modulepreload\" href=...>`,\n- every `<link rel=\"stylesheet\" href=...>`.\n\nTo each it adds two attributes: `integrity=\"sha384-<base64>\"` and `crossorigin=\"anonymous\"`.\n\n- **`sha384`** is the hash algorithm: the file's bytes are run through SHA-384 (a member of the SHA-2 family), producing a fixed fingerprint that changes if even one byte changes. The `integrity` value is that fingerprint, base64-encoded, with a `sha384-` prefix.\n- **`crossorigin=\"anonymous\"`** is required for the browser to actually verify the fingerprint. It is a no-op when the asset is same-origin, so adding it is always safe.\n\nTags that are not toil's to hash are left completely untouched: external URLs (anything with a scheme like `https:`, a protocol-relative `//cdn/...`, or a `data:` URI), inline scripts (a `<script>` with no `src`), and any tag that already carries its own `integrity`.\n\n### Why an import map is also emitted\n\nA per-tag `integrity` only protects the **entry** script: the one file named directly in the tag. But a modern app is an ES module graph (ECMAScript modules, the browser's native `import` system). The entry script statically imports other chunks (for example the shared React vendor chunk), and it lazily pulls in each route with a dynamic `import()` as the user navigates. Those follow-on files are fetched by the browser's **module loader**, which does **not** inherit the entry tag's `integrity`. Without more, everything past the entry point would be unverified.\n\nSo toil also emits an **import map**: a `<script type=\"importmap\">` tag (a standard browser mechanism for describing module resolution). Its `integrity` section maps every emitted JavaScript chunk to its own `sha384` fingerprint. This is the platform mechanism that extends verification to the module loader's fetches, so the **full** dynamic `import()` graph is covered, not just the entry tag. Browsers that do not yet support the `integrity` key simply ignore it (progressive enhancement: nothing breaks on older browsers, they just get one fewer check).\n\n### Why it stays stable\n\nA fingerprint is only useful if it keeps matching the bytes actually served. Toil guarantees that by **content-hashing** every asset it puts SRI on: the build tool (Vite) names each JavaScript chunk and each CSS file with a hash of its own contents baked into the URL (verified in [../../src/compiler/vite.ts](../../src/compiler/vite.ts)). Because the URL is pinned to the exact bytes, the fingerprint baked into the HTML can never falsely mismatch.\n\nThis prevents a real failure mode. Imagine a stylesheet kept a stable URL but its bytes changed on a new deploy. A user still holding an older HTML page (with the old baked fingerprint) would fetch the new bytes, the hashes would disagree, and the browser would **block** the stylesheet, breaking the page for that user. Content-hashing sidesteps this: new bytes get a new URL, so old HTML keeps pointing at the old bytes, and every fingerprint always matches. (Images and fonts keep stable names on purpose: they carry no SRI and are often referenced by fixed path.)\n\n### What the browser does\n\nFor each asset that carries an `integrity` value (from a tag or from the import map), the browser hashes the bytes it received and compares them to the expected fingerprint. If they match, it runs the script or applies the stylesheet. If they do not match, it refuses: the asset is blocked and tampered code never runs.\n\n```mermaid\nflowchart TD\n A[\"Browser loads the built page HTML\"] --> B[\"Reads integrity tags plus the importmap integrity section\"]\n B --> C[\"Fetches an asset: entry script, vendor chunk, route chunk, or stylesheet\"]\n C --> D[\"Computes the sha384 hash of the received bytes\"]\n D --> E{\"Hash matches the baked fingerprint?\"}\n E -->|yes| F[\"Runs the script or applies the stylesheet\"]\n E -->|no| G[\"Refuses: blocks the asset, tampered code never runs\"]\n```\n\n### When it applies\n\nSRI is part of the **production build output**. The dev server hands the browser un-hashed modules straight from Vite, where SRI would be meaningless, so it is not applied there. You get the protection automatically in what you ship, with nothing to configure.\n\n## Secrets\n\nSecrets (API keys, signing keys, and the like) are **never compiled into the wasm**. They are provided per-host at runtime through the secure environment store and read with `Environment.getSecure(key)`, which is kept strictly separate from the plain-variable `Environment.get(key)`: a secret can never leak out through a code path that only reads plain config. Because secrets live outside your code, they are never in your repository or its history. See [../services/environment.md](../services/environment.md) for the full model.\n\n## Transport and rate limiting\n\nAll traffic runs over modern **TLS**, so nothing sensitive travels in the clear. The edge speaks **HTTP/3** (built on QUIC, a faster, head-of-line-blocking-free transport) with graceful fallback to HTTP/2 and HTTP/1.1, so newer clients get the modern path and older clients still connect and work.\n\nOn top of that, you can cap how often a client may hit any given route. Adding the `@ratelimit` decorator to a route makes the edge reject a client that goes over the limit **before your code runs**, which blunts brute-force and abuse and helps absorb bursts and denial-of-service (DDoS) pressure at the edge rather than inside your handler. Only routes that opt in pay any cost; everything else stays on the untouched fast path. See [../services/ratelimit.md](../services/ratelimit.md) for the strategies and per-user (keyed) limits.\n\n## Related\n\n- [Authentication](../auth/README.md): the post-quantum login system and `@auth`-guarded routes.\n- [Environment](../services/environment.md): plain variables versus secrets, and where they live.\n- [Rate limiting](../services/ratelimit.md): the `@ratelimit` decorator, strategies, and keyed limits.\n- [Design principles](../introduction/design-principles.md): the ideas the framework is built on.\n",
25
+ "concepts/tiers.md": "# Compute tiers\n\nYour toiljs backend runs across four **compute tiers**, from a per-request handler at the very edge (L1) up to a single worldwide coordinator (L4). You write one project; the build decides which code belongs to which tier.\n\n## What a tier is\n\nA **tier** is a place your server code runs, defined by two things: how *close* it sits to the user, and how *long* one copy of it lives. The Dacely **edge** is a fleet of servers in many cities. Some work wants to run as close to the user as possible for speed. Other work wants exactly one copy in the whole world so it stays coordinated. A tier is the framework's answer to \"where and for how long should this piece of code live?\"\n\nThere are four tiers, named L1 through L4. The number is the **scope**: L1 is the smallest and nearest (one edge node), and each higher number widens the scope until L4 covers the entire globe.\n\n```mermaid\nflowchart TB\n subgraph L4[\"L4 - Global (one leader worldwide)\"]\n direction TB\n D[\"@daemon / @scheduled<br/>rollups, cleanup, polling\"]\n subgraph L3[\"L3 - Continental (per continent)\"]\n direction TB\n C[\"@stream (Continental scope)<br/>cross-region coordination\"]\n subgraph L2[\"L2 - Regional (per region)\"]\n direction TB\n R[\"@stream (Regional scope)<br/>chat, presence, live cursors\"]\n subgraph L1[\"L1 - Hot / edge (per request)\"]\n H[\"@rest, @service, @remote<br/>HTTP + RPC, the default\"]\n end\n end\n end\n end\n```\n\nRead the diagram as concentric rings: L1 is the inner ring nearest the user, and each outer ring is a wider, longer-lived scope. Most of your code lives in the innermost ring.\n\n## The four tiers at a glance\n\n| Tier | Name | How close | How many copies | Lives for | Runs |\n| --- | --- | --- | --- | --- | --- |\n| **L1** | Hot / edge | The exact node nearest the user | A fresh one per request | One request | `@rest`, `@service` / `@remote` |\n| **L2** | Regional | A region (a group of nearby cities) | One box per connection | The connection | `@stream` (Regional scope) |\n| **L3** | Continental | A continent | One box per connection | The connection | `@stream` (Continental scope) |\n| **L4** | Global / daemon | One place worldwide | Exactly one leader (plus a warm standby) | Until redeployed | `@daemon`, `@scheduled` |\n\nThe tier names come straight from the edge runtime (its node roles are `Hot`, `Regional`, `Continental`, and `Daemon`).\n\n## L1: Hot (request/response)\n\n**What:** a plain request-in, response-out handler. This is the default tier and where most code lives.\n\n**How it behaves:** a fresh copy of your handler serves each request, on whatever node is nearest the caller. Nothing you store on a field survives to the next request, because the next request may be a brand-new copy on the other side of the planet. This is called being **stateless**. It is what lets your app scale to the whole world with no coordination. When you need something to persist, you write it to [ToilDB](../database/README.md), the shared database.\n\nL1 is the lowest latency tier: the code runs on the node the user already reached, so there is no extra hop.\n\n```ts\n// L1: a fresh box per request. `hits` does NOT persist across requests.\n@rest('hello')\nclass Hello {\n private hits: i32 = 0; // reset every request\n\n @get('/')\n public greet(): Response {\n this.hits = this.hits + 1; // always 1\n return Response.text('hi\\n');\n }\n}\n```\n\nSee [REST](../backend/rest.md) and [RPC](../backend/rpc.md).\n\n## L2 and L3: streams (long-lived connections)\n\n**What:** a **stream** is a long-lived connection (think a chat socket) where the server keeps state *per connected client* for as long as they stay connected.\n\n**How it behaves:** when a client connects, the edge creates one resident **box** (one running copy of your `@stream` class) and pins it to a single worker. That same box handles every message from that client and is torn down when they leave. Because the box stays alive, its fields *do* persist across events, unlike L1. This is what makes a running message count, a game session, or a live cursor possible.\n\nA `@stream` picks its scope with `@stream({ scope })`:\n\n- **`StreamScope.Regional`** puts the box in the user's region (L2). Closest, lowest latency, smallest blast radius. Use it for per-connection state that only that one client cares about.\n- **`StreamScope.Continental`** puts the box at a continent-wide node (L3). A little farther, but many clients across a continent can be steered to the *same* box, which is what you want when connected users must share state (a shared room, a leaderboard everyone updates).\n\nThe trade-off is the theme of this whole page: nearer (L2) is faster; wider (L3) lets more clients coordinate on one box.\n\n```ts\n// L2/L3: the box is resident, so `count` survives every message.\n@stream({ scope: StreamScope.Regional })\nclass Echo {\n private count: i32 = 0;\n\n @connect onConnect(): StreamOutbound {\n this.count = 0;\n return StreamOutbound.accept();\n }\n @message onMessage(pkt: StreamPacket): StreamOutbound {\n this.count = this.count + 1; // persists across events\n return StreamOutbound.empty();\n }\n @close onClose(): void { /* box torn down after this */ }\n}\n```\n\nSee [Realtime streams](../realtime/streams.md).\n\n## L4: the daemon (global coordination)\n\n**What:** the daemon is a single, elected leader for your whole app, running recurring background work on a schedule.\n\n**How it behaves:** unlike every other tier, there is exactly **one** live daemon box in the entire world for your domain (with a warm standby ready to take over if the leader fails). It is not tied to any request or connection. It wakes on a cadence (an interval like `\"1h\"`, or a cron schedule) and does coordination work: rolling up analytics, cleaning up expired rows, polling an upstream API. Because there is only ever one leader, a `@scheduled` task runs **at most once** per due tick across the planet, which is exactly what you want for work that must not double-fire.\n\nL4 is the highest-latency, most-consistent tier: it is far from any single user, but it is the one place where \"do this exactly once, globally\" is true.\n\n```ts\n// L4: one leader worldwide runs this hourly.\n@daemon\nclass Jobs {\n @scheduled('1h')\n hourly(): void {\n // rollups, cleanup, polling an upstream, ...\n }\n}\n```\n\nSee [Daemons](../background/daemons.md).\n\n## How code is routed to a tier\n\nYou do not configure tiers. You opt into one by adding an **entry file** with the right name and using the matching **surface decorator**. A surface decorator is the top-level decorator that says what kind of endpoint a class is (`@rest`, `@stream`, `@daemon`).\n\nAt build time, `toiljs build` runs the compiler once per tier and hands each pass only the code that belongs to it. Each tier compiles into its own WebAssembly file (its own `.wasm` **artifact**), so the request build never carries stream or daemon code, and vice versa.\n\n| Entry file (`server/`) | Surface decorator | Artifact | Tier |\n| --- | --- | --- | --- |\n| `main.ts` | `@rest`, `@service` / `@remote` | `release.wasm` | L1 request |\n| `main.stream.ts` | `@stream` | `release-stream.wasm` | L2 / L3 stream |\n| `main.daemon.ts` | `@daemon`, `@scheduled` | `release-cold.wasm` | L4 daemon |\n\nShared building blocks carry no tier of their own. Your [`@data` classes](../backend/data.md) and your [`@database` schema](../database/README.md) are compiled into *every* artifact, because any tier may need to read or write the database. The stream and daemon tiers are opt-in: a project with no `@stream` and no `@daemon` just builds the single `release.wasm` and behaves exactly like a request-only app.\n\nFor the entry-file conventions and the full build flow, see [Project structure](../getting-started/project-structure.md).\n\n## Why tiers matter: latency vs consistency\n\nPicking a tier is a trade-off between two things a junior developer should keep in mind:\n\n- **Latency** (how fast a single user gets an answer): lower tiers win. L1 runs on the node the user already reached.\n- **Consistency and shared state** (many actors agreeing on one thing): higher tiers win. L4 has exactly one copy in the world, so it is trivially consistent, but it is far from everyone.\n\nA rule of thumb:\n\n- Answering one request as fast as possible: **L1**.\n- Keeping per-connection state for one live client: **L2**.\n- Letting many connected clients across a continent share one box's state: **L3**.\n- Doing a job exactly once, globally, on a schedule: **L4**.\n\n## Gotchas\n\n- **L1 fields reset every request.** If you set a field in one request and read it in the next, it will be gone. Persist to [ToilDB](../database/README.md) instead.\n- **A project using `@stream` may not also declare `@service` or `@remote`** anywhere (the compiler enforces this). Streams and RPC live in different artifacts. Keep them in separate entry files (`main.stream.ts` vs `main.ts`).\n- **There is at most one `@daemon` class per project.** It compiles only into the cold artifact; a `@daemon` in the request build is a compile error.\n- **Higher tier is not \"better\".** Do not reach for L4 to hold shared state you read on every request; that adds a long hop. Use the database for shared reads, and the daemon only for scheduled, run-once work.\n\n## Related\n\n- [Backend overview](../backend/README.md): the L1 request model in depth.\n- [REST](../backend/rest.md) and [RPC](../backend/rpc.md): the L1 surfaces.\n- [Realtime streams](../realtime/streams.md): the L2 / L3 surface and `StreamScope`.\n- [Daemons](../background/daemons.md) and [@derive](../background/derive.md): scheduled and background work.\n- [The database (ToilDB)](../database/README.md): where shared, persistent state lives.\n- [Decorators](./decorators.md): the full list of surface decorators and their tiers.\n- [Project structure](../getting-started/project-structure.md): entry files and the build.\n",
26
+ "concepts/types.md": "# The server type system\n\nYour backend is TypeScript, but a stricter, faster dialect of it. Numbers have an exact bit width (`u32`, `i64`, `f64`, and friends) instead of one loose `number`, and a few TypeScript features do not exist. This page explains the types you write in `server/` code.\n\n## Why the server types are different\n\nYour frontend TypeScript runs in a browser, where a JavaScript engine figures out types as it goes. Your **server** code is different: the **toilscript** compiler turns it into [WebAssembly](../backend/README.md) (WASM), a compact machine-like binary. To emit that binary, the compiler must know the *exact* size of every value ahead of time. \"A number\" is not enough; it needs to know \"a 32-bit unsigned integer\" so it can pick the right machine instruction and lay out memory.\n\ntoilscript is built on **AssemblyScript**, a strict subset of TypeScript designed to compile to WASM. So the server language looks and reads like TypeScript, but every number is a fixed-width type, and there is no room for the fuzzy parts of JavaScript. The payoff is speed and safety: your code compiles to something close to hand-written native code, and whole classes of bugs cannot occur.\n\nYou already know the syntax. You only need to learn the number types and a handful of rules.\n\n## The number types\n\nThere is no plain `number` in server code. Instead you pick the exact type. The name tells you three things: signed or unsigned, how many bits, integer or float.\n\n- The letter: `u` = unsigned integer (zero and up), `i` = signed integer (can be negative), `f` = floating-point (has a fractional part).\n- The number: how many bits wide it is.\n\n| Type | Kind | Bits | Range | Use it for |\n| --- | --- | --- | --- | --- |\n| `bool` | boolean | 1 | `true` / `false` | flags, yes/no |\n| `u8` | unsigned int | 8 | 0 to 255 | a byte, a small enum |\n| `u16` | unsigned int | 16 | 0 to 65,535 | ports, small counts |\n| `u32` | unsigned int | 32 | 0 to ~4.29 billion | sizes, ids, most counts |\n| `u64` | unsigned int | 64 | 0 to ~1.8e19 | timestamps (ms), big counters, large ids |\n| `i8` | signed int | 8 | -128 to 127 | tiny signed values |\n| `i16` | signed int | 16 | -32,768 to 32,767 | small signed values |\n| `i32` | signed int | 32 | ~-2.1 to 2.1 billion | the default integer; loop counters, deltas |\n| `i64` | signed int | 64 | ~-9.2e18 to 9.2e18 | large signed counts, database counters |\n| `f32` | float | 32 | ~7 decimal digits | low-precision decimals (rare) |\n| `f64` | float | 64 | ~15-16 decimal digits | any value with a fraction: prices, ratios, math |\n\nThere are also 128-bit and 256-bit integers (`u128`, `i128`, `u256`, `i256`) for cryptography and very large ids. See [Big integers](#big-integers-u128-to-u256) below.\n\n### Which one should I pick?\n\nWhen in doubt, use these defaults and you will rarely be wrong:\n\n- A counter, a size, a loop index, an id you do not do math on: **`u32`** (or `i32` if it can go negative).\n- A millisecond timestamp, a like count that might grow huge, or a value that must never wrap: **`u64`** / **`i64`**.\n- Anything with a decimal point (money, averages, percentages): **`f64`**.\n- A true/false flag: **`bool`**.\n- One raw byte, or an element of a byte buffer: **`u8`**.\n\nSmaller types do not make your program meaningfully faster; they exist to match binary layouts and save memory in big arrays. Default to 32-bit unless a value is genuinely large or genuinely a byte.\n\n## Integer overflow: it wraps, it does not throw\n\nThis is the single most important difference from everyday JavaScript. Integer math is **modular**: if a value goes past the top of its range, it silently wraps around to the bottom (two's complement). It never throws an error and never turns into a bigger type on its own.\n\n```ts\nlet x: u8 = 255;\nx = x + 1; // x is now 0, not 256 (wrapped around)\n\nlet y: u8 = 0;\ny = y - 1; // y is now 255 (wrapped the other way)\n\nlet z: i32 = 2147483647; // the largest i32\nz = z + 1; // z is now -2147483648 (wrapped to the minimum)\n```\n\nThe lesson: choose a type wide enough that your values cannot reach its edge. A counter that could exceed ~4.29 billion needs `u64`, not `u32`. Timestamps in milliseconds always use `u64`.\n\nFloating-point (`f32` / `f64`) does not wrap; it follows the usual IEEE rules (very large values become `Infinity`, `0/0` is `NaN`).\n\n## Casting between number types\n\nBecause types are explicit, the compiler will not silently mix them. To convert, cast. There are two equivalent spellings; use whichever reads better:\n\n```ts\nconst big: u64 = 300;\nconst small: u8 = big as u8; // \"as\" form\nconst small2 = u8(big); // call the type like a function\n```\n\nCasting a wider integer into a narrower one **truncates**: it keeps only the low bits, so `u8(300)` is `44` (300 wraps modulo 256). Casting a float to an integer drops the fraction toward zero (`i32(3.9)` is `3`). None of these throw, so cast deliberately.\n\n```ts\nconst f: f64 = 3.9;\nconst i = i32(f); // 3 (fraction dropped)\nconst n = u8(300); // 44 (truncated to 8 bits)\n```\n\nInteger division also truncates (it is not float division):\n\n```ts\nconst half = 5 / 2; // both operands are i32 -> result is 2, not 2.5\nconst real = 5.0 / 2.0; // f64 division -> 2.5\n```\n\n## `bool`\n\n`bool` is a real 1-bit type, not \"any truthy value\". Comparisons (`==`, `<`, `>=`) produce a `bool`, and `if`/`while` expect one. It stores as 0 or 1.\n\n## Big integers: `u128` to `u256`\n\nFor values too large for 64 bits, toilscript provides `u128`, `i128`, `u256`, and `i256` as native global types (no import needed). They support the normal operators (`+`, `-`, `*`, `/`, `==`, `<`, `<<`, and so on) through operator overloading, plus static helpers like `u256.fromString(...)` and instance methods like `.toString()`, `.toBytes()`, and `.toUint8Array()`.\n\n```ts\nconst a = u256.fromString('123456789012345678901234567890');\nconst b = a + u256.One;\nconst hex = b.toString(16);\n```\n\nThese are mainly for cryptography and very large ids. Everyday counts and sizes should stay in `u32` / `u64`.\n\n### `ToilUserId`: a 256-bit identity\n\nThe built-in auth system represents a logged-in user as a **`ToilUserId`**, a stable 256-bit value (four `u64` words) derived from their key, identifier, and your domain. It is a global type, like `crypto`. It is the right key to store per-user data on. Comparison is overloaded and O(1), and `.toHex()` gives you a 64-character string key.\n\n```ts\nconst id: ToilUserId = AuthService.userId()!; // the current user's stable id\nconst key = id.toHex(); // a convenient string key\n```\n\nFull reference (including the `==` null-check gotcha) is in [Extending auth](../auth/extending.md).\n\n## Strings\n\nStrings work as you expect: `'hello'`, template literals, `.length`, `.substring(...)`, `+` to concatenate. Under the hood a server string is **UTF-16** (the same 16-bit code units as JavaScript), so `.length` counts code units, not visible characters or bytes.\n\nWhen you need the **bytes** of a string (to hash it, write it to a binary body, or send it over a stream), encode it explicitly. UTF-8 is almost always what you want on the wire:\n\n```ts\nconst buf: ArrayBuffer = String.UTF8.encode('hello'); // UTF-8 bytes\nconst text: string = String.UTF8.decode(buf); // back to a string\n```\n\n`String.UTF8.byteLength(s)` gives the encoded byte length. There is a matching `String.UTF16` namespace when you need raw UTF-16 bytes.\n\n## Binary data: `Uint8Array`\n\nRaw bytes are a `Uint8Array` (a fixed-length array of `u8`), exactly like the browser type. It is the standard currency for request bodies, hashes, crypto keys, and stream packets.\n\n```ts\nconst bytes = new Uint8Array(32);\nbytes[0] = 0xff; // each element is a u8\nconst n: i32 = bytes.length;\n```\n\nYou will also see `StaticArray<u8>` (a fixed-size, lower-overhead byte array) and `ArrayBuffer` (a raw buffer that `Uint8Array` and the encoders view). For most app code, `Uint8Array` is all you need.\n\n## Arrays and collections\n\nTyped arrays and the familiar collection types are available and must be typed:\n\n```ts\nconst nums: i32[] = [1, 2, 3]; // Array<i32>\nconst names = new Array<string>();\nconst seen = new Map<string, u32>();\nconst tags = new Set<string>();\n```\n\n`Array`, `Map`, and `Set` behave like their JavaScript counterparts (`.push`, `.get`, `.has`, `.forEach`), but every element type is fixed at compile time. There is no mixed-type array.\n\n## Objects: `@data` classes, not object literals\n\nServer code has no free-form object type. A structured value is a **class**, and to send it between the browser and the server (or in and out of the database) you tag it `@data`. That makes the compiler generate a binary codec plus a matching client type. See [Data types](../backend/data.md).\n\n```ts\n@data\nclass Player {\n username: string = '';\n score: u64 = 0;\n constructor(username: string = '', score: u64 = 0) {\n this.username = username;\n this.score = score;\n }\n}\n```\n\n## Key differences from normal TypeScript\n\n| Thing | Normal TypeScript | Server (toilscript) |\n| --- | --- | --- |\n| Numbers | one `number` | explicit `u8` / `i32` / `u64` / `f64` / ... |\n| `number` type | everywhere | avoid it; pick a fixed-width type |\n| `any` | allowed | not allowed: everything is typed |\n| Integer overflow | numbers just get bigger | wraps around silently |\n| `/` on integers | `5 / 2` is `2.5` | `5 / 2` is `2` (truncates) |\n| Mixing number types | implicit | explicit cast (`x as u8` / `u8(x)`) |\n| npm packages | `import` anything | only toilscript's standard library and toiljs APIs |\n| `undefined` | common | use `null` (a `T | null`), not `undefined` |\n| Objects | `{ a: 1 }` literals | a `class` (usually `@data`) |\n| Strings | UTF-16 | UTF-16 (same), encode to bytes for the wire |\n\nA few rules worth stating plainly:\n\n- **No `any`.** Every value has a concrete type. This is what makes the code compile to WASM at all.\n- **No arbitrary npm.** Server code cannot pull in npm packages; it uses the toilscript standard library (the types on this page) plus the toiljs host APIs (database, crypto, email, and so on). This is the sandbox that keeps the edge safe.\n- **No plain `number`.** If you write `number`, it resolves to `f64` (a float), which is almost never what you want for an id or a count. Always pick an explicit type.\n- **Use `null`, not `undefined`,** for \"no value\": a nullable is written `T | null` and you narrow it with an `if (x != null)` check or a `!` assertion when you are sure.\n\n## How server types cross to the browser\n\nWhen a `@data` value or an RPC result travels to your frontend, the compiler maps each server type to a matching TypeScript type in the generated client. You do not write this mapping; it is generated. It matters because it tells you what your React code receives.\n\n| Server type | Browser (generated TS) |\n| --- | --- |\n| `u8`, `u16`, `u32`, `i8`, `i16`, `i32`, `f32`, `f64` | `number` |\n| `u64`, `i64`, `u128`, `i128`, `u256`, `i256` | `bigint` |\n| `bool` | `boolean` |\n| `string` | `string` |\n| a `@data` class `T` | the generated class `T` |\n| `T[]` | `T[]` |\n\nThe important row is the 64-bit-and-larger integers: they become `bigint` on the client and travel as decimal strings on the JSON wire, so they stay **exact at any size** (they never lose precision the way a giant JavaScript `number` would). See [RPC](../backend/rpc.md) for the generated client.\n\n## Related\n\n- [Data types (`@data`)](../backend/data.md): defining structs that cross the wire and the database.\n- [RPC and the generated client](../backend/rpc.md): how server types map to browser types.\n- [Extending auth](../auth/extending.md): the `ToilUserId` 256-bit identity in full.\n- [The database (ToilDB)](../database/README.md): the collections your typed keys and values live in.\n- [Decorators](./decorators.md): the decorators referenced throughout this page.\n",
27
+ "database/capacity.md": "# Capacity (limited stock without overselling)\n\nA `Capacity` collection models a **finite resource that must never be oversold**:\nconcert tickets, seats, limited-drop stock, rate grants. It uses a two-phase\n`reserve` then `confirm`/`cancel` pattern so two buyers can never grab the same\nlast unit.\n\n## What and why\n\nSome resources are strictly limited. There are 100 tickets, and selling 101 is a\nreal, expensive mistake. The classic bug is a race: two requests both read \"1\nleft\", both decide they can sell it, and both sell it. Now you have oversold.\n\nA `Capacity` collection prevents that. Instead of \"read, then decrement\" (two\nsteps a race can slip between), it does an atomic **hold**: the check that enough\nis available and the decrement happen as one indivisible step, at one place. And\nit adds a second phase so a shopper who starts a checkout but never pays does not\nlock up stock forever.\n\nThe lifecycle has three moves:\n\n- **`reserve`**: hold some units for a short time. The units come out of\n \"available\" immediately, and you get back a **reservation id**. If there is not\n enough available, the reserve fails (and nothing is oversold).\n- **`confirm`**: the shopper paid. Turn the hold into a permanent sale.\n- **`cancel`**: the shopper backed out. Release the hold back to available.\n\nIf a hold is neither confirmed nor cancelled within its time limit (its **TTL**,\ntime to live), it **auto-releases**. So an abandoned checkout heals itself.\n\n```mermaid\nsequenceDiagram\n participant Buyer\n participant App as Your route\n participant Cap as Capacity ledger\n Buyer->>App: start checkout\n App->>Cap: reserve(key, 2, ttl)\n Cap-->>App: reservationId (available drops by 2)\n Note over Cap: 2 units are held, not yet sold\n alt payment succeeds\n Buyer->>App: pay\n App->>Cap: confirm(key, reservationId)\n Cap-->>App: true (hold becomes a permanent sale)\n else buyer cancels\n Buyer->>App: cancel\n App->>Cap: cancel(key, reservationId)\n Cap-->>App: true (2 units returned to available)\n else buyer disappears\n Note over Cap: TTL passes, hold auto-releases (2 units returned)\n end\n```\n\nUse `Capacity` whenever selling one too many is unacceptable: tickets, seats,\ninventory for a limited drop, a pool of licences, or any \"N and no more\" resource.\n\n## Why not just a counter?\n\nA [Counter](./counters.md) can only `add` and `get`. To sell a ticket with a\ncounter you would read the remaining count, check it is above zero, and then\nsubtract one. Those are separate steps. Two requests can both read \"1 left\"\nbefore either subtracts, and both proceed: **oversold**. A counter has no way to\natomically check-and-decrement, and no notion of a temporary hold that expires.\n\n`Capacity` fixes both problems:\n\n- The availability check and the hold are **one atomic step**, so contenders are\n serialized: the first reserve takes the unit, the second sees zero available\n and fails.\n- The two-phase **reserve then confirm/cancel** means an unpaid, abandoned\n checkout does not permanently consume stock: its hold expires on the TTL.\n\nIf a small over- or under-count is harmless (likes, page views, an\napproximate inventory display), a counter is simpler and cheaper. If overselling\nby one is a real problem, use `Capacity`.\n\n## The type\n\n```ts\nCapacity<K>\n```\n\n`Capacity` has a single type parameter, the **key** (`K`): it picks *which*\nresource. There is no value type, because the value is a private ledger the\nplatform manages for you (it tracks the total, the confirmed sales, and the live\nholds). For one ticketed event, the key is the event id.\n\nDeclare it as a `@collection` field on a `@database` class:\n\n```ts\n@data\nclass ShowKey {\n show: string = '';\n constructor(show: string = '') { this.show = show; }\n}\n\n@database\nclass TicketsDb {\n @collection static seats: Capacity<ShowKey>;\n}\n```\n\n## Operations\n\nFive operations, matching the toilscript API exactly. Exact signatures:\n\n| Operation | Signature | What it does |\n| --- | --- | --- |\n| `available` | `available(key: K): i64` | How many units can be reserved right now. |\n| `reserve` | `reserve(key: K, amount: i64, ttlMs: i64): u64` | Hold `amount` units for `ttlMs` milliseconds. Returns a reservation id (> 0), or `0` if there was not enough. |\n| `confirm` | `confirm(key: K, reservationId: u64): bool` | Turn a hold into a permanent sale. Returns whether the id was valid. |\n| `cancel` | `cancel(key: K, reservationId: u64): bool` | Release a hold back to available. Returns whether it was released. |\n| `setTotal` | `setTotal(key: K, total: i64): void` | Set the ceiling (seed or restock). Background task only. |\n\nThe core accounting the platform maintains for you:\n\n```txt\navailable = total - confirmed - held\n```\n\n`total` is the ceiling you set. `confirmed` is units permanently sold. `held` is\nunits currently reserved but not yet confirmed. `available` never goes below zero.\n\n### `available`\n\nReads how many units could be reserved right now. It is a keyed read, legal from\nany handler including a `@get`.\n\n```ts\nconst left = TicketsDb.seats.available(new ShowKey('jazz-night'));\n```\n\n### `reserve`\n\nHolds `amount` units for `ttlMs` milliseconds and returns a reservation id. This\nis the operation that prevents overselling.\n\n```ts\nconst id = TicketsDb.seats.reserve(new ShowKey('jazz-night'), 2, 120_000); // hold 2 for 2 minutes\nif (id == 0) {\n // Not enough available. This is a normal outcome, NOT an error: no oversell.\n return Response.conflict('sold out');\n}\n// id > 0: you now hold 2 units for up to 2 minutes. Keep the id.\n```\n\nTwo things to know:\n\n- A return of `0` means \"not enough available\". It is the safe, expected answer\n when the resource is (nearly) sold out. Always check for it.\n- The hold auto-releases after `ttlMs` if you do not confirm or cancel it. Choose\n a TTL that covers a realistic checkout (a couple of minutes), not hours.\n\n`reserve` is a **write**, so call it from an action handler (`@post`, `@put`,\n`@patch`, `@del`), not from a `@get`.\n\n### `confirm`\n\nTurns a hold into a permanent sale. Call it once payment (or whatever finalizes\nthe deal) succeeds.\n\n```ts\nconst ok = TicketsDb.seats.confirm(new ShowKey('jazz-night'), id);\n// ok === true -> the id was valid; those units are now permanently sold\n// ok === false -> the id was unknown (never reserved, or already expired)\n```\n\nA confirmed sale is **permanent**: it can never be cancelled and its TTL never\nreclaims it. Confirm is safe to call more than once for the same id (it stays\nconfirmed).\n\n### `cancel`\n\nReleases a hold back to available. Call it when the shopper backs out before\npaying.\n\n```ts\nconst released = TicketsDb.seats.cancel(new ShowKey('jazz-night'), id);\n// released === true -> the hold was released, units are available again\n// released === false -> the id was unknown, already expired, OR already confirmed\n```\n\nYou **cannot** cancel a confirmed sale (a sale is final). `cancel` returns `false`\nin that case.\n\n### `setTotal`\n\nSets the ceiling: the total number of units. You call it to **seed** a new\nresource (\"this show has 100 seats\") and to **restock** (\"we opened the balcony,\nnow 150\"). Existing holds and confirmed sales are untouched; only the ceiling\nmoves, and `available` reflects the new total.\n\n`setTotal` is a **privileged** operation: it may only run from a background task\n(a `@job`), never from a request handler. Seeding and restocking are\nadministrative actions, so they live off the request path. See\n[background tasks](../background/README.md).\n\n```ts\n@database\nclass TicketsDb {\n @collection static seats: Capacity<ShowKey>;\n\n // A background task seeds/restocks the ceiling. This runs off the request path.\n @job\n openSeats(): void {\n TicketsDb.seats.setTotal(new ShowKey('jazz-night'), 100);\n }\n}\n```\n\nNote that lowering the total below what is already sold plus held does not\ncancel anything (confirmed sales are permanent); `available` simply floors at\nzero until holds clear.\n\n## The lifecycle in words\n\n1. **Seed.** A background `@job` calls `setTotal(key, 100)`. Now `available` is\n 100.\n2. **Reserve.** A buyer starts checkout. Your `@post` calls `reserve(key, 1,\n ttl)` and gets an id. `available` drops to 99. The unit is *held*, not sold.\n3. **Finalize, one of:**\n - **Confirm.** Payment succeeds; your `@post` calls `confirm(key, id)`. The\n unit is now permanently sold. `available` stays 99.\n - **Cancel.** The buyer backs out; your `@post` calls `cancel(key, id)`. The\n unit returns to available; `available` goes back to 100.\n - **Expire.** The buyer vanishes. After `ttl` passes, the hold auto-releases;\n `available` goes back to 100 on its own.\n\nAt no point can the confirmed count exceed the total. That is the guarantee.\n\n## Worked example: selling limited tickets\n\n```ts\nimport { ShowKey } from '../models/ShowKey';\nimport { ReserveRequest } from '../models/ReserveRequest';\nimport { FinalizeRequest } from '../models/FinalizeRequest';\nimport { ReserveResult } from '../models/ReserveResult';\n\n@database\nclass TicketsDb {\n @collection static seats: Capacity<ShowKey>;\n\n // Background task: seed the show's capacity (and restock if you reopen it).\n @job\n openSeats(): void {\n TicketsDb.seats.setTotal(new ShowKey('jazz-night'), 100);\n }\n}\n\n@rest('tickets')\nclass Tickets {\n // How many seats are left (a keyed read, legal in a GET).\n @get('/jazz-night/available')\n public left(): i64 {\n return TicketsDb.seats.available(new ShowKey('jazz-night'));\n }\n\n // Start checkout: hold the seats. Returns a reservation id, or 0 if sold out.\n @post('/jazz-night/reserve')\n public reserve(input: ReserveRequest): ReserveResult {\n const id = TicketsDb.seats.reserve(new ShowKey('jazz-night'), input.count, 120_000);\n // id === 0 means not enough available. No oversell; tell the buyer honestly.\n return new ReserveResult(id, id != 0);\n }\n\n // Payment succeeded: make the hold a permanent sale.\n @post('/jazz-night/confirm')\n public confirm(input: FinalizeRequest): bool {\n return TicketsDb.seats.confirm(new ShowKey('jazz-night'), input.reservationId);\n }\n\n // Buyer backed out before paying: release the hold.\n @post('/jazz-night/cancel')\n public cancel(input: FinalizeRequest): bool {\n return TicketsDb.seats.cancel(new ShowKey('jazz-night'), input.reservationId);\n }\n}\n```\n\nThe models:\n\n```ts\n@data\nexport class ReserveRequest {\n count: i64 = 1;\n}\n\n@data\nexport class FinalizeRequest {\n reservationId: u64 = 0;\n}\n\n@data\nexport class ReserveResult {\n reservationId: u64 = 0;\n ok: bool = false;\n constructor(reservationId: u64 = 0, ok: bool = false) {\n this.reservationId = reservationId;\n this.ok = ok;\n }\n}\n```\n\nEven if a thousand people hit `reserve` at the same instant for the last few\nseats, the ledger serializes them: the first buyers get ids, the rest get `0`.\nThe total is never exceeded.\n\n## Consistency\n\nUnlike most ToilDB families, `Capacity` is **strongly consistent**, and that is\nthe whole point. Every reserve/confirm/cancel for a key is routed to that key's\none home location and applied there in order, at a single serialization point. So\neven though ToilDB spans many regions, there is exactly one place that decides\nwhether a reserve succeeds. That is what makes \"never oversell\" a hard guarantee\nrather than a best effort. `available` reflects that home ledger.\n\n## Gotchas\n\n- **Check for `0` from `reserve`.** A `0` is \"not enough available\", the normal\n sold-out answer, not an error. Do not treat it as a failure to retry blindly.\n- **Always finalize a reservation.** After a successful `reserve`, you should\n `confirm` it (paid) or `cancel` it (abandoned). If you do neither, the hold\n sits until its TTL expires, temporarily reducing availability.\n- **Pick a sensible TTL.** Too short and a slow-but-real checkout loses its hold\n mid-payment. Too long and abandoned carts starve real buyers. A couple of\n minutes is typical for a checkout.\n- **Confirmed sales are permanent.** You cannot `cancel` a confirmed sale, and\n its TTL never reclaims it. If you support refunds, model that as your own\n restock (raise `setTotal`), not as a cancel.\n- **`reserve` is not automatically deduplicated.** Each call to `reserve` creates\n a new, distinct hold. If a client might retry the same reserve, avoid holding\n twice: reserve once, keep the returned id, and drive `confirm`/`cancel` off\n that id.\n- **`setTotal` is background-only.** You cannot set the total from a route. Seed\n and restock from a `@job`.\n- **There is a per-key cap on live holds.** A key can carry only so many\n simultaneous unconfirmed holds; a flood of holds beyond that is rejected as\n \"not enough available\" (and abandoned holds clear on their TTL). Normal traffic\n never hits this; it is a guardrail against a hold flood.\n\n## Related\n\n- [Counters](./counters.md): a running total when overselling is not a concern\n (and why it cannot guarantee a limit).\n- [Documents](./documents.md): store the order/booking record that a confirmed\n sale produces.\n- [background tasks](../background/README.md): where `setTotal` (seeding/restock)\n runs.\n- [Data types (`@data`)](../concepts/types.md): how the capacity key is stored.\n- [Decorators](../concepts/decorators.md): which handler kinds may reserve,\n confirm, and cancel.\n",
28
+ "database/counters.md": "# Counters\n\nThe **Counter** family is a distributed running total that many callers can increase at the same time, from anywhere in the world, without ever losing a count. It is how you build likes, view counts, and inventory tallies correctly.\n\n## What and why\n\nA **Counter collection** maps a `@data` key to a single whole number. You do just two things with it: read the current total (`get`) and add to it (`add`). There is deliberately **no \"set to this value\"** operation, and that missing operation is the entire point of the family, as the next section explains.\n\nReach for a Counter whenever the thing you are storing is a number that many requests bump concurrently:\n\n- likes or upvotes on a post\n- page views or play counts\n- a stock or quantity tally\n- any \"increase this by N\" metric\n\nDeclare one by typing a `@collection` as `Counter<Key>`. Notice there is no value type: the value is always a number the database manages for you.\n\n```ts\n@data\nclass PostId {\n id: string = '';\n constructor(id: string = '') { this.id = id; }\n}\n\n@database\nclass AppDb {\n @collection static likes: Counter<PostId>;\n @collection static views: Counter<PostId>;\n}\n```\n\n## The operations\n\n`K` is the key type. The value is always a 64-bit signed integer (`i64`).\n\n| Operation | Signature | Returns | Use it to |\n| --- | --- | --- | --- |\n| `get` | `get(key: K): i64` | the current total (`0` if nothing has been added) | read the count |\n| `add` | `add(key: K, delta: i64): void` | nothing | change the count by `delta` (may be negative) |\n\n`get` is a read (works in any function). `add` is a write, so it needs an **Action** (a `@post` route or an `@action`); see [Setup](./setup.md#how-access-is-gated-query-action-and-friends).\n\n### `get`\n\n`get` returns the current total. A counter that has never been touched reads as `0`, not `null`: there is no \"absent\" state to handle.\n\n```ts\nconst likeCount: i64 = AppDb.likes.get(new PostId('p_42'));\n```\n\n### `add`\n\n`add` changes the total by a delta. The delta can be positive to increase or negative to decrease:\n\n```ts\nAppDb.likes.add(new PostId('p_42'), 1); // one more like\nAppDb.likes.add(new PostId('p_42'), -1); // undo a like\nAppDb.stock.add(new SkuId('sku_9'), -3); // sold three\n```\n\n`add` saturates at the `i64` limits: it will not wrap around from a huge positive to a negative if you somehow overflow. Note that `add` returns nothing, not the new total. If you need the total right after adding, call `get` (bearing in mind the consistency note below).\n\n## Why a Counter, and not a number in a Documents record?\n\nThis is the question the family exists to answer, so it is worth walking through.\n\nSuppose you stored the like count as a field in a Documents record and incremented it the obvious way: read the record, add one, write it back. Now two servers on opposite sides of the world each get a like at the same moment:\n\n```mermaid\nsequenceDiagram\n participant Tokyo as Server (Tokyo)\n participant Paris as Server (Paris)\n participant DB as Documents record\n\n Note over DB: likes = 10\n Tokyo->>DB: read likes -> 10\n Paris->>DB: read likes -> 10\n Tokyo->>DB: write likes = 11\n Paris->>DB: write likes = 11\n Note over DB: likes = 11 (one like LOST)\n```\n\nBoth read `10`, both wrote `11`, and one like vanished. This is a **lost update**, and it happens whenever two callers do read-modify-write on the same value at once. It is not a rare edge case on a busy post; it is the normal case.\n\nA Counter avoids this entirely because you never send a final value, you send a **delta**. Each server just says \"add 1.\" The database merges the two deltas into \"add 1, add 1 = add 2,\" so the total goes from 10 to 12 and nothing is lost. Deltas are **commutative** (the order they arrive in does not matter) and **additive**, so concurrent increments from anywhere in the world always combine correctly. That property is what \"conflict-free\" means: there is no conflict to resolve, because two adds are never at odds.\n\nThis is also why there is no `set` operation. A `set` would reintroduce the exact race above (two callers overwriting each other). By offering only `add`, the Counter family makes the lost-update bug impossible to write.\n\n**Use a Counter when** the number is bumped concurrently by many requests. **Use a Documents field instead when** the number is only ever changed by a single logical owner in a controlled way (for example, a value you always overwrite with a freshly computed absolute figure, not an increment), where you would use `patch` to store the whole new value.\n\n## Worked example: page views and likes\n\nHere is a small feature that counts views on every read and likes on demand, then reports both.\n\n```ts\nimport { Response, RouteContext } from 'toiljs/server/runtime';\n\n@data\nclass PostId {\n id: string = '';\n constructor(id: string = '') { this.id = id; }\n}\n\n@data\nclass Stats {\n views: i64 = 0;\n likes: i64 = 0;\n}\n\n@database\nclass AppDb {\n @collection static views: Counter<PostId>;\n @collection static likes: Counter<PostId>;\n}\n\n@rest('posts')\nclass Posts {\n // POST /posts/:id/view -> count a view, return the running totals (Action)\n @post('/:id/view')\n public view(ctx: RouteContext): Stats {\n const key = new PostId(ctx.param('id'));\n AppDb.views.add(key, 1);\n const s = new Stats();\n s.views = AppDb.views.get(key);\n s.likes = AppDb.likes.get(key);\n return s;\n }\n\n // POST /posts/:id/like -> add a like (Action)\n @post('/:id/like')\n public like(ctx: RouteContext): Stats {\n const key = new PostId(ctx.param('id'));\n AppDb.likes.add(key, 1);\n const s = new Stats();\n s.views = AppDb.views.get(key);\n s.likes = AppDb.likes.get(key);\n return s;\n }\n\n // GET /posts/:id/stats -> just read the totals (Query: read-only)\n @get('/:id/stats')\n public stats(ctx: RouteContext): Stats {\n const key = new PostId(ctx.param('id'));\n const s = new Stats();\n s.views = AppDb.views.get(key);\n s.likes = AppDb.likes.get(key);\n return s;\n }\n}\n```\n\nCounting a view lives in a `@post` because it is a write (a counter `add`), even though it feels like part of a read. The `@get` stats route only reads, so it runs as a Query.\n\n## Consistency notes\n\n- **Counters are eventually consistent.** `get` returns the total known to the copy nearest you, which may briefly trail increments applied moments ago in other regions. The copies converge quickly, so the count catches up on its own.\n- **No increment is ever lost.** That is the strong guarantee: every `add` from anywhere is eventually included in the total, because deltas merge. The total may be a little behind, but it is never wrong in the \"lost a like\" sense.\n- **Monotonic when you only add positives.** If every `add` is positive, the total only ever goes up as it converges. Mixing in negative `add`s (undoing a like, selling stock) is fine and correct; it simply means the total is not monotonic.\n- Because of the lag, do not treat the value `get` returns immediately after an `add` as a globally final figure. It is a fast, close-enough total, which is exactly right for likes and views.\n\n## Gotchas\n\n- **There is no `set`.** To move a counter to a specific number, add the difference (`add(key, target - get(key))`), but be aware that read-then-add reintroduces a race if two callers do it at once. Prefer to think in deltas.\n- **`add` does not return the new total.** Call `get` after if you need it, remembering it may lag concurrent remote adds.\n- **A never-touched counter reads `0`, not `null`.** There is no \"does this counter exist\" check; every key answers `0` until something is added.\n- **Counters hold a number, not a record.** If you need per-key fields alongside the count, keep the record in [Documents](./documents.md) and the tally in a Counter, keyed the same way (the example above effectively does this with two counters).\n\n## Related\n\n- [ToilDB overview](./README.md): the seven families and how to choose.\n- [Setup](./setup.md): declaring the collection and which function kinds may write.\n- [Documents](./documents.md): when the number is part of a record you overwrite wholesale.\n- [Events](./events.md): when you need the individual events, not just a total.\n- [Data types (`@data`)](../backend/data.md): the counter's key type.\n",
29
+ "database/documents.md": "# Documents\n\nThe **Documents** family is ToilDB's general-purpose record store: you keep a value under a key and look it up, update it, or delete it by that key. It is the family you will use most, and the one to reach for whenever the other six do not obviously fit.\n\n## What and why\n\nA **Documents collection** maps a `@data` key to a `@data` value: one value per key, which you can read, replace, and remove. Think users by user id, posts by post id, orders by order number. If you are storing \"a thing with fields that I look up and update by its id,\" this is the family.\n\nDeclare one by typing a `@collection` as `Documents<Key, Value>`:\n\n```ts\n@data\nclass UserId {\n id: string = '';\n constructor(id: string = '') { this.id = id; }\n}\n\n@data\nclass User {\n id: string = '';\n name: string = '';\n email: string = '';\n score: u64 = 0;\n}\n\n@database\nclass AppDb {\n @collection static users: Documents<UserId, User>;\n}\n```\n\n## The operations\n\nHere is every operation, its shape, and what it gives back. `K` is your key type, `V` your value type.\n\n| Operation | Signature | Returns | Use it to |\n| --- | --- | --- | --- |\n| `get` | `get(key: K): V \\| null` | the value, or `null` if absent | read one record |\n| `require` | `require(key: K): V` | the value; **traps** if absent | read a record you are sure exists |\n| `getMany` | `getMany(keys: K[]): Array<V \\| null>` | one entry per key, in order, each value or `null` | read several records in one call |\n| `exists` | `exists(key: K): bool` | `true` if the record is present | check presence without reading the value |\n| `create` | `create(key: K, value: V): bool` | `true` if inserted, `false` if the key was already taken | add a **new** record without overwriting |\n| `patch` | `patch(key: K, value: V): V` | the newly stored value; **traps** if the record is absent | replace an **existing** record's value |\n| `enqueue` | `enqueue(key: K, value: V): bool` | `true` if applied, `false` if a concurrent write won first or the record is absent | a version-checked (compare-and-swap) overwrite of an existing record |\n| `delete` | `delete(key: K): void` | nothing (idempotent) | remove a record |\n| `getDelete` | `getDelete(key: K): V \\| null` | the value that was there, or `null`; removes it atomically | consume a record exactly once |\n\nWhich kind of function may call which operation is covered in [Setup](./setup.md#how-access-is-gated-query-action-and-friends). In short: reads (`get`, `getMany`, `exists`) work anywhere; writes (`create`, `patch`, `enqueue`, `delete`, `getDelete`) need an **Action** (a `@post` route or an `@action`).\n\n### Reading: `get`, `require`, `exists`, `getMany`\n\n`get` is the everyday read. It returns the value or `null`, so you handle \"not found\" explicitly:\n\n```ts\nconst user = AppDb.users.get(new UserId('u_123'));\nif (user == null) {\n return Response.notFound();\n}\n// user is a fully typed User here\n```\n\n`require` is `get` for the case where absence is a bug, not a normal outcome: it returns the value directly and traps (aborts the request) if the record is missing. Use it only when you have already guaranteed the record exists.\n\n`exists` answers \"is there a record here?\" without paying to decode the value. It is handy as a cheap precondition:\n\n```ts\nif (AppDb.users.exists(new UserId(name))) {\n // username already registered, do not overwrite\n}\n```\n\n`getMany` reads several keys in a single operation. You hand it an array of keys; you get back an array the same length and in the same order, each entry either the value or `null` for a key that was absent. Reach for it instead of a loop of `get` calls when you already know the handful of keys you need.\n\n```ts\nconst ids = [new UserId('a'), new UserId('b'), new UserId('c')];\nconst found: Array<User | null> = AppDb.users.getMany(ids);\n// found[0] lines up with ids[0], and so on; each is a User or null\n```\n\n`getMany` is a **bounded batch of point reads**, not a scan: the number of keys you may pass is capped by the request budget, and it never walks the whole collection. There is no \"get all records\" operation on a request path, by design (an unbounded scan could fan out across a huge collection). If you need \"the latest N of something,\" model it as [Events](./events.md) or precompute a [View](./views.md).\n\n### Writing: `create` vs `patch` vs `enqueue`\n\nThese three all put a value under a key, but they differ in one important way each. Choosing correctly is the heart of using this family.\n\n```mermaid\nflowchart TD\n START[\"I want to write a record\"] --> Q1{\"Is this a brand-new<br/>record that must not<br/>clobber an existing one?\"}\n Q1 -->|Yes| CREATE[\"create<br/>(insert only; false if the key exists)\"]\n Q1 -->|\"No, it already exists\"| Q2{\"Must the write fail if another<br/>request changed the record<br/>since I read it?\"}\n Q2 -->|\"Yes, guard against a lost update\"| ENQUEUE[\"enqueue<br/>(version-checked CAS; false if<br/>someone wrote first, then retry)\"]\n Q2 -->|\"No, last write wins\"| PATCH[\"patch<br/>(unconditional overwrite;<br/>returns the new value)\"]\n```\n\n**`create` inserts a new record.** It only writes if the key is free. If the key already has a record, `create` does nothing and returns `false`. This is your tool for \"sign up a new user\" or \"claim this order id,\" where accidentally overwriting an existing record would be a bug. Because every key is serialized at its home (see [eventual consistency](./README.md#eventual-consistency-in-plain-words)), `create` is race-safe: if two requests create the same key at the same instant, exactly one gets `true` and the other gets `false`.\n\n```ts\nconst ok = AppDb.users.create(new UserId(input.id), input);\nif (!ok) {\n return Response.text('that id is taken', 409);\n}\n```\n\n**`patch` overwrites an existing record** and returns the value now stored. The record **must already exist**: `patch` on a missing key traps (aborts the request), so create it first. Despite the name, `patch` replaces the whole value; there is no field-level partial update, so read the current value, change the fields you want, and patch the whole thing back:\n\n```ts\nconst current = AppDb.users.get(new UserId('u_123'));\nif (current == null) return Response.notFound();\ncurrent.score = current.score + 10;\nconst saved: User = AppDb.users.patch(new UserId('u_123'), current);\n// saved is what is now stored\n```\n\n**`enqueue` is a version-checked overwrite (a compare-and-swap).** Like `patch`, it replaces the whole value of an **existing** record, but it does so *only if the record has not changed since you read it*. A **compare-and-swap** (CAS) is exactly that: \"write my new value, but only if the current value is still the one I saw.\" It returns a `bool`: `true` means your write was applied; `false` means either a concurrent write changed the record first (someone else beat you to it) or the record is absent. A `false` is **not an error**; it is the signal to **re-read and try again**. This approach is called **optimistic concurrency**: rather than locking the record, you assume nobody else will touch it, and you simply re-run the update on the rare occasion someone did.\n\nReach for `enqueue` when several requests may update the *same* record at once and you must not silently lose any of their changes. A plain `patch` cannot promise that: two overlapping patches clobber each other (the last writer wins and the earlier update just vanishes). The intended pattern for `enqueue` is always a read-modify-CAS **retry loop**:\n\n```ts\nconst key = new UserId('u_123');\nfor (let attempt = 0; attempt < 5; attempt++) {\n const current = AppDb.users.get(key); // 1. read the current value\n if (current == null) return Response.notFound();\n current.score = current.score + 10; // 2. modify your copy\n if (AppDb.users.enqueue(key, current)) { // 3. try to commit it\n return Response.text('ok'); // true: applied, we are done\n }\n // false: someone wrote between our get and our enqueue.\n // Loop: re-read the now-newer value and reapply the change on top of it.\n}\nreturn Response.text('too much contention, try again later', 409);\n```\n\nBecause every retry re-reads the latest value, the two updates **compose** (both `+10`s land) instead of one silently overwriting the other. If you do not need this guard (only one writer touches the key, or last-write-wins is genuinely fine), a plain `patch` is simpler and also hands you the stored value back directly.\n\n> There is no single \"create or overwrite\" (upsert) call. To get that behavior, try `create` first and fall back to `patch` if the key was taken:\n>\n> ```ts\n> const key = new UserId(input.id);\n> if (!AppDb.users.create(key, input)) {\n> AppDb.users.patch(key, input);\n> }\n> ```\n\n### Removing: `delete` and `getDelete`\n\n`delete` removes a record. It is **idempotent**: deleting a key that is already gone is not an error, it just does nothing. So you can call it without first checking that the record exists.\n\n```ts\nAppDb.users.delete(new UserId('u_123'));\n```\n\n`getDelete` is the atomic **fetch-and-remove**: in one indivisible step it reads the current value and deletes it, returning what it removed (or `null` if there was nothing). \"Atomic\" here means no other request can slip in between the read and the delete, so exactly one caller can ever receive a given value. That makes it the right tool for **consume-once** data: one-time login challenges, single-use invite codes, password-reset tokens. The PQ-auth demo uses it to consume a login challenge exactly once, so a challenge cannot be replayed:\n\n```ts\nconst challenge = AppDb.challenges.getDelete(new ChallengeId(cid));\nif (challenge == null) return fail(); // unknown, already used, or expired\n// ...verify against challenge...\n```\n\nIf two requests race to `getDelete` the same key, only one gets the value; the other gets `null`. That is the guarantee a plain `get` then `delete` cannot give you, because two racers could both `get` the value before either `delete`s it.\n\n## A full worked example: a small CRUD entity\n\nPutting it together, here is a complete `notes` resource: create, read, update, and delete, backed by a Documents collection.\n\n```ts\nimport { Response, RouteContext } from 'toiljs/server/runtime';\n\n// ---- key + value ----\n@data\nclass NoteId {\n id: string = '';\n constructor(id: string = '') { this.id = id; }\n}\n\n@data\nclass Note {\n id: string = '';\n title: string = '';\n body: string = '';\n updatedAt: u64 = 0;\n}\n\n// ---- database ----\n@database\nclass NotesDb {\n @collection static notes: Documents<NoteId, Note>;\n}\n\n// ---- routes ----\n@rest('notes')\nclass Notes {\n // GET /notes/:id -> read one (Query: read-only)\n @get('/:id')\n public read(ctx: RouteContext): Response {\n const note = NotesDb.notes.get(new NoteId(ctx.param('id')));\n if (note == null) return Response.notFound();\n return Response.json(note.toJSON().toString());\n }\n\n // POST /notes -> create a new one, refusing a duplicate id (Action: may write)\n @post('/')\n public create(input: Note): Response {\n input.updatedAt = <u64>(Date.now() / 1000);\n if (!NotesDb.notes.create(new NoteId(input.id), input)) {\n return Response.text('id already exists', 409);\n }\n return Response.json(input.toJSON().toString());\n }\n\n // POST /notes/:id -> overwrite an existing note (Action)\n @post('/:id')\n public update(input: Note, ctx: RouteContext): Response {\n const key = new NoteId(ctx.param('id'));\n if (!NotesDb.notes.exists(key)) return Response.notFound();\n input.id = ctx.param('id');\n input.updatedAt = <u64>(Date.now() / 1000);\n const saved = NotesDb.notes.patch(key, input);\n return Response.json(saved.toJSON().toString());\n }\n\n // POST /notes/:id/delete -> remove one (Action)\n @post('/:id/delete')\n public remove(ctx: RouteContext): Response {\n NotesDb.notes.delete(new NoteId(ctx.param('id')));\n return Response.text('deleted');\n }\n}\n```\n\nThat is a complete persistent CRUD entity. Run it under `toiljs dev` and the notes survive across requests; deploy it and the same code stores them worldwide on the edge.\n\n## Consistency notes\n\nDocuments follows ToilDB's general model (see [the overview](./README.md#eventual-consistency-in-plain-words)):\n\n- **Writes to one key are serialized at that key's home**, so `create` is race-safe and `patch`/`enqueue`/`getDelete` never corrupt a record under concurrency.\n- **Reads are eventually consistent across regions.** Right after a write, a read from a far-away region may briefly still see the old value (or, for a just-created record, not see it yet). The copies converge within moments.\n- Because `patch` replaces the whole value, two updates to *different* fields of the same record can clobber each other if they overlap (read-modify-write races). `enqueue`'s version check is exactly the guard against that: use the read-modify-CAS retry loop shown above so a lost update turns into a retry instead of silent data loss. If you find yourself contending on one hot record a lot, a counter or a set is often a better fit than a Documents value. See [Counters](./counters.md) and [Membership](./membership.md).\n\n## Gotchas\n\n- **`patch` requires an existing record.** Calling it on a missing key traps the request. Use `create` for new records, or the `create`-then-`patch` upsert pattern above.\n- **`patch` replaces the whole value.** There is no field-level merge; read, modify, and write back the full value.\n- **`enqueue` returning `false` is not a failure.** It means a concurrent write beat you to the record (or the record is absent), so re-read and retry in a loop; never ignore the return value or treat `false` as a hard error. `enqueue` also does not hand back the stored value; use `patch` when you want the value returned and last-write-wins is acceptable.\n- **No \"get all.\"** There is no scan on the request path. Use `getMany` for known keys, and [Events](./events.md) or a [View](./views.md) for \"the latest N.\"\n- **`getDelete`, not `get` + `delete`, for consume-once.** Only `getDelete` guarantees exactly one caller receives the value.\n\n## Related\n\n- [ToilDB overview](./README.md): the seven families and how to choose.\n- [Setup](./setup.md): declaring the collection and which function kinds may write.\n- [Data types (`@data`)](../backend/data.md): keys and values.\n- [Counters](./counters.md): when you are really just counting.\n- [Events](./events.md) and [Views](./views.md): for \"the latest N\" and precomputed reads.\n",
30
+ "database/events.md": "# Events (append-only logs)\n\nAn `Events` collection is an **append-only log**: a growing list of immutable\nfacts under a key. You add events to the end and read the newest ones back. You\nnever edit or delete an event once it is written.\n\n## What and why\n\nAn \"append-only log\" means exactly two things happen to it:\n\n1. You **append** a new event (it goes on the end).\n2. You **read** events back (newest first).\n\nThat is it. There is no \"update event 5\" and no \"delete event 3\". Each event is a\npermanent record of something that happened: \"Ada signed the guestbook\", \"order\n1234 was refunded\", \"user logged in from a new device\".\n\nUse an `Events` log when you care about the **history**, not just the latest\nstate:\n\n- **Activity feeds:** \"what happened in this room, newest first\".\n- **Audit trails:** a tamper-resistant record of every important action.\n- **Event sourcing:** treat the log as the source of truth and compute other\n values (totals, summaries, leaderboards) from it.\n\nIf you only ever need the *current* value of something (a user's profile, a\nshopping cart), use [Documents](./documents.md) instead. If you only need a\nrunning total (likes, page views), use a [Counter](./counters.md). Events are\nfor keeping the full list of what happened.\n\n```mermaid\nflowchart LR\n A[\"append(user, event)\"] --> L[(\"Event log for user<br/>(oldest ... newest)\")]\n B[\"append(user, event)\"] --> L\n L --> R[\"latest(user, 20)<br/>returns newest 20\"]\n```\n\n## The type\n\nAn `Events` collection has two type parameters: the **key** (which log) and the\n**event** (what each entry stores).\n\n```ts\nEvents<K, V>\n```\n\n- `K` is the key type: it picks *which* log you are appending to. One key, one\n log. For a per-user activity feed, the key is the user id.\n- `V` is the event type: the shape of a single entry. Both `K` and `V` are\n [`@data`](../concepts/types.md) classes so they can be stored as bytes.\n\nYou declare it as a `@collection` field inside a `@database` class:\n\n```ts\n@data\nclass UserKey {\n id: string = '';\n constructor(id: string = '') { this.id = id; }\n}\n\n@data\nclass Activity {\n action: string = ''; // 'login', 'post.create', 'comment', ...\n detail: string = '';\n at: u64 = 0; // unix seconds\n}\n\n@database\nclass FeedDb {\n @collection static activity: Events<UserKey, Activity>;\n}\n```\n\nThe field name (`activity`) names the collection; the class name (`FeedDb`) names\nthe database. You reach the log through `FeedDb.activity`.\n\n## Operations\n\nThere are three operations, exactly matching the [toilscript](../concepts/decorators.md)\nAPI. Their exact signatures:\n\n| Operation | Signature | What it does |\n| --- | --- | --- |\n| `append` | `append(key: K, event: V): void` | Add one event to the end of the log. |\n| `appendOnce` | `appendOnce(key: K, eventId: string, event: V): bool` | Add one event, but only if that `eventId` was never seen before. |\n| `latest` | `latest(key: K, limit: i32): V[]` | Read up to `limit` events, newest first. |\n| `since` | `since(key: K, limit: i32): V[]` | Read the NEXT batch of events past a `@derive`'s checkpoint, oldest first. For folding a growing log incrementally. |\n\n### `append`\n\nThe everyday operation. It adds one event and returns nothing.\n\n```ts\nFeedDb.activity.append(\n new UserKey('ada'),\n new Activity('login', 'from Paris', now),\n);\n```\n\n`append` is a **write**, so you may call it from an action handler (`@post`,\n`@put`, `@patch`, `@del`) but not from a read-only `@get`. See\n[decorators](../concepts/decorators.md) for what each handler kind may do.\n\n### `appendOnce` (idempotent append)\n\n`appendOnce` adds an event **at most once** for a given id. \"Idempotent\" means\nrunning it more than once has the same effect as running it once: the extra calls\nchange nothing.\n\nWhy you want this: networks retry. A client (or another service) may send the\nsame request twice because the first reply got lost. With plain `append`, that\ndouble-delivery writes the event twice. With `appendOnce`, you pass a stable\n`eventId` (a string you choose that is unique for that logical event), and the\nlog records it only once.\n\n```ts\n// `orderId` uniquely identifies this order event. If the request is retried,\n// the second call is a no-op and the log still has exactly one entry.\nconst first = FeedDb.activity.appendOnce(\n new UserKey('ada'),\n `order-refunded:${orderId}`, // the idempotency id\n new Activity('order.refund', orderId, now),\n);\n// first === true -> this call appended the event\n// first === false -> a previous call already appended it; nothing happened now\n```\n\nThe return value tells you which happened:\n\n- `true`: this call appended the event (it was the first with that id).\n- `false`: a call with the same id already appended it, so this was a no-op.\n\nPick an `eventId` that is truly unique per event: an order id, a payment id, a\nmessage uuid. Do not reuse one id for two different events, or the second will be\nsilently dropped as a \"duplicate\".\n\n### `latest`\n\nReads the newest events back, up to a limit, newest first.\n\n```ts\nconst recent = FeedDb.activity.latest(new UserKey('ada'), 20);\n// recent[0] is the most recent event, recent[1] the next, ...\n```\n\n`latest` is a **scan**: it can walk many rows. Scans are barred on the request\npath, so you **cannot** call `latest` from a `@get` or a `@post`. This is a\ndeliberate guardrail: a log can grow without bound, and a per-request scan would\nget slower and more expensive as the log grows.\n\nInstead you scan the log **off** the request path, in a\n[`@derive`](../background/derive.md), and publish a small precomputed\n[View](./views.md) that your routes read cheaply. The next section shows the full\npattern.\n\n### `since` (incremental read for a `@derive`)\n\n`latest` rescans the tail every time. For a log that grows without bound, that gets slower and slower. `since`\nreads only the events you have **not folded yet**: the next batch past a saved cursor, oldest first, up to a\nlimit. It is how a [`@derive`](../background/derive.md) folds a huge log incrementally instead of rescanning\nit from the start on every change.\n\nYou do not pass or manage the cursor. The host owns it: it seeds `since` from a durable checkpoint, advances\nit as it hands you events, and saves it after your fold's view publish lands. So you just loop until the\nbatch is empty:\n\n```ts\n@derive\nrollup(): void {\n const key = new StatsKey('global');\n const view = StatsDb.summary.get(key) ?? new Summary(); // the running view\n let batch = StatsDb.events.since(key, 500);\n while (batch.length > 0) { // drain 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\nTwo rules:\n\n- **`@derive` or `@job` only.** Like `latest`, `since` is a scan, so it is barred on the request path (`@get`\n / `@post`). Calling it there is rejected.\n- **Your fold must be idempotent per event.** A rare crash-recovery case (a \"healed\" event that arrives at an\n older position) makes the host re-read the log from the start that one run, so applying an event twice must\n not change the result. Use set-style updates (`view.byId[e.id] = e.value`), not blind accumulation\n (`view.count += 1`). If your fold cannot be idempotent, use `latest` and recompute instead.\n\n`since` is the efficient path; `latest` (recompute) is the simple path. See [`@derive`](../background/derive.md)\nfor the full picture.\n\n## Worked example: an activity feed\n\nHere is the whole loop: an action appends an event, a `@derive` folds the log\ninto a view, and a `@get` serves the view.\n\n```ts\nimport { Activity } from '../models/Activity';\nimport { UserKey } from '../models/UserKey';\nimport { FeedView } from '../models/FeedView';\nimport { NewActivity } from '../models/NewActivity';\n\n@database\nclass FeedDb {\n // The source of truth: every activity, appended forever.\n @collection static activity: Events<UserKey, Activity>;\n // The precomputed snapshot the GET serves: the newest 20 activities.\n @collection static feed: View<UserKey, FeedView>;\n\n // A @derive runs OFF the request path, so it is allowed to scan the log\n // (`latest`) and publish the view. The runtime runs it right after an append,\n // so the view reflects the new event on the next read.\n @derive\n rebuild(): void {\n const key = new UserKey('ada'); // (real code would loop per active user)\n const view = new FeedView();\n view.items = FeedDb.activity.latest(key, 20);\n FeedDb.feed.publish(key, view);\n }\n}\n\n@rest('feed')\nclass Feed {\n // GET reads the precomputed view: a single keyed read, NOT a scan.\n @get('/')\n public list(): FeedView {\n const view = FeedDb.feed.get(new UserKey('ada'));\n return view == null ? new FeedView() : view;\n }\n\n // POST appends one activity. The @derive republishes `feed` right after.\n @post('/')\n public record(input: NewActivity): FeedView {\n const at = <u64>(Date.now() / 1000);\n FeedDb.activity.append(new UserKey('ada'), new Activity(input.action, input.detail, at));\n // We do NOT scan here (that would be barred). We just acknowledge; the GET\n // serves the updated list from the view the derive rebuilds.\n return new FeedView();\n }\n}\n```\n\nThe two `@data` models:\n\n```ts\n@data\nexport class Activity {\n action: string = '';\n detail: string = '';\n at: u64 = 0;\n}\n\n@data\nexport class FeedView {\n items: Activity[] = [];\n}\n```\n\nThe key idea: the **log** is the durable history, and the **view** is a small,\nread-cheap snapshot derived from it. Reads hit the view; the log grows quietly in\nthe background.\n\n## Ordering and consistency\n\nToilDB is a **worldwide** database: your data lives in many regions at once. A\nfew honest details about what that means for events:\n\n- **Ordering within a log is stable.** Each key (each log) has one \"home\" location\n where appends are serialized and stamped with an increasing sequence number. So\n `latest` always returns a consistent newest-first order for that key.\n- **`latest` is newest first.** Index `0` is the most recent event.\n- **Reads can lag slightly across regions.** An append is applied at the log's\n home first, then copied out to other regions in the background (this is called\n *asynchronous replication*, and the result is *eventual consistency*: given a\n little time, every region converges to the same log). A read served from a far\n region may briefly miss the very newest event. A `@derive` runs right after the\n append that triggered it, so the view it publishes reflects that write.\n- **`appendOnce` dedups by your id.** The at-most-once guarantee is anchored to\n the `eventId` you pass, so retries are safe even across a flaky network.\n- **A log grows without bound.** Appending never shrinks it. Keep events small,\n and read them through a bounded `latest(key, N)` in a derive rather than trying\n to hold the whole log in memory.\n\n## Gotchas\n\n- **You cannot read the log from a route.** `latest` is a scan and is only legal\n in a `@derive` or a `@job`. If you try to call it from a `@get`/`@post`, the\n compiler rejects it. Read a [View](./views.md) from your route instead.\n- **Events are immutable.** There is no \"edit\" or \"delete an event\". If a fact\n changes, append a new event that records the change (for example, an\n `order.refund` event, not an edit to the original `order.create`).\n- **Choose `eventId` carefully for `appendOnce`.** It must be unique per logical\n event. Reusing an id drops the new event as a duplicate.\n- **Do not treat a counter as a log.** A [Counter](./counters.md) gives you a\n running total but forgets the individual events. If you need the list, use\n `Events`.\n\n## Related\n\n- [Documents](./documents.md): mutable, one value per key (edit in place).\n- [Counters](./counters.md): a single running total per key.\n- [Views](./views.md): the precomputed snapshot your routes read.\n- [`@derive`](../background/derive.md): folds an event log into a View, off the\n request path.\n- [Data types (`@data`)](../concepts/types.md): how keys and events are stored.\n- [Decorators](../concepts/decorators.md): which handler kinds may append vs scan.\n",
31
+ "database/membership.md": "# Membership (sets and relationships)\n\nA `Membership` collection stores **sets**: unordered groups of members under a\nkey. You add and remove members, ask whether a member is in the set, and list a\nset's members. It is the natural fit for many-to-many relationships like group\nmembers, followers, and tags.\n\n## What and why\n\nA \"set\" is a collection with no duplicates and no order: a member is either in it\nor not. `Membership` gives you one set per key. The key names the set; each member\nis one item in it.\n\nThis maps directly onto **many-to-many relationships**, where each thing on one\nside can relate to many things on the other:\n\n- **Group members:** the key is a group id, the members are user ids.\n- **Followers:** the key is a user id, the members are the ids of their followers.\n- **Tags:** the key is an article id, the members are tag names.\n- **Access control:** the key is a resource id, the members are the users allowed\n in.\n\nYou could try to store these as an array field inside a [Document](./documents.md)\n(for example `group.memberIds: string[]`). That works for **small, rarely-changed**\nsets, but it has real limits:\n\n| | Array in a Document | `Membership` set |\n| --- | --- | --- |\n| Add/remove one member | Read the whole array, edit it, write it all back | One direct `add`/`remove`, no read-modify-write |\n| Concurrent edits | Two writers can clobber each other's array | Each `add`/`remove` is its own operation |\n| Big sets | The whole array is loaded on every read/write | Members are stored separately; you read a bounded page |\n| \"Is X a member?\" | Load the array and search it | One direct `contains` check |\n\nRule of thumb: a **handful** of stable items (a user's two or three roles) is fine\nas an array in a Document. A set that **grows** or is **edited concurrently**\n(followers, group members, tags across many articles) should be a `Membership`.\n\n```mermaid\nflowchart LR\n K[\"Group key: 'eng'\"] --> S[(\"Membership set\")]\n S --> M1[\"ada\"]\n S --> M2[\"grace\"]\n S --> M3[\"linus\"]\n```\n\n## The type\n\n```ts\nMembership<K, M>\n```\n\n- `K` is the key type: it picks *which* set. For group membership, the key is the\n group id.\n- `M` is the member type: the shape of one member. For user membership, that is a\n user id. Both are [`@data`](../concepts/types.md) classes.\n\nDeclare it as a `@collection` field on a `@database` class:\n\n```ts\n@data\nclass GroupKey {\n group: string = '';\n constructor(group: string = '') { this.group = group; }\n}\n\n@data\nclass Member {\n userId: string = '';\n constructor(userId: string = '') { this.userId = userId; }\n}\n\n@database\nclass GroupsDb {\n @collection static members: Membership<GroupKey, Member>;\n}\n```\n\n## Operations\n\nFour operations, matching the toilscript API exactly. Exact signatures:\n\n| Operation | Signature | What it does |\n| --- | --- | --- |\n| `contains` | `contains(key: K, member: M): bool` | Is `member` in the set? |\n| `add` | `add(key: K, member: M): void` | Put `member` in the set (idempotent). |\n| `remove` | `remove(key: K, member: M): void` | Take `member` out of the set (idempotent). |\n| `list` | `list(key: K, limit: i32): M[]` | Up to `limit` members of the set. |\n\n### `contains`\n\nA direct membership check. It is a keyed read, so it is allowed from any handler,\nincluding a read-only `@get`.\n\n```ts\nif (GroupsDb.members.contains(new GroupKey('eng'), new Member('ada'))) {\n // ada is in the 'eng' group\n}\n```\n\n### `add` and `remove`\n\nBoth are **writes**, so you call them from an action handler (`@post`, `@put`,\n`@patch`, `@del`), not from a `@get`. Both are **idempotent**: \"idempotent\"\nmeans doing it again has no extra effect.\n\n- `add` on a member already in the set: no change, no error.\n- `remove` on a member not in the set: no change, no error.\n\n```ts\nGroupsDb.members.add(new GroupKey('eng'), new Member('ada')); // ada joins\nGroupsDb.members.add(new GroupKey('eng'), new Member('ada')); // no-op, still one 'ada'\nGroupsDb.members.remove(new GroupKey('eng'), new Member('linus')); // linus leaves (or no-op)\n```\n\nBecause they are idempotent, you do not need to check `contains` before calling\nthem. Just `add` to join and `remove` to leave.\n\n### `list`\n\nReturns up to `limit` members of a set.\n\n```ts\nconst roster = GroupsDb.members.list(new GroupKey('eng'), 100);\n```\n\n`list` is a **scan** (it can walk many rows), so it is barred on the request\npath: you **cannot** call it from a `@get` or a `@post`. Like reading a whole\nevent log, listing a whole set belongs off the request path, in a\n[`@derive`](../background/derive.md) or a `@job`, which publishes a\n[View](./views.md) your routes read. The [worked example](#worked-example-group-membership)\nbelow shows this.\n\n`list` returns up to `limit` members. A set can be larger than one page, so treat\nthe result as \"a bounded page of members\", not necessarily \"every member\".\n\n## Worked example: group membership\n\nA user joins or leaves a group from a route; a derive lists the roster into a\nview; a route reads the view. Membership checks happen directly in the route.\n\n```ts\nimport { GroupKey } from '../models/GroupKey';\nimport { Member } from '../models/Member';\nimport { Roster } from '../models/Roster';\nimport { JoinRequest } from '../models/JoinRequest';\n\n@database\nclass GroupsDb {\n // The set: who is in each group.\n @collection static members: Membership<GroupKey, Member>;\n // A precomputed roster for the GET (listing is a scan, barred on routes).\n @collection static roster: View<GroupKey, Roster>;\n\n @derive\n rebuild(): void {\n const key = new GroupKey('eng');\n const r = new Roster();\n r.users = GroupsDb.members.list(key, 500); // scan, allowed in a derive\n GroupsDb.roster.publish(key, r);\n }\n}\n\n@rest('groups')\nclass Groups {\n // GET the roster from the view (a keyed read, not a scan).\n @get('/eng')\n public list(): Roster {\n const r = GroupsDb.roster.get(new GroupKey('eng'));\n return r == null ? new Roster() : r;\n }\n\n // Direct membership check: `contains` is a keyed read, legal in a GET.\n @get('/eng/is-member')\n public isMember(userId: string): bool {\n return GroupsDb.members.contains(new GroupKey('eng'), new Member(userId));\n }\n\n // Join: `add` is idempotent, so joining twice is harmless.\n @post('/eng/join')\n public join(input: JoinRequest): bool {\n GroupsDb.members.add(new GroupKey('eng'), new Member(input.userId));\n return true; // the @derive rebuilds the roster view right after\n }\n\n // Leave: `remove` is idempotent, so leaving when not a member is harmless.\n @post('/eng/leave')\n public leave(input: JoinRequest): bool {\n GroupsDb.members.remove(new GroupKey('eng'), new Member(input.userId));\n return true;\n }\n}\n```\n\nThe models:\n\n```ts\n@data\nexport class Member {\n userId: string = '';\n}\n\n@data\nexport class Roster {\n users: Member[] = [];\n}\n```\n\nThe split is the point: `contains`/`add`/`remove` act on a single member and are\nlegal on the request path; `list` scans the whole set and lives in a derive that\nfeeds a view.\n\n## Consistency\n\n- **Add and remove serialize at the set's home.** Each set key has one home\n location where its writes are applied in order, so an `add` followed by a\n `remove` of the same member lands in that order and the set ends up correct.\n- **Reads can lag slightly across regions.** ToilDB is worldwide. A change made\n at a set's home is copied to other regions in the background (asynchronous\n replication, giving eventual consistency: every region converges after a short\n delay). A `contains` or a `list` served from a far region may briefly miss a\n just-made change.\n- **`add`/`remove` are idempotent**, so retries are safe: re-adding an existing\n member or re-removing an absent one does nothing.\n- **A set can grow without bound.** Adding never shrinks it. Read it through a\n bounded `list(key, N)` in a derive, not all at once.\n\n## Gotchas\n\n- **You cannot `list` from a route.** `list` is a scan, legal only in a `@derive`\n or `@job`. To show a roster on a page, publish it to a [View](./views.md) from a\n derive and `get` that view in the route.\n- **`list` returns a bounded page.** A large set may have more members than your\n `limit`. Do not assume the returned array is the entire set.\n- **Sets are unordered.** Do not rely on the order `list` returns members in.\n- **Use `Membership` for growth or concurrency.** A small, stable set is fine as\n an array in a Document. Reach for `Membership` when the set grows, is edited\n concurrently, or you frequently ask \"is X a member?\".\n\n## Related\n\n- [Documents](./documents.md): when a small array field is enough (and when it is\n not).\n- [Views](./views.md): where a roster/list belongs so routes can read it cheaply.\n- [`@derive`](../background/derive.md): runs `list` off the request path.\n- [Data types (`@data`)](../concepts/types.md): how set keys and members are stored.\n- [Decorators](../concepts/decorators.md): which handler kinds may add/remove vs scan.\n",
32
+ "database/README.md": "# ToilDB\n\nToilDB is the database built into toiljs. It is **global**, it needs **no setup or connection string**, and your backend talks to it with plain typed method calls.\n\nIf you have used Postgres, MySQL, or MongoDB before, ToilDB will feel a little different, and this page explains why. If you have never used a database, that is fine too: read on and every term is defined as it appears.\n\n## What \"a globally distributed edge database\" means\n\nLet us take that phrase one word at a time.\n\n- A **database** is a place your program stores data so it is still there on the next request. Your handler runs, finishes, and forgets everything in memory (a fresh WebAssembly instance runs each request), so anything you want to keep, a user account, a like count, a guestbook entry, has to go into a database.\n- The **edge** is a network of servers spread around the world, each one physically close to some of your users. Your compiled backend runs on the edge server nearest whoever is calling it. (For the full picture of where code runs, see [Tiers](../concepts/tiers.md).)\n- **Globally distributed** means the database is not one machine in one city. Its data lives on many machines in many regions at once. A user in Tokyo and a user in Paris each read from a copy near them, so reads are fast everywhere instead of fast in one place and slow everywhere else.\n\nPut together: ToilDB is storage that lives out on that same worldwide edge, next to your code, so a database read does not have to fly across an ocean and back.\n\n```mermaid\nflowchart LR\n subgraph edge[\"Dacely edge (worldwide)\"]\n direction LR\n H1[\"Your backend<br/>(Tokyo)\"] --> D1[(\"ToilDB<br/>copy near Tokyo\")]\n H2[\"Your backend<br/>(Paris)\"] --> D2[(\"ToilDB<br/>copy near Paris\")]\n end\n D1 <-->|\"replication<br/>(keeps copies in sync)\"| D2\n UA[\"User in Tokyo\"] --> H1\n UB[\"User in Paris\"] --> H2\n```\n\nYou never pick a region, open a connection, or run a migration script. You declare what you want to store (see [Setup](./setup.md)) and ToilDB is there.\n\n## Why seven families instead of one big \"table\"\n\nMost databases give you one general-purpose tool: a table (or a collection) that you read and write however you like. That is flexible, but on a system spread across the whole planet, the flexible tool is also the slow and error-prone tool. The classic example is a counter. If two servers on opposite sides of the world both try to do \"read the number, add one, write it back\" at the same moment, one of the two increments is silently lost, because each read the same starting value.\n\nToilDB solves this by giving you **seven specialized collection types**, called **families**. Each family is tuned for one job and exposes only the operations that are safe and fast for that job. A counter family, for instance, has no \"set to this value\" operation at all: you can only `add` a delta, and the database merges concurrent deltas from around the world without losing any. You cannot misuse it, because the unsafe operation does not exist.\n\nSo instead of one generic table you reach for, you pick the family that matches what you are doing. Picking the right one is the main skill, and the guide below walks you through it.\n\nThe seven families are:\n\n| Family | It stores | Read a page |\n| --- | --- | --- |\n| **Documents** | Records you look up by id (users, posts, orders). | [Documents](./documents.md) |\n| **Unique** | A claim on a value that must be one-of-a-kind (usernames, emails, slugs). | [Unique](./unique.md) |\n| **Counter** | A running total that many callers increment at once (likes, views). | [Counters](./counters.md) |\n| **Events** | An append-only log of things that happened (feeds, audit trails). | [Events](./events.md) |\n| **Capacity** | A limited quantity you hand out without overselling (tickets, seats). | [Capacity](./capacity.md) |\n| **Membership** | Sets of \"who belongs to what\" (followers, tags, room members). | [Membership](./membership.md) |\n| **View** | A precomputed, read-optimized snapshot (home pages, leaderboards). | [Views](./views.md) |\n\n## The one idea shared by all seven: key then value\n\nEvery family, underneath, works the same way: it maps a **key** to a **value**.\n\n- A **key** is how you find your data. Think of it as the label on a drawer: a user id, a username, a room name.\n- A **value** is what is in the drawer: the user record, the owner of a username, the members of a room.\n\nIn ToilDB, both the key and the value are ordinary TypeScript classes that you tag with `@data`. The `@data` tag tells toilscript (the compiler that turns your backend into WebAssembly) how to pack that class into bytes for storage and unpack it again. You write a normal class with normal fields, give each field a default, and the compiler does the rest.\n\n```ts\n// A key: how you address one record.\n@data\nclass UserId {\n id: string = '';\n constructor(id: string = '') { this.id = id; }\n}\n\n// A value: what you store under that key.\n@data\nclass User {\n id: string = '';\n name: string = '';\n score: u64 = 0;\n}\n```\n\nYou then declare a collection that maps that key type to that value type, for example `Documents<UserId, User>`. Every family is generic over its key and value types in exactly this way, so once you understand `@data` keys and values you understand all seven. The full `@data` reference is on the [data types page](../backend/data.md); how to declare collections is on [Setup](./setup.md).\n\n## Eventual consistency, in plain words\n\nBecause ToilDB keeps copies of your data in many regions, there is one honest trade-off you need to understand: **eventual consistency**.\n\nEvery key has one **home**: a single region that officially owns that key's data. All **writes** to a key travel to its home, where they are applied one at a time, in order. That is what makes writes safe: even if a thousand servers write the same key at once, the home lines them up so nothing is lost or corrupted.\n\n**Reads**, on the other hand, are served from the copy nearest the reader, which is fast but may be a beat behind. After a write lands at the home, it takes a brief moment (usually milliseconds) to fan out to the other regions' copies. During that moment, a reader in another region might still see the previous value. The copies always catch up; they are *eventually* consistent, not *instantly* consistent everywhere.\n\nWhat this means in practice:\n\n- **It is usually invisible.** The lag is tiny, and most apps never notice.\n- **Do not assume a write is visible everywhere the instant it returns.** For example, right after you create a record, a read from a far-away region might briefly not see it yet.\n- **Some families are stronger.** Because writes to a single key are serialized at its home, operations that must never race, claiming a unique username, reserving the last ticket, are decided at the home and are safe. The Unique and Capacity families lean on this so two callers can never both win. See [Unique](./unique.md) and [Capacity](./capacity.md).\n\nEach family page spells out its own consistency behavior, so you always know what to expect.\n\n## Choosing a family: the decision guide\n\nStart from what you are trying to do, not from a data structure. This table maps a real need to the family that was built for it.\n\n| I need to... | Use | Why |\n| --- | --- | --- |\n| Store and update a thing I look up by id (a user, a post, an order). | **Documents** | The general-purpose record store: create, read, update, delete by key. |\n| Guarantee a value is used by only one owner across the whole world (username, email, slug). | **Unique** | Claims are decided at the key's home, so two people cannot claim the same name. |\n| Count something that many people bump at the same time (likes, page views, stock tally). | **Counter** | Concurrent `add`s from anywhere merge without ever losing an increment. |\n| Keep a growing list of things that happened, in order (activity feed, audit log). | **Events** | Append-only: you add events and read the newest, but never edit history. |\n| Hand out a limited quantity and never sell more than exist (tickets, seats, inventory). | **Capacity** | Reserve/confirm/cancel holds at the home prevent overselling. |\n| Track which members belong to a set (followers, tags, room members, permissions). | **Membership** | Add/remove/check membership without loading a whole record to edit a list. |\n| Serve a precomputed, ready-to-render result quickly (leaderboard, home page snapshot). | **View** | A background job computes it once; requests read it with a single fast lookup. |\n\nIf two families seem to fit, this flowchart resolves it. Read it top to bottom.\n\n```mermaid\nflowchart TD\n START[\"What are you storing?\"] --> Q1{\"Is it a running<br/>total (a number many<br/>callers increase)?\"}\n Q1 -->|Yes| COUNTER[\"Counter\"]\n Q1 -->|No| Q2{\"Must a value be<br/>one-of-a-kind across<br/>everyone (username,<br/>email, slug)?\"}\n Q2 -->|Yes| UNIQUE[\"Unique\"]\n Q2 -->|No| Q3{\"Is it a limited stock<br/>you must not oversell<br/>(tickets, seats)?\"}\n Q3 -->|Yes| CAPACITY[\"Capacity\"]\n Q3 -->|No| Q4{\"Is it an append-only<br/>log of events that<br/>happened, kept in order?\"}\n Q4 -->|Yes| EVENTS[\"Events\"]\n Q4 -->|No| Q5{\"Is it a set of members<br/>you add/remove/check<br/>(followers, tags)?\"}\n Q5 -->|Yes| MEMBERSHIP[\"Membership\"]\n Q5 -->|No| Q6{\"Is it a precomputed<br/>read-only snapshot a<br/>background job builds?\"}\n Q6 -->|Yes| VIEW[\"View\"]\n Q6 -->|No| DOCUMENTS[\"Documents<br/>(the default record store)\"]\n```\n\nA quick sanity check when you land somewhere: **Documents is the default.** If you are storing \"a thing with fields that I update by id,\" it is almost always Documents. The other six exist for the specific jobs above where Documents would be slow, unsafe under concurrency, or awkward.\n\nReal apps mix families freely. The demo guestbook, for example, uses **Events** for the log of signatures, a **Counter** for the running total, and a **View** for the ready-to-serve snapshot, all in one small feature.\n\n## Where to go next\n\n- [Setup](./setup.md): declare a database, its collections, and reach them from a handler.\n- [Documents](./documents.md): the general-purpose record store (start here).\n- [Unique](./unique.md), [Counters](./counters.md), [Events](./events.md), [Views](./views.md), [Membership](./membership.md), [Capacity](./capacity.md): the specialized families.\n\n## Related\n\n- [Setup](./setup.md): how to declare `@database`, `@collection`, and `@data` types.\n- [Data types (`@data`)](../backend/data.md): the typed keys and values every family uses.\n- [Tiers](../concepts/tiers.md): where your backend and its data run.\n- [Decorators](../concepts/decorators.md): `@query`, `@action`, `@derive`, and friends.\n",
33
+ "database/setup.md": "# Declaring a database\n\nYou set up ToilDB by **declaring** it in code: a `@database` class listing your collections, plus the `@data` key and value types those collections use. There is no schema file, no migration command, and no connection string.\n\n## What and why\n\nA **database declaration** tells toiljs three things at compile time: which collections exist, which family each one is, and what key and value types it stores. The compiler bakes that list into your server's WebAssembly, and both the dev server and the production edge read it back to set up your collections automatically. You declare; the platform provisions.\n\nReach for this on day one: before you can read or write any data, you need a `@database` with at least one `@collection`.\n\n## How: the three pieces\n\nA working database is always these three pieces together.\n\n1. **`@data` key and value types.** Plain classes, tagged `@data`, that describe what you store.\n2. **A `@database` class** whose static fields are `@collection`s, each typed by a family.\n3. **A handler** (a `@rest` route or an RPC method) that reads and writes those collections.\n\nHere is a complete, minimal example: a store of users you look up by id.\n\n```ts\n// A @data key: how you address one user.\n@data\nclass UserId {\n id: string = '';\n constructor(id: string = '') { this.id = id; }\n}\n\n// A @data value: what you store for each user.\n@data\nclass User {\n id: string = '';\n name: string = '';\n score: u64 = 0;\n}\n\n// The database declaration. Each @collection is one collection,\n// typed by its family (here, Documents) with <Key, Value>.\n@database\nclass AppDb {\n @collection static users: Documents<UserId, User>;\n}\n\n// A route that reads and writes the collection.\n@rest('users')\nclass Users {\n @get('/:id')\n public getUser(ctx: RouteContext): User {\n const user = AppDb.users.get(new UserId(ctx.param('id')));\n return user == null ? new User() : user;\n }\n\n @post('/')\n public createUser(input: User): User {\n AppDb.users.create(new UserId(input.id), input);\n return input;\n }\n}\n```\n\nThat is the whole setup. Run `toiljs dev` and `AppDb.users` works immediately.\n\n### The `@data` types\n\nBoth the key and the value are `@data` classes. `@data` is what makes a class storable: the compiler synthesizes a binary codec (pack to bytes, unpack from bytes) so ToilDB can persist it. The rules that matter here:\n\n- **Give every field a default** (`= ''`, `= 0`, and so on). The decoder builds an empty instance and fills it, so it needs defaults.\n- **A value type must be default-constructible** (creatable with `new User()` and no arguments). Keys often add a convenience constructor, as `UserId` does above, so you can write `new UserId('abc')`.\n- Fields may be numbers (`u8`..`u256`, `i8`..`i256`, `f32`, `f64`), `bool`, `string`, another `@data` class, or an array of any of these.\n\nThe full reference, including how the same type becomes a typed client type, is on the [data types page](../backend/data.md).\n\n### The `@database` class and `@collection` fields\n\n```ts\n@database\nclass AppDb {\n @collection static users: Documents<UserId, User>;\n @collection static likes: Counter<UserId>;\n}\n```\n\n- `@database` marks the class as a database. You can have more than one `@database` class; each is a separate namespace of collections.\n- Each `@collection` is one collection. Declare it as a **`static`** field with **no initializer**: you write the type, and the compiler wires up the actual handle for you.\n- The field's **type is its family**: `Documents<K, V>`, `Counter<K>`, `Events<K, V>`, `Unique<K, V>`, `Membership<K, M>`, `Capacity<K>`, or `View<K, V>`. The compiler reads the family straight from this type, so getting it right here is how you pick a family (see [Choosing a family](./README.md#choosing-a-family-the-decision-guide)).\n\nYou reach a collection through the class, statically: `AppDb.users.get(...)`, `AppDb.likes.add(...)`. There is nothing to instantiate.\n\n### Reaching collections from a handler\n\nAny backend function can read and write collections by referencing them on the database class. What that function is allowed to do depends on its **kind**, covered next.\n\n## How access is gated: `@query`, `@action`, and friends\n\nToilDB will not let every function do everything. Each backend function runs as one **function kind**, and each kind is allowed a different slice of database operations. This is a safety rail: a read-only endpoint physically cannot write, and an expensive scan cannot run on the hot request path.\n\nYou rarely write these decorators by hand, because routes get a sensible kind automatically:\n\n- A **`@get`** route (a safe, read-only HTTP method) runs as a **Query**.\n- A **`@post`** route (a mutating method) runs as an **Action**.\n- A plain RPC **`@remote`** method defaults to a **Query** (read-only) because it has no HTTP method to infer from. Tag it `@action` if it writes.\n\nYou can override the default with `@query` or `@action` on the method when the automatic choice is wrong (for example, a `@get` that genuinely needs to write, though that is unusual).\n\nWhat each kind may do:\n\n| Kind | Set by | May do | May **not** do |\n| --- | --- | --- | --- |\n| **Query** | `@get`, plain `@remote`, or `@query` | Point reads: `get`, `getMany`, `exists`, `lookup`, `contains`, counter `get`, view `get`, capacity `available`. | Any write. Any scan. |\n| **Action** | `@post` or `@action` | Everything a Query can, plus bounded writes: `create`, `patch`, `delete`, `getDelete`, `enqueue`, `append`, `appendOnce`, counter `add`, membership `add`/`remove`, unique `claim`/`release`, capacity `reserve`/`confirm`/`cancel`. | Scans. Publishing a View. |\n| **Derive / Job** | `@derive` / `@job` (background work) | Reads including **scans** (`latest`, membership `list`), plus `publish` a View. | (Run off the request path; see below.) |\n\nTwo rules trip people up, so they are worth stating plainly:\n\n- **Scans are barred from request handlers.** Reading \"the newest N events\" (`events.latest`) or \"the members of this set\" (`membership.list`) can fan out across many rows, so a `@get` or `@post` cannot call them. Do the scan in a `@derive` (a small function that recomputes a snapshot off the request path) and have the request read the snapshot. See [Views](./views.md) and [@derive](../background/derive.md).\n- **Only a `@derive` or `@job` may `publish` a View.** Requests read views; background work writes them.\n\nBoth gates are enforced twice: the compiler rejects an illegal call at build time, and the edge rejects it again at runtime, so a hand-edited module cannot sneak past. For the decorator catalog, see [Decorators](../concepts/decorators.md).\n\n## No manual provisioning: how it actually gets set up\n\nYou never create a table or run a migration. Here is the machinery, so the \"it just works\" is not a mystery.\n\nWhen toilscript compiles your backend, it scans every `@database` class and writes a small catalog into the `.wasm` file: for each collection, its name, its family, its key and value type names, and the value's schema version. This catalog rides *inside* the compiled module.\n\nThen, wherever your backend runs, the host reads that catalog once at startup and builds your collections to match:\n\n- Under **`toiljs dev`**, the host is an in-process, in-memory emulator. It reads the catalog and stands up all seven families in memory. This is a development store: single process, single tenant, and cleared when you restart. It exists so you can build and test against real ToilDB behavior with no services to run.\n- On the **Dacely edge**, the host is the real ToilDB, backed by a globally distributed ScyllaDB cluster. It reads the *same* catalog and serves the *same* operations, now durable and worldwide.\n\nBecause both sides read the same catalog, **the same code runs unchanged in dev and in production.** There is no connection string to swap and no provisioning step to run.\n\n```mermaid\nflowchart TD\n SRC[\"@database AppDb<br/>@collection static users: Documents...<br/>@data UserId / User\"]\n SRC -->|toilscript compile| WASM[\"server.wasm<br/>(with an embedded<br/>toildb catalog section)\"]\n WASM -->|toiljs dev reads the catalog| DEV[\"In-memory dev store<br/>(one process, cleared on restart)\"]\n WASM -->|edge reads the same catalog| EDGE[(\"ToilDB on the Dacely edge<br/>(ScyllaDB, worldwide, durable)\")]\n```\n\n## Gotchas\n\n- **Declare collections as `static` fields with no initializer.** Do not try to `new` a collection or assign a handle yourself; the compiler owns that.\n- **Every `@data` field needs a default, and value types must be default-constructible.** A missing default fails to compile.\n- **Dev data is not durable.** The `toiljs dev` store lives in memory and resets on restart. Do not rely on it to persist across dev runs; that is what the edge is for.\n- **Changing a `@data` type is a format change.** Reordering fields or changing a field's type changes the stored layout. Add new fields at the end, and use a migration when you evolve a stored type. See [data types](../backend/data.md).\n- **Pick the family at the type.** The family is read from the collection's declared type, so `Counter<K>` versus `Documents<K, V>` is a real, load-bearing choice, not a hint. Revisit [Choosing a family](./README.md#choosing-a-family-the-decision-guide) if unsure.\n\n## Related\n\n- [ToilDB overview](./README.md): the seven families and how to choose.\n- [Documents](./documents.md): the general-purpose record family (a good first collection).\n- [Data types (`@data`)](../backend/data.md): keys, values, and the codec.\n- [Decorators](../concepts/decorators.md): `@database`, `@collection`, `@query`, `@action`, `@derive`.\n- [@derive](../background/derive.md): recompute snapshots and run scans off the request path.\n",
34
+ "database/unique.md": "# Unique\n\nThe **Unique** family enforces that a value is claimed by only one owner across the entire world. It is how you make usernames, email addresses, and URL slugs one-of-a-kind, safely, even when two people try to grab the same one at the same instant.\n\n## What and why\n\nA **Unique collection** maps a `@data` **claim key** (the thing that must be unique, like a username) to a `@data` **owner value** (who or what claimed it, like a user id). At any moment a claim key is either **unclaimed** or **owned by exactly one owner**. The family gives you three operations: look up who owns a key, claim a key, and release it.\n\nReach for Unique whenever a value must be globally singular:\n\n- usernames or handles\n- email addresses\n- URL slugs or workspace names\n- any \"reserve this name for me\" scenario\n\nWhy not just check a Documents record first? Because a check-then-write has a race: two requests can both check \"is `alice` free?\", both see yes, and both create it. Unique closes that gap. The claim is decided at the key's single **home** (see [consistency](./README.md#eventual-consistency-in-plain-words)), where claims are processed one at a time, so exactly one of the two racers wins.\n\nDeclare one by typing a `@collection` as `Unique<ClaimKey, OwnerValue>`:\n\n```ts\n@data\nclass Username {\n name: string = '';\n constructor(name: string = '') { this.name = name; }\n}\n\n@data\nclass OwnerId {\n userId: string = '';\n constructor(userId: string = '') { this.userId = userId; }\n}\n\n@database\nclass AppDb {\n @collection static usernames: Unique<Username, OwnerId>;\n}\n```\n\n## The operations\n\n`K` is the claim-key type, `V` the owner value type.\n\n| Operation | Signature | Returns | Use it to |\n| --- | --- | --- | --- |\n| `lookup` | `lookup(key: K): V \\| null` | the current owner, or `null` if unclaimed | find out who owns a name |\n| `claim` | `claim(key: K, value: V): ClaimResult<V>` | a `ClaimResult` (see below) | try to take a name for an owner |\n| `release` | `release(key: K, value: V): void` | nothing; **traps** if you are not the owner | give a name back |\n\n`lookup` is a read, so it works in any function. `claim` and `release` are writes, so they need an **Action** (a `@post` route or an `@action`); see [Setup](./setup.md#how-access-is-gated-query-action-and-friends).\n\n### `ClaimResult`\n\n`claim` returns a small object that tells you what happened:\n\n```ts\nclass ClaimResult<V> {\n claimed: bool; // true if YOU own the key now\n owner: V | null; // when claimed is false, who owns it instead\n}\n```\n\n- **`claimed == true`**: you own the key. This covers both a fresh claim and an **idempotent re-claim**: if you claim a key you already own (same owner value), you still get `true`, so retrying a claim is safe. `owner` is `null` in this case.\n- **`claimed == false`**: someone else got there first. `owner` is their value, so you can tell the user \"that name is taken.\"\n\n### `lookup`\n\n`lookup` just reads the current owner without changing anything:\n\n```ts\nconst owner = AppDb.usernames.lookup(new Username('alice'));\nif (owner == null) {\n // 'alice' is free\n} else {\n // owner.userId currently holds 'alice'\n}\n```\n\nNote that `lookup` is subject to [eventual consistency](./README.md#eventual-consistency-in-plain-words): a claim made moments ago in another region may not show up in a far-away `lookup` yet. Do not use `lookup` as your uniqueness guard. `lookup` is for display (\"this name is taken by ...\"); the real guarantee comes from `claim`, which is decided at the home and cannot race.\n\n### `claim`\n\n`claim` is the operation that actually enforces uniqueness. You pass the key and the owner value:\n\n```ts\nconst result = AppDb.usernames.claim(new Username('alice'), new OwnerId('u_123'));\nif (result.claimed) {\n // 'alice' is now yours\n} else {\n // taken; result.owner is the current owner\n}\n```\n\nBecause claims are serialized at the key's home, this is race-safe. If two requests anywhere in the world call `claim('alice', ...)` at the same moment, the home applies them in order: the first gets `claimed: true`, the second gets `claimed: false` with the first as `owner`. There is no window where both win.\n\n### `release`\n\n`release` gives a claim back so the key becomes available again. Only the **current owner** may release: you pass both the key and the owner value, and if that value is not the current owner, `release` **traps** (aborts the request). This prevents one user from releasing another user's name.\n\n```ts\nAppDb.usernames.release(new Username('alice'), new OwnerId('u_123'));\n```\n\nRelease when a name is being changed or an account is deleted, so the name returns to the pool.\n\n## The claim / release lifecycle\n\nA claim is a small state machine: unclaimed, owned, and back to unclaimed.\n\n```mermaid\nstateDiagram-v2\n [*] --> Unclaimed\n Unclaimed --> Owned: claim(key, me) -> claimed: true\n Owned --> Owned: claim(key, me) again -> claimed: true (idempotent)\n Owned --> Owned: claim(key, someoneElse) -> claimed: false, owner: me\n Owned --> Unclaimed: release(key, me)\n```\n\nThe key insight: **claiming is the guard, releasing is cleanup.** You claim to reserve, you release to free. A claim that is never released stays owned forever (there is no automatic expiry; if you want time-limited holds, that is what the [Capacity](./capacity.md) family's TTL reservations are for).\n\n## Worked example: reserving a username on signup\n\nThe safe signup pattern is: claim the username first, then create the account, and if creating the account fails, release the claim so the name is not stranded.\n\n```ts\nimport { Response } from 'toiljs/server/runtime';\n\n@data\nclass Username {\n name: string = '';\n constructor(name: string = '') { this.name = name; }\n}\n\n@data\nclass OwnerId {\n userId: string = '';\n constructor(userId: string = '') { this.userId = userId; }\n}\n\n@data\nclass UserId {\n id: string = '';\n constructor(id: string = '') { this.id = id; }\n}\n\n@data\nclass User {\n id: string = '';\n username: string = '';\n}\n\n@data\nclass SignupInput {\n username: string = '';\n userId: string = '';\n}\n\n@database\nclass AppDb {\n @collection static usernames: Unique<Username, OwnerId>;\n @collection static users: Documents<UserId, User>;\n}\n\n@rest('signup')\nclass Signup {\n // POST /signup (Action: may claim and write)\n @post('/')\n public signup(input: SignupInput): Response {\n const owner = new OwnerId(input.userId);\n\n // 1. Claim the username. This is the uniqueness guard.\n const claim = AppDb.usernames.claim(new Username(input.username), owner);\n if (!claim.claimed) {\n return Response.text('username taken', 409);\n }\n\n // 2. Create the account. If this fails, undo the claim so the name is free.\n const user = new User();\n user.id = input.userId;\n user.username = input.username;\n if (!AppDb.users.create(new UserId(input.userId), user)) {\n AppDb.usernames.release(new Username(input.username), owner);\n return Response.text('could not create account', 409);\n }\n\n return Response.json(user.toJSON().toString());\n }\n}\n```\n\nTwo things make this correct:\n\n- **The claim happens before the account write**, so uniqueness is decided up front.\n- **The claim is released if the follow-up write fails**, so a failed signup does not permanently burn a username.\n\nBecause `claim` is idempotent for the same owner, a client that retries the whole request after a network hiccup does not get a false \"taken\" error: the second `claim('alice', u_123)` returns `claimed: true` again.\n\n## Consistency notes\n\n- **`claim` and `release` are strongly consistent at the key's home.** Two callers can never both own the same key; the home serializes claims. This is the whole point of the family.\n- **`lookup` is eventually consistent.** It reads a possibly-nearby copy, so a very recent claim from elsewhere may not appear yet. Never gate uniqueness on `lookup`; gate it on the result of `claim`.\n- **Claims do not expire.** A claim stays until someone releases it. Use [Capacity](./capacity.md) if you need holds that auto-release after a timeout.\n\n## Gotchas\n\n- **Do not use `lookup` as the uniqueness check.** Its answer can be stale. Call `claim` and trust its `claimed` flag.\n- **Release on failure and on account deletion.** A claim you forget to release stays owned forever, quietly blocking that name.\n- **`release` traps for a non-owner.** Pass the correct owner value, or you abort the request. If you are not sure you own it, `lookup` first (for display) but expect `release` to enforce ownership regardless.\n- **The owner value is data you choose.** Store enough in it (a user id, say) to know who holds the claim, so you can display and release it later.\n\n## Related\n\n- [ToilDB overview](./README.md): the seven families and how to choose.\n- [Setup](./setup.md): declaring the collection and which function kinds may claim.\n- [Documents](./documents.md): the account record you create alongside a claim.\n- [Capacity](./capacity.md): time-limited holds that auto-release (a different kind of \"reserve\").\n- [Data types (`@data`)](../backend/data.md): the claim key and owner value.\n",
35
+ "database/views.md": "# Views (materialized views)\n\nA `View` collection is a **precomputed, read-optimized result** that you publish\nonce and read many times. Reads are a single cheap keyed lookup, no scanning, no\nrecomputing.\n\n## What and why\n\nA \"materialized view\" is a fancy name for a simple idea: instead of computing an\nanswer every time someone asks, you compute it **once**, store the finished\nanswer, and just hand it out on each request. \"Materialized\" means the result is\nmade real and saved (as opposed to computed on the fly). \"View\" means it is a\nread-friendly projection of your underlying data.\n\nThink of a leaderboard. Computing \"the top 10 players by score\" means scanning\nevery player and sorting. You do not want to do that on every page load. So you\ncompute it in the background, save the finished top-10 list as a view, and every\npage load just reads that saved list. Fast, cheap, and it does not get slower as\nyou add players.\n\nUse a `View` when:\n\n- A page needs a result that is **expensive to compute** (a scan, a sort, a fold\n over many rows).\n- That result is **read far more often than it changes** (a home page, a feed, a\n leaderboard, a \"latest N\" list, a rendered summary).\n\nThe rule of thumb: if a route wants \"the newest N of something\" or \"the top N of\nsomething\", that is a scan, and scans are barred on the request path. A `View`\nis how you serve that result without scanning per request.\n\n```mermaid\nflowchart LR\n subgraph off[\"Off the request path (@derive)\"]\n S[(\"Source data<br/>events / counters / records\")] --> D[\"@derive: scan + compute\"]\n D --> P[\"publish(key, result)\"]\n end\n P --> V[(\"View\")]\n subgraph on[\"On the request path (@get)\"]\n G[\"view.get(key)\"] --> V\n end\n```\n\n## The type\n\nA `View` has two type parameters: the **key** (which view) and the **value**\n(the precomputed result).\n\n```ts\nView<K, V>\n```\n\n- `K` is the key type: it picks *which* view to read or publish. For a single\n global leaderboard, the key can be a fixed constant like `'main'`.\n- `V` is the value type: the finished result you serve. Both are\n [`@data`](../concepts/types.md) classes.\n\nDeclare it as a `@collection` field inside a `@database` class, alongside the\nsources it is built from:\n\n```ts\n@database\nclass BoardDb {\n @collection static scores: Events<GameKey, ScoreEvent>; // a source\n @collection static board: View<GameKey, Leaderboard>; // the view\n}\n```\n\n## Operations\n\nA `View` has exactly two operations you use directly (plus `require`, a\nconvenience wrapper). Exact signatures:\n\n| Operation | Signature | What it does |\n| --- | --- | --- |\n| `get` | `get(key: K): V \\| null` | Read the published view, or `null` if nothing has been published yet. |\n| `require` | `require(key: K): V` | Like `get`, but traps (aborts the request) if nothing is published. |\n| `publish` | `publish(key: K, value: V): void` | Overwrite the view for `key` with a fresh result. |\n\n### `get`\n\nA plain keyed read. It is cheap and is allowed from **any** handler, including a\nread-only `@get` route.\n\n```ts\nconst board = BoardDb.board.get(new GameKey('main'));\nif (board == null) {\n // Nothing published yet (e.g. brand-new game). Serve an empty default.\n return new Leaderboard();\n}\nreturn board;\n```\n\nAlways handle the `null` case: until a `@derive` (or `@job`) has published at\nleast once, `get` returns `null`.\n\n### `publish`\n\nOverwrites the stored view with a new value. This is how the view gets its\ncontent.\n\n```ts\nBoardDb.board.publish(new GameKey('main'), freshBoard);\n```\n\n`publish` is **restricted**: you may only call it from a\n[`@derive`](../background/derive.md) or a `@job` (a background task), never from a\nrequest handler (`@get`/`@post`/...). The compiler enforces this, and so does the\nedge at runtime. The reason: a view is meant to be maintained off the request\npath from the source of truth, not written ad hoc by whichever request happens to\nrun.\n\n### How `publish` differs from a Documents write\n\nA `View`'s `publish` looks like writing a value, but it is not the same as a\n[Documents](./documents.md) `patch`/`create`. The differences matter:\n\n| | `Documents` write (`create` / `patch`) | `View` `publish` |\n| --- | --- | --- |\n| Who may call it | Any action handler (`@post`, ...) | Only a `@derive` or `@job` |\n| Meaning | The record IS the source of truth | The view is a **copy** derived from a source |\n| Version control | You may version-check (optimistic concurrency) | The host assigns the version; a later publish always wins (last writer wins) |\n| Read path | Standard keyed read | A read-optimized fast path (views are heavily read) |\n| Who owns correctness | You (each writer edits the true value) | The derive (it recomputes from the source, so the view always converges) |\n\nIn short: a Document is the truth; a View is a saved snapshot of a computation\nover the truth. If the view is ever wrong or stale, the derive just recomputes and\nrepublishes it. You never hand-edit a view from a route.\n\n## Automatic maintenance with `@derive`\n\nYou rarely call `publish` by hand. The normal pattern is a\n[`@derive`](../background/derive.md): a method on your `@database` class that\nreads the sources, builds the value, and publishes it. The runtime runs it for\nyou:\n\n- **Right after a write to a source.** When a request writes one of the\n database's source collections (an `append`, a `counter.add`, a record write),\n the database's derives run right after the response is produced. So the view\n reflects the new data on the next read.\n- **On box load.** When the server starts or reloads, the views are rebuilt from\n their sources before the first read is served.\n\nYou never call a derive yourself, and a derive's own `publish` never re-triggers\nit. See [`@derive`](../background/derive.md) for the full rules.\n\n## Worked example: a leaderboard\n\nA game appends score events; a derive folds them into a top-10 list; a route\nserves that list with a single `get`.\n\n```ts\nimport { ScoreEvent } from '../models/ScoreEvent';\nimport { Leaderboard } from '../models/Leaderboard';\nimport { GameKey } from '../models/GameKey';\nimport { NewScore } from '../models/NewScore';\n\n@database\nclass BoardDb {\n // The source of truth: every score, appended as it happens.\n @collection static scores: Events<GameKey, ScoreEvent>;\n // The materialized view: the current top entries, ready to serve.\n @collection static board: View<GameKey, Leaderboard>;\n\n // Off the request path: scan the recent scores, sort, publish the top 10.\n @derive\n rebuild(): void {\n const key = new GameKey('main');\n const recent = BoardDb.scores.latest(key, 500); // a scan, allowed in a derive\n const board = new Leaderboard();\n board.entries = topTen(recent); // your own sort/aggregate\n BoardDb.board.publish(key, board);\n }\n}\n\n@rest('leaderboard')\nclass LeaderboardRoutes {\n // GET reads the precomputed view: one keyed read, no scan.\n @get('/')\n public top(): Leaderboard {\n const board = BoardDb.board.get(new GameKey('main'));\n return board == null ? new Leaderboard() : board;\n }\n\n // POST records a score. The @derive rebuilds `board` right after.\n @post('/')\n public submit(input: NewScore): Leaderboard {\n BoardDb.scores.append(new GameKey('main'), new ScoreEvent(input.player, input.score));\n return new Leaderboard(); // ack; the GET serves the updated board from the view\n }\n}\n```\n\nThe models:\n\n```ts\n@data\nexport class ScoreEvent {\n player: string = '';\n score: u64 = 0;\n}\n\n@data\nexport class Leaderboard {\n entries: ScoreEvent[] = [];\n}\n```\n\nA \"latest N\" view is the same shape: the derive calls `latest(key, N)` and\npublishes the list. See the [Events](./events.md) page for that variant end to\nend.\n\n## Consistency\n\n- **Last writer wins.** The host assigns each `publish` a version, and a later\n publish always supersedes an earlier one. Because a derive recomputes the whole\n view from the source of truth, the view converges to a correct snapshot; you do\n not need to coordinate concurrent publishes.\n- **A view can be briefly stale.** ToilDB is worldwide, and a view is published\n at its home then copied to other regions in the background (asynchronous\n replication). A read from a far region can lag the newest publish by a moment.\n This is usually fine for the things views hold (feeds, leaderboards, summaries):\n a leaderboard that is a second behind is still a good leaderboard.\n- **`get` returns `null` until the first publish.** Always default the empty case.\n\n## Gotchas\n\n- **`publish` is derive/job only.** You cannot publish from a route. If you need\n to update a view in response to a request, write the *source* (append an event,\n add to a counter) from the action and let the `@derive` republish.\n- **Handle `null` from `get`.** A never-published view reads as `null`, not as an\n empty value. Return a sensible default.\n- **A view is a copy, not the truth.** Never store data *only* in a view. Keep\n the real data in a source family (Documents, Events, Counters) and treat the\n view as a disposable, recomputable projection.\n- **Keep views bounded.** A view value is read whole on every request, so build\n it from a bounded read (top N, latest N, a total), not the entire history.\n\n## Related\n\n- [`@derive`](../background/derive.md): the normal way a view is maintained.\n- [Events](./events.md): the append-only log a view is often folded from.\n- [Counters](./counters.md): another common source for a view (totals).\n- [Documents](./documents.md): mutable source-of-truth records (and how a write\n differs from a publish).\n- [Data types (`@data`)](../concepts/types.md): how view keys and values are stored.\n",
36
+ "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## 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",
37
+ "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",
38
+ "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",
39
+ "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; active links; and navigating in code.\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\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",
40
+ "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) + a coherence hash\n U->>Edge: GET / (ssr route)\n Edge->>Edge: run the wasm render -> 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 + route chunk\n Note over U: React hydrates: attaches to the existing HTML\n Note over U: Page is interactive; no redraw, no 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",
41
+ "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\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",
42
+ "frontend/scripts.md": "# Scripts\n\n`Toil.Script` loads an external or inline `<script>` for you, with control over *when* it runs and a guarantee it runs only *once* across your whole app. Use it instead of a hand-written `<script>` tag for third-party snippets (analytics, chat widgets, embeds). It is the toiljs analog of Next.js `next/script`.\n\n## Why not just write a `<script>` tag?\n\nDropping a raw `<script>` into your JSX is unreliable in a single-page app (an app where the browser loads one HTML shell and JavaScript swaps pages in place, with no full reload). Two problems:\n\n1. **It may not execute.** When React inserts a `<script>` element into the page, the browser does not always run it the way it runs scripts present in the original HTML.\n2. **It runs too often.** As the user navigates between routes that both render that script, React can mount it more than once, so an analytics library or a widget initialises twice.\n\n`Toil.Script` fixes both: it injects a real `<script>` into `<head>` so the browser runs it, and it **deduplicates** by a key so a given script executes at most once for the whole life of the app, even across client-side navigations. It renders nothing into your layout.\n\n## The simplest usage: an external script\n\nGive it a `src`. By default it loads once the app is interactive, which is right for most third-party scripts:\n\n```tsx\n// client/layout.tsx\nimport { type ReactNode } from 'react';\n\nexport default function Layout({ children }: { children?: ReactNode }) {\n return (\n <div className=\"app\">\n <Toil.Script src=\"https://cdn.example-analytics.com/analytics.js\" />\n {children}\n </div>\n );\n}\n```\n\nPutting it in the root layout means it loads once for the whole app and stays loaded as the user navigates. For an external script, the dedup key defaults to its `src`, so you never need an `id`.\n\n## Load strategies\n\nThe `strategy` prop decides *when* the script is injected, relative to your app becoming interactive:\n\n| Strategy | When it runs | Use it for |\n| --- | --- | --- |\n| `afterInteractive` (default) | On mount, once the app is running. | Analytics, chat widgets, most third-party scripts. |\n| `lazyOnload` | Deferred until the browser is idle, after the page's `load` event. | Low-priority extras (a feedback button, a social embed) that should not compete with the initial render. |\n| `beforeInteractive` | As early as possible: injected immediately on first mount. | A script other code depends on immediately. |\n\nOne honest caveat about `beforeInteractive`: a toiljs frontend is a client-only single-page app, so there is no server-rendered `<script>` to run before hydration. `beforeInteractive` therefore still runs *after* hydration, just as early and eagerly as possible on the first mount. It is a priority hint, not a true \"before the page is interactive\" guarantee.\n\n```tsx\n<Toil.Script\n src=\"https://widget.example.com/embed.js\"\n strategy=\"lazyOnload\"\n/>\n```\n\n## Inline scripts\n\nTo run a snippet of code instead of loading a URL, put the code in `children` (as a **string**) and give the script an `id`. An inline script has no `src`, so the `id` is what identifies it for dedup, and it is required:\n\n```tsx\n<Toil.Script id=\"init-theme\" strategy=\"beforeInteractive\">\n {`document.documentElement.dataset.theme =\n localStorage.getItem('theme') ?? 'light';`}\n</Toil.Script>\n```\n\nWithout an `id` (and no `src`), an inline script has nothing to dedup on, so `Toil.Script` does nothing at all. This is a deliberate no-op, not an error, so remember the `id`.\n\n## Reacting to load\n\nThree optional callbacks let you run code around the script's lifecycle:\n\n```tsx\n<Toil.Script\n src=\"https://widget.example.com/embed.js\"\n strategy=\"lazyOnload\"\n onReady={() => {\n // The global the script defines is now available.\n window.MyWidget?.init();\n }}\n onError={(err) => {\n console.warn('widget failed to load', err);\n }}\n/>\n```\n\n- `onLoad` fires **once**, when an external script finishes loading (or an inline script is inserted).\n- `onReady` fires after load **and on every later mount** once the script is already loaded. So if a route that renders the `Toil.Script` is left and revisited, `onReady` runs again, which is the right place to re-initialise a widget.\n- `onError` fires if an **external** script fails to load. After an error the script's key is cleared, so a later remount retries the load.\n\n## All props\n\nRead them straight from the source (`src/client/components/Script.tsx`):\n\n| Prop | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `src` | `string` | (none) | URL of an external script. Omit it when you provide an inline body via `children`. |\n| `children` | `string` | (none) | Inline script body. Mutually exclusive with `src`. |\n| `strategy` | `'beforeInteractive' \\| 'afterInteractive' \\| 'lazyOnload'` | `'afterInteractive'` | When to inject the script (see above). |\n| `id` | `string` | `src` for external scripts | Stable identity for dedup. **Required** for inline scripts. |\n| `type` | `string` | (none) | The `type` attribute, e.g. `'module'` or `'application/json'`. |\n| `onLoad` | `() => void` | (none) | Fired once the script has loaded (external) or been inserted (inline). |\n| `onReady` | `() => void` | (none) | Fired after load, and on every later mount once the script is already loaded. |\n| `onError` | `(error: unknown) => void` | (none) | Fired if an external script fails to load. |\n\nExternal scripts are injected with `async` set, so they never block other work while downloading.\n\n## Gotchas\n\n- **Inline scripts need an `id`.** No `id` and no `src` means nothing to dedup on, so the component quietly does nothing.\n- **Dedup is app-wide and survives navigation.** The same `Toil.Script` rendered on two different routes runs a total of once, keyed by `id` (or `src`). This is the point, but it means you cannot use two copies of the same key to run something twice.\n- **Props are read at injection time, and the script re-injects only if its key or strategy changes.** Changing a handler or an inline body *without* changing the `id` will not re-run the script. If you truly need to re-inject a changed inline script, change its `id`.\n- **`onReady` versus `onLoad`.** Use `onLoad` for one-time setup that must happen exactly once; use `onReady` for setup that should also run each time a component remounts against an already-loaded script.\n- **`beforeInteractive` is not truly before hydration** in a client-only app (see the caveat above). Do not rely on it running before your React tree mounts.\n- **`Toil.Script` renders nothing.** It returns `null`, so you can place it anywhere in your component tree; a layout is the usual home for app-wide scripts.\n\n## Related\n\n- [Rendering and SSR](./rendering.md): how the client-only app and its first paint fit together.\n- [Metadata and SEO](./metadata.md): for `<head>` tags (title, meta, Open Graph), which is a different job from loading scripts.\n- [Frontend overview](./README.md): the `Toil` global and the rest of the client API.\n",
43
+ "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",
44
+ "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",
45
+ "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",
46
+ "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",
47
+ "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",
48
+ "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## 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",
49
+ "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",
50
+ "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",
51
+ "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",
52
+ "introduction/design-principles.md": "# Why toil is built this way (the RSG bar)\n\ntoil is opinionated on purpose. Almost every design decision traces back to one internal rubric it\nholds itself against, and this page shows the rubric and the choice each axis forces.\n\n## The rubric in a paragraph\n\n**RSG** (Resilience and Scale Grade) is toil's own internal rubric for how resilient, distributed,\nfast, lean, and secure an app really is, scored as a single letter from AAA down to D. It grades\nnine axes, and the one rule that matters is this: **your grade is your weakest axis**, never the\naverage and never the best. To earn AAA, all nine must be AAA at the same time. A globally edged\nfrontend on a single-region database is capped by the database. A worldwide system serving a\none-second app is capped by latency. The lowest column sets the grade, every time. RSG is not an\nexternal certification and no auditor issues it; it is a design mirror the team holds up to find\nthe weakest link and fix it first. The full rubric lives at the repository root in\n[`RSG.md`](../../RSG.md).\n\nThe reason for the weakest-link rule is the most common lie in this space: calling something\n\"global scale\" because the read path is global, while the write path is one box in one region.\nAveraging would let that one strong axis hide the weak one. The minimum refuses to.\n\n## The nine axes, and the one choice each one forces\n\nEach axis names a way a system can be weak. Each row is the single design decision toil makes so\nthat 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: your 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 (measured, under 100ms = AAA) | An allocation-free hot path, measured rather than assumed, so the response is fast for real, not just on paper. |\n| **Program performance + efficiency** | Hot-path code quality, and cost per request (no brute-forcing latency with a big server bill) | No blocking work on the request path; the fast path does no wasted work, so 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 (the password never reaches the server in usable form), 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 protocols, *with* graceful fallback for older clients | HTTP/3, QUIC, and WebTransport where they fit, negotiating down cleanly so nobody one version behind gets a blank screen. |\n\n## The punchline\n\ntoil is opinionated because being AAA on **every** axis at once forces these choices. You cannot\nreach the top grade with a fast frontend on a centralized database, or a global system running\nslow code, or a modern stack that only works on the newest browser. The weakest-link rule closes\nevery one of those escape hatches. Any framework that lets a single axis slip is, by its own\nhonest scoring, not AAA. Holding all nine at once is the whole reason 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\n writes is the hard one.\n- [What makes toil hyper-scalable](./hyperscale.md): the topology, latency, and program axes in\n practice.\n- [Security](../concepts/security.md): the security axis and its hard caps.\n- [`RSG.md`](../../RSG.md) at the repository root: the full rubric, the internal mirror this page\n summarizes.\n",
53
+ "introduction/distributed.md": "# How toil is distributed\n\nDistributing a website's reads is easy. Distributing its writes is the hard part almost nobody solves, and it is why \"global\" apps are usually only half global. Here is the problem, and how ToilDB (the database built into toil) actually distributes the writes.\n\n## Reads are easy, writes are hard\n\nA read never changes anything, 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 thing* can collide. A counter says `10`. Tokyo and Paris both read `10`, both add one, both write `11`. The real answer was `12`, so one add vanished with no error. That is a **write conflict**.\n\nYou could make every copy agree before accepting a write, but the network will eventually split (a **partition**), and the **CAP tradeoff** says that during a split you keep only two of consistency, availability, and partition tolerance. You either refuse the write (correct but unavailable) or accept it on one side and reconcile later (available but briefly inconsistent). Distributing writes is a real tradeoff to design around.\n\n\n## So almost everyone centralizes the write database\n\nFaced with that, nearly every stack keeps **one** primary write database in **one** region and spreads only read replicas worldwide. All writes funnel to that one box, one at a time, so conflicts cannot happen.\n\nIt is a reasonable choice, and it 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 your write crosses the planet and back before anything saves. The page was local; the action was not.\n- **The primary is a single point of failure.** One region holds every write, so if it has a bad day, nothing anywhere can be changed.\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 answer: every key has a home\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 is assigned one home: the single source of truth that orders its writes.\n\nTwo things follow, and together they are the whole trick:\n\n- **Writes to one key are safe.** Every write to a key travels to that key's home, which **serializes** them (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 every write funnels through, 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 out, so a reader anywhere gets a nearby copy. Those copies are **eventually consistent**, meaning that for a brief moment (usually milliseconds) after a write lands at the home, a far read can lag before it catches up. For almost all app data this is invisible; the [database overview](../database/README.md) has the full picture.\n\nWhich location owns a key is decided by a shared formula (rendezvous hashing) every node computes 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 pick the right consistency tool\n\nOne \"home orders the writes\" rule fixes the counter, but different jobs want different guarantees. So ToilDB ships **seven families**, each a collection type tuned for one shape of data, each exposing 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 is not one problem with one answer; each family is the right tool for one shape.\n\n## The hard machinery toil provides so you do not have to\n\nThe per-key-home model only works if a lot of unglamorous machinery runs reliably across a flaky network, and ToilDB owns it: 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), ordered **cross-region replication** with per-stream cursors that detect and backfill gaps, **idempotent apply** so redelivered writes cannot double-count, **capacity escrow** and **tenant quotas**, and **failover** with snapshot re-seeding for a cell that has fallen too far behind. Getting all of these right at once is exactly why truly distributed websites are rare.\n\n**Still being finished:** live multi-cell **WAN routing** (wiring many real regions into one running mesh) and the full database-level **leader fencing** on the write path (the host-side leader gate is the current version). The design is settled; the last-mile host wiring is what remains.\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",
54
+ "introduction/how-it-works.md": "# How toil works\n\nWhat your project compiles into, and what happens when a user makes a request. Every term is defined as it appears.\n\n## What \"build\" produces\n\n`toiljs build` turns your one TypeScript project into three outputs, because your code has two homes: the browser and the edge.\n\n- **Client bundle.** Your React app plus any pages toil renders ahead of time, packaged as ordinary web files (HTML, JS, CSS, images). Runs in the browser.\n- **`server.wasm`.** Your backend. You write it as normal TypeScript classes in `server/`, and the **toilscript** compiler turns it into WebAssembly. It runs on the edge, not in the browser and not in Node.\n- **Generated typed client (`shared/`).** A small browser-side client toil generates from the shape of your backend. Your React code calls a normal-looking async function, and the types line up end to end: rename a field on the server and the frontend stops compiling until you fix it.\n\n**WebAssembly** (WASM), in two sentences: a compact binary format that runs at close to native speed with no interpreter warm-up, and runs **sandboxed**, in a locked box that cannot open files, reach the operating system, or make network calls on its own. It touches the outside world only through the small, fixed set of functions the host hands it (read the request, build a response, query the database), which is what makes it safe to pack many apps onto one shared box. ([Why that matters for scale](./hyperscale.md).)\n\n## The request lifecycle\n\nIt all happens on the **edge node nearest the user**, with no trip to a central origin.\n\nTwo terms first:\n\n- **The edge** is a fleet of servers spread across many cities. A request is served by whichever node is physically closest, which means lower latency (the delay before something happens).\n- An **origin server** is the single far machine a traditional site calls back to for anything real. toil has none: your backend and its database are replicated out to the edge, so there is nothing far away to call.\n\n```mermaid\nsequenceDiagram\n participant U as User's browser\n participant E as Nearest edge node\n participant W as server.wasm (your handler)\n participant DB as ToilDB (local copy)\n\n U->>E: Request (page or API call)\n alt Path is a page/asset it can serve\n E-->>U: Prerendered / SSR page or static file\n else Path is a dynamic API call\n E->>W: Decode bytes, call your handler\n W->>DB: Read / write (a local, nearby copy)\n DB-->>W: Result\n W-->>E: Response\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 code?** If the path is a prerendered page, a server-rendered page, or a static asset, the edge serves it and never wakes your backend. This is the fast path for most page loads.\n3. **Otherwise it runs your backend.** The edge decodes the raw bytes into a `Request` object and calls the single entry point of your `server.wasm`, which routes to your handler.\n4. **Your handler reads and writes locally.** When it needs stored data it talks to [ToilDB](../database/README.md), which has a copy right there at the edge. No ocean crossing.\n5. **Your handler returns a `Response`,** toil encodes it back to bytes, and the edge sends it to the browser.\n\nThe mental model for your backend: a function of the request. Bytes in, bytes out, one request at a time.\n\n### Stateless by default\n\nA fresh copy of your handler serves each request, and the next request might be served by a node on the other side of the planet. So anything you set on a field does not survive. This is a feature: interchangeable copies with nothing to coordinate are what let the backend scale worldwide. When you need something to persist, write it to ToilDB. See the [backend overview](../backend/README.md#stateless-by-default).\n\n## The pieces\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 TypeScript backend compiled to `server.wasm`. | The edge |\n| **The Dacely edge** | The Rust runtime that terminates the connection, serves pages, and runs your WASM. | Servers in many cities |\n| **ToilDB** | The globally distributed database, 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\nMost of your backend is the stateless, per-request handler above (tier **L1**). Long-lived connections (L2/L3) and single-worldwide scheduled jobs (L4 daemons) run on other tiers. Full detail, and how the build assigns them, is on the [tiers page](../concepts/tiers.md).\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): where persistent, shared state lives.\n- [Compute tiers](../concepts/tiers.md): L1 request, L2/L3 stream, L4 daemon.\n- [What makes toil hyper-scalable](./hyperscale.md): why this design serves the planet cheaply.\n",
55
+ "introduction/hyperscale.md": "# What makes toil hyper-scalable\n\n**Hyper-scale** is serving very large, worldwide traffic at low latency without rebuilding your app as it grows. The test: when traffic goes from a thousand users to a hundred million across every continent, do you rewrite the system, or just run more of it?\n\nMost stacks scale the easy half (serving pages and cached reads from many places) but leave the hard half (writes, where data actually changes) in one region, so they slow down at once when enough far-away users start writing. toil scales both halves out together. It promises a design where scaling is cheap, adding more identical edge nodes with no central part everything funnels through, not a specific requests-per-second number.\n\n## The mechanisms\n\n**1. Compute next to the user.** toil has no origin server. Your `server.wasm` and its database are replicated to the [edge](../concepts/tiers.md) and run on the node nearest each user, so there is no slow hop to a faraway box. This is the biggest latency win, and the rest of the design exists to support it.\n\n```mermaid\nflowchart LR\n subgraph Origin[\"Everything funnels to one origin\"]\n direction TB\n A1[\"User (Tokyo)\"] -->|slow| O[(\"Origin + DB<br/>(Virginia)\")]\n A2[\"User (Paris)\"] -->|slow| O\n A3[\"User (Sydney)\"] -->|slow| O\n end\n subgraph Toil[\"Edge + distributed DB scales out\"]\n direction TB\n B1[\"User (Tokyo)\"] --> E1[\"Edge + data (Tokyo)\"]\n B2[\"User (Paris)\"] --> E2[\"Edge + data (Paris)\"]\n B3[\"User (Sydney)\"] --> E3[\"Edge + data (Sydney)\"]\n E1 <-.->|\"replicate\"| E2\n E2 <-.->|\"replicate\"| E3\n end\n```\n\nThe left side has one hot center every user drags a request to and back from, a bottleneck no amount of caching removes. The right side has no center: add a city, add an edge node.\n\n**2. WASM isolation and density.** Each site compiles to its own tiny, [sandboxed](./how-it-works.md#what-build-produces) WASM module that starts fast and cannot touch another tenant's files, memory, or network. Hard per-request limits (a memory cap in the tens of MiB, a **hard compute cap** that cuts off a looping handler, rate limits, and hostile-wasm containment) let one box safely hold many tenants at once, which is what makes compute in many cities affordable instead of a luxury.\n\n**3. Allocation-free hot path.** The code that runs on every request wastes nothing: no per-request allocations, and no garbage-collection pauses (so no random latency spikes under load). This earns latency with lean code rather than by overprovisioning hardware to hide slow code, which is a bar toil holds itself to explicitly ([the RSG rubric](./design-principles.md)).\n\n**4. Stateless tier over a distributed database.** A fresh copy of your handler serves each request and keeps nothing, so every node is interchangeable and you scale out purely by adding more. The data they share lives in [ToilDB](../database/README.md), which distributes **writes** too, not just reads, so there is no single box every write funnels through. The write mechanism and its honest trade-off (eventual consistency) are in [how toil is distributed](./distributed.md).\n\n**5. Modern transport.** The edge speaks HTTP/3 over QUIC (with graceful fallback to HTTP/2 and HTTP/1.1, plus WebTransport for realtime), and its networking is tuned to keep the connection-level cost of each request low as traffic grows. You configure none of it.\n\nThe five reinforce each other: take any one away and a bottleneck reappears. No density and edge compute is too expensive to spread; no distributed writes and the database caps you; a wasteful hot path and you are back to buying latency with servers.\n\n## An honest note on numbers\n\nThis describes a design, not a benchmark: real throughput and latency depend on your hardware, where your users are, how your data is shaped, and how your handler is written, and toil removes central bottlenecks and keeps per-request cost low without making a slow handler fast or repealing the speed of light between continents.\n\n## Related\n\n- [How toil is distributed](./distributed.md): distributing the writes, the hard problem this rests on.\n- [Why toil is built this way (the RSG bar)](./design-principles.md): the efficiency check behind the hot path.\n- [Compute tiers](../concepts/tiers.md): L1 through L4, and the stateless request model.\n- [How toil works](./how-it-works.md): the build outputs and the request lifecycle.\n- [The database (ToilDB)](../database/README.md): families, homes, and eventual consistency.\n",
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 and then send you shopping: a database, an auth provider, email, a rate limiter, analytics, a realtime service, a job runner, all wired together and kept in sync by you. toil owns those parts instead: built in, and on from the first line. This page is the catalog of what ships; for how the edge and distribution actually work, see [How toil works](./how-it-works.md) and [How toil is distributed](./distributed.md).\n\n## What is built in\n\n| Feature | What it is | Why it matters |\n| --- | --- | --- |\n| Edge compute over HTTP/3 | Frontend and backend both run on servers in many cities, over HTTP/3 with automatic fallback to HTTP/2 or HTTP/1.1. | Code runs next to the user, so there is no slow round trip to one origin. |\n| [ToilDB](../database/README.md) | A global database with no connection string and seven families (documents, unique, counter, events, capacity, membership, view). | Distributes writes, not just reads, so a thousand servers can write at once without a single-region bottleneck (trade: eventual consistency). |\n| [Post-quantum auth](../auth/README.md) | Password login via `server: { auth: true }`; the password becomes an ML-DSA-44 key in the browser and the server stores only the public key. | The password never crosses the wire in a replayable form, so a breached server yields no usable passwords, and there is no identity vendor to rent. |\n| Automatic SRI | A SHA-384 fingerprint on every local script, preload, and stylesheet, plus an import map covering the whole module graph. | Tampered assets simply do not run, even if a CDN or cache hop is compromised. |\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 call: validated, per-tenant capped, and 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 and time series](../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, correct across every edge location. |\n| [Realtime streaming](../realtime/README.md) | A `@stream` class with `@connect`/`@message`/`@close`/`@disconnect` hooks, opened from React with `useChannel`; WebTransport in production, WebSocket in dev. | A resident instance stays alive per connection and remembers state next to the user (chat, presence, progress). |\n| [Daemons and @derive](../background/README.md) | `@daemon` runs one global background worker on a schedule (interval or cron) with lease-based failover; `@derive` re-runs when its source data changes to refresh a View. | Nightly jobs and rollups run once globally, with no cron server, queue, or leader election to run yourself. |\n| [Owned globals](../services/README.md) | No-import cookies (read/set, sign or encrypt), crypto (hash, HMAC, AES, random), time (the edge clock), and environment/secrets. | Your request's small dependencies live in one system, and your compiled program carries no credential. |\n| [Toolchain](../cli/README.md) | `toiljs create` scaffolding, a shared ESLint config, Prettier plus a decorator-aware plugin, an editor plugin, one CLI, and `toiljs doctor --fix`. | Set up for you and wired by types, so a server change is a compile error at your desk, not a production bug. |\n| LLM-friendly docs | A machine-readable `llms.txt` plus a generated `.toil/docs/` folder and pointer files (`CLAUDE.md`, `AGENTS.md`, editor rules), refreshed on every build. | An assistant reads your current conventions instead of guessing from stale training. |\n\n## Built in versus assemble it yourself\n\n| Capability | toil (built in, zero setup) | Typical stack (you assemble it) |\n| --- | --- | --- |\n| Edge and transport | Frontend and backend worldwide over HTTP/3 | A separate edge product, often reads-only |\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`, `@derive` | A realtime service plus a cron server, queue, and leader election |\n| Asset integrity and toolchain | Automatic SRI, ESLint/Prettier/editor plugins, one CLI | Manual or skipped; configured and maintained by you |\n| Critical-path ownership | The core is toil's own | A mix of vendors you cannot inspect or fix |\n\n## Why it is all a default\n\nNone of this is an upgrade you unlock later. toil grades itself against [RSG](./design-principles.md) (Resilience and Scale Grade), whose one rule is that your grade is your weakest axis, never the average, so these batteries exist to keep any single axis from quietly capping the whole. Where the honest trade fits your project (ToilDB is not general SQL, the server language is a strict TypeScript subset, the catalog is younger than long-established platforms), the built-in stack is the whole point; [Why toil](./why-toil.md) says where it does not.\n",
57
+ "introduction/README.md": "# Understanding toil\n\nThis section is the \"why.\" It explains what toil is, the problem it solves, how it works underneath, and why it is built the way it is. If you read nothing else first, read this: it is what turns \"another framework\" into \"oh, that is the point.\"\n\n## The one big idea\n\nAlmost every website has a split personality. The pretty part (pages, buttons) is served from servers all over the world, close to you. The important part (the database, where your data lives and changes) sits in **one place**, one region, often one machine. Post a comment in Tokyo when the database is in Virginia and your click flies halfway around the planet and back before anything happens.\n\ntoil removes the split. Your **frontend** (React) and **backend** (TypeScript, compiled to a tiny WebAssembly program) both run at the **edge**, near your users, and **ToilDB** is distributed too, so writes do not travel to one far-away box. One language, one project, one deploy, running close to everyone.\n\n```mermaid\nflowchart TB\n subgraph Ordinary[\"The ordinary web\"]\n U1[\"User in Tokyo\"] -->|fast| E1[\"Edge (pretty part)\"]\n E1 -->|slow, one region| DB1[(\"Database in Virginia\")]\n end\n subgraph Toil[\"toil\"]\n U2[\"User in Tokyo\"] -->|fast| E2[\"Edge near Tokyo<br/>(frontend + backend + data)\"]\n end\n```\n\nThat is the entire pitch. The rest of these pages is how toil pulls it off, and why almost nobody else does.\n\n## Read these in order\n\n1. **[Why toil? Who is it for?](./why-toil.md)** The problem with today's stacks, who benefits most, and the honest cases against.\n2. **[The modern stack](./modern-stack.md)** The full catalog of modern tech baked in with zero setup.\n3. **[How toil works](./how-it-works.md)** The whole machine end to end: React client, WebAssembly backend, the edge, ToilDB, the four compute tiers.\n4. **[What makes toil hyper-scalable](./hyperscale.md)** What \"hyper-scale\" means, and the mechanisms that let one small program serve the planet.\n5. **[How toil is distributed](./distributed.md)** The hardest problem in web infrastructure, distributing the writes, and how ToilDB solves it.\n6. **[toil versus other frameworks](./vs-other-frameworks.md)** An honest comparison with Next.js, Rails, Django, serverless, edge runtimes, and backend-as-a-service.\n7. **[Why toil is built this way (the RSG bar)](./design-principles.md)** The rubric toil grades itself against.\n\n## The short version\n\n- **Who it is for:** people building real products who want global speed and reliability without a platform team or ten stitched-together vendors. See [Why toil](./why-toil.md).\n- **Why it is fast:** the code runs next to the user, with no slow trip to a central origin. See [Hyper-scale](./hyperscale.md).\n- **Why it is different:** it distributes the writes, not just the reads. See [Distributed](./distributed.md).\n- **Why it is safe:** the backend is a sandbox, passwords never reach the server in a usable form, secrets never ship in the code, and the browser verifies every asset it loads. See [Security](../concepts/security.md).\n\nWhen you are ready to build, jump to [Getting started](../getting-started/README.md).\n",
58
+ "introduction/vs-other-frameworks.md": "# toil versus other stacks\n\nAn honest look at where each stack you already use hits a ceiling, and why toil bets on a different shape. Each of these tools is genuinely good at what it was built for, so the goal is not to crown a winner but to show *where* each one typically caps.\n\nWe grade with the [RSG rubric](./design-principles.md), whose rule is that a system's grade is its **weakest** axis, never the average. So the useful question is not \"what is it great at?\" but \"what quietly caps it?\" For most stacks the answer is the same axis: the **data path** (how and where writes happen), sometimes joined by **dependencies** (how much of the critical path you rent versus own).\n\nRSG is toil's own internal rubric, not an external standard, and every stack below can be configured many ways. The caps described are the *typical* production shape, not a claim that they are unavoidable.\n\n## Where each stack caps\n\n| Stack | Great at | Typical binding axis | Why it caps there |\n| --- | --- | --- | --- |\n| **Next.js / Vercel** | DX, React, global edge reads | data path | Global reads, single-region writes: a sudden write-heavy spike concentrates on one DB box that edge caches and read replicas cannot relieve, while serverless cold starts add latency and per-invocation billing climbs exactly when load peaks |\n| **Rails / Django** | Maturity, batteries included | topology / availability / data | Centralized single-region monolith: one place to be near, one place to fail, and one primary every write must reach |\n| **Serverless functions** (Lambda, Cloud Functions) | Elastic stateless compute | data path | Distributes compute, not state; the central DB stays the write bottleneck, and a cold-start burst adds latency and cost right when traffic surges |\n| **Edge runtimes** (Workers, Deno Deploy) | Code at the edge, near users | data path | Distributes compute beautifully, but the DB you attach is usually single-region (Durable Objects / D1 excepted, below) |\n| **BaaS** (Supabase, Firebase) | Fastest to start | dependencies + data path | You rent a managed service you cannot inspect or fix, and writes resolve against a primary |\n| **toil** | Owned stack, distributed writes | aims for no single binder | Every key has one home region that serializes its writes; auth, DB, email, and jobs are owned, so the usual caps are designed out (latency and client axes are still yours to earn) |\n\n## One line each\n\n- **Next.js / Vercel:** superb reads and DX, but a sudden spike (a viral launch, a flash sale, a timed drop where everyone writes in the same second) lands as a thundering herd on the one write region, so latency, timeouts, and per-invocation cost climb together while edge caching helps only the reads.\n- **Rails / Django:** mature and productive, capped by its single-region shape; you can add replicas and standbys to climb, but distributed *writes* are not in the default model.\n- **Serverless functions:** great elastic compute, but it is stateless compute in front of a central database, so a write burst still bottlenecks on that store and each cold invocation bills separately.\n- **Edge runtimes:** the closest in spirit to toil's compute model, yet edge compute in front of a central database is just a faster front door to the same bottleneck.\n- **Cloudflare Durable Objects / D1:** the closest mainstream analog to toil's idea, and credit is due. A Durable Object gives one object a single-writer home that serializes 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 DB product, auth, email), whereas toil ships distributed writes, the seven database families, auth, email, streaming, and jobs as one integrated owned stack. Which you prefer is a genuine trade-off.\n- **BaaS (Supabase, Firebase):** fastest to start, but the convenience is a managed service on your critical path, and writes still resolve against a primary, so it is **dependency-bound and data-bound**.\n\n## Being honest about toil's own limits\n\nRSG grades toil by the same weakest-link rule, and some axes are not handed to you for free:\n\n- **Younger, smaller ecosystem.** Fewer integrations, tutorials, and hosting options than the mature stacks above. If your project is defined by a large existing integration catalog, that gap is real today.\n- **The server language is a TypeScript subset.** toilscript compiles a strict subset of TypeScript to WebAssembly: no arbitrary npm packages or Node APIs on the server, built-in globals instead. That is the price of the small, fast, safe sandbox.\n- **ToilDB is not SQL.** It is seven purpose-built families, not a relational engine, so heavy ad-hoc joins and existing SQL schemas are not its shape (see the [database overview](../database/README.md)).\n- **Some axes you still earn.** RSG measures delivered latency, program performance, and client performance from *your* code. toil removes the structural caps, but it cannot make slow application code fast or a bloated client light.\n\n## toil's bet\n\nEvery stack above is capped in almost the same place: the write path is one box in one region, or the critical path leans on a service you rent. toil's bet is to refuse both at once, own the whole stack and distribute the writes, so the axes that usually cap a \"global\" system are designed out and the only limits left are the ones your own code sets.\n\nWhether that bet fits *your* project is the honest checklist in [Why toil](./why-toil.md).\n\n## Related\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- [How toil is distributed](./distributed.md): the mechanism behind distributed writes (every key's single home region).\n- [Why toil is built this way (the RSG bar)](./design-principles.md): the weakest-link rubric this comparison uses.\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\nThe thesis in one line: toil is the modern full-stack tech a developer would actually want, AAA-grade from the very first line, and hyper-scalable at the same time. Even a simple pizza site gets top-tier infrastructure with zero setup. Distributed writes are one pillar of that. The modern stack that just works is the heart.\n\n## The problem with today's stacks\n\nTwo problems, really.\n\n**Read-global, write-central.** Your pages load fast from caches worldwide. But a *write* (a comment, a like, an order) usually travels to one database in one region. A user in Sydney writing to a database in Virginia pays for the round trip. That single region is also a single point of failure.\n\n**The ten-vendor tax.** A typical production stack is stitched from rented services: a frontend host, serverless functions, a managed database, auth, email, a queue, a cache, analytics, realtime. Each is its own account, bill, SDK, and failure mode. You did not set out to be a systems integrator, but the stack hands you the job.\n\nAnd every third-party service on your **critical path** (what must work for a request to succeed) is a black box you cannot inspect, patch, or fully secure. When it is slow, you are slow. When it is breached, part of you is breached.\n\nThe result: a solo builder and a funded startup hit the *same* wall. Good, safe, fast infrastructure means assembling and babysitting a lot of parts, so most people settle for less.\n\n## What toil does instead\n\nClose the gap, own the pieces, and make the good version the *default* version. Four pillars.\n\n### 1. AAA-grade from the first line\n\nTop-tier infrastructure on day one, on the smallest project, with zero setup: edge compute (your code runs near users worldwide), one-line post-quantum login, automatic tamper-proofing of your app's code, HTTP/3, and a global database already there.\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### 2. Batteries-included, and owned\n\nAuth, database, email, rate limiting, analytics, realtime streaming, and background jobs are all built in and are 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 praying 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### 3. A modern DX that just works\n\nTypeScript 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 (a compile error at your desk, not a production bug).\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` that fixes common problems in place. The docs are even LLM-friendly, so an AI assistant reads your current conventions instead of guessing. More in [The modern stack](./modern-stack.md).\n\n### 4. Hyper-scalable and distributed (one pillar, not the whole story)\n\nYour backend compiles to a tiny sandboxed **WebAssembly** module (a compact, locked-down binary that runs at near-native speed), so one edge box safely runs many apps, which makes running near everyone affordable.\n\nAnd the 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. The trade is eventual consistency: a far read can lag a few milliseconds. 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, and 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",
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",
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",
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",
63
+ "services/analytics.md": "# Analytics\n\nRead your site's own usage numbers (requests, bytes, cache hits, database ops, errors, and more) straight from a route, with no extra code and no third-party service.\n\nReach for analytics to build a status or usage dashboard for a site: how much traffic it serves, how effective its cache is, how many database operations it runs. It is **infrastructure metering per domain**, not per-user product analytics. There are no custom events; you read a fixed catalog of counters the edge already keeps for you.\n\n`Analytics` is an **ambient global**: use it with no import, like [`crypto`](./crypto.md) or `Time`.\n\n## What \"metering\" means\n\nEvery time the edge (the Dacely server running your code) serves a request, runs a database operation, opens a stream, or sends an email, it **counts** that into a set of per-domain counters. Those counters live in the same worldwide, distributed database that backs [ToilDB](../database/README.md), so the total for your site is correct across every edge location, not just the one machine that happened to serve a request.\n\n`Analytics.self()` reads a **snapshot** of your site's current counter values. You do not record anything yourself; the numbers are already there.\n\n```mermaid\nflowchart LR\n R[\"Every request, DB op,<br/>stream, email\"] --> C[\"Edge counts it into<br/>per-domain counters\"]\n C --> D[(\"Distributed counters,<br/>worldwide\")]\n D --> S[\"Analytics.self()<br/>reads a snapshot\"]\n S --> Y[\"Your route returns<br/>the numbers\"]\n```\n\n## Read your site's stats\n\n`Analytics.self()` returns a `TenantStats` object. Every counter is a **named getter** that returns a `u64` (an unsigned 64-bit integer, always zero or positive), so mapping the numbers into a response needs no casts at all.\n\n```ts\nimport { RouteContext } from 'toiljs/server/runtime';\n\n@rest('metrics')\nclass Metrics {\n @get('/')\n public overview(ctx: RouteContext): string {\n const s = Analytics.self();\n // Every getter is a u64. cacheRatio is the one f64 (a fraction 0..1).\n return `requests=${s.requests} cacheHits=${s.cacheHits} ratio=${s.cacheRatio}`;\n }\n}\n```\n\nYou can also read any counter by its numeric id with `s.metric(MetricId.Requests)`, which returns `0` for an unknown id. The named getters are preferred: they are self-documenting and there are no magic strings to mistype.\n\n### The counter catalog\n\nEvery getter below is a lifetime running total: it only ever goes up, and reading it never resets it. They are grouped by area. (`MetricId` is the matching numeric id, an ambient enum you can use with no import.)\n\n**Requests and the L1 edge**\n\n| Getter | What it counts |\n| --- | --- |\n| `requests` | HTTP requests served |\n| `bytesOutL1` | Response bytes sent |\n| `bytesInL1` | Request bytes received |\n| `status2xx` | 2xx responses (success) |\n| `status3xx` | 3xx responses (redirects) |\n| `status4xx` | 4xx responses (client errors) |\n| `status5xx` | 5xx responses (server errors) |\n| `staticHits` | Static assets served without running your code |\n| `wasmDispatches` | Times your compiled handler ran |\n| `executorFullRejects` | Requests rejected because the worker pool was full |\n| `unknownHostRejects` | Requests rejected for an unknown host |\n| `rateLimitedRejects` | Requests rejected by rate limiting |\n| `gasUsed` | Total compute \"gas\" your handlers consumed |\n\n**Database (ToilDB)**\n\n| Getter | What it counts |\n| --- | --- |\n| `dbOps` | Database operations issued |\n| `dbReads` | Read operations |\n| `dbWrites` | Write operations |\n| `dbErrors` | Operations that errored |\n| `dbLatencyNsSum` | Summed operation latency, in nanoseconds |\n| `meanDbLatencyNs` | Derived: `dbLatencyNsSum / dbOps` (0 with no ops) |\n\n**Streams (WebTransport realtime)**\n\n| Getter | What it counts |\n| --- | --- |\n| `streamAccepts` | Stream connections accepted |\n| `streamRejectWrongNode` | Streams that reached the wrong node |\n| `streamRejectCapacity` | Streams rejected for capacity |\n| `streamRejectArtifact` | Streams rejected for a missing or invalid build |\n| `streamRejectGuest` | Streams your code rejected |\n| `streamTraps` | Stream handler crashes |\n| `streamIdleTimeouts` | Streams closed for being idle |\n| `streamBytesIn` / `streamBytesOut` | Bytes received / sent on streams |\n| `streamBackpressureEvents` | Times a stream had to slow down |\n| `streamCloses` | Clean closes |\n| `streamDisconnects` | Abrupt disconnects |\n\n**Daemons (L4 background jobs)**\n\n| Getter | What it counts |\n| --- | --- |\n| `daemonStarts` / `daemonStartFailures` | Daemon starts / failed starts |\n| `daemonTicksFired` | Scheduled ticks that ran |\n| `daemonTicksSkippedNotLeader` | Ticks skipped because this node was not the leader |\n| `daemonTicksFailed` | Ticks that failed |\n| `daemonLeaderAcquires` / `daemonLeaderFenced` | Leadership gained / lost |\n| `daemonHttpCallAttempts` / `daemonHttpCallFailures` | Outbound HTTP calls attempted / failed |\n\n**Memory, email, and cache**\n\n| Getter | What it counts |\n| --- | --- |\n| `memGrownBytes` | Wasm memory grown, in bytes |\n| `emails` | Emails sent |\n| `cacheHits` | Responses served from the edge cache |\n| `cacheMisses` | Cacheable responses that missed the cache |\n| `cacheRatio` | Derived **`f64`**: `cacheHits / (cacheHits + cacheMisses)`, a fraction from 0 to 1 (0 when there were no cacheable responses) |\n\n### Live gauges and request windows\n\nA few fields are not lifetime totals. They still read as `u64`.\n\n**Live gauges** are the current level right now, not a running total:\n\n- `connectedStreams`: streams connected at this moment.\n- `committedMemory`: wasm memory committed right now, in bytes.\n\n**Request windows** show your current rate-limit usage against your plan cap (a cap of `0` means unlimited):\n\n- `reqMinuteUsed` / `reqMinuteCap`: requests used and the cap for the current 1-minute window.\n- `reqDayUsed` / `reqDayCap`: requests used and the cap for the current 24-hour window.\n\nThere is also `nowMs`, the edge wall-clock time (in milliseconds) when the snapshot was taken, so you can show \"as of\" and compute how fresh the numbers are.\n\n## Time series (for graphs)\n\nThe totals above are single numbers. To draw a graph over time, use `Analytics.series(metric, range)`, which returns a `Series`: a list of per-bucket totals from oldest to newest.\n\n```ts\n@get('/requests-24h')\npublic requests24h(ctx: RouteContext): string {\n const s = Analytics.series(MetricId.Requests, AnalyticsRange.H24);\n // s.points -> per-bucket totals, oldest to newest\n // s.bucketSecs -> seconds each bucket covers\n // s.headMs -> end time (ms) of the newest bucket\n // s.ratePerSec(i) -> requests/second for bucket i (points[i] / bucketSecs)\n return `buckets=${s.points.length} bucketSecs=${s.bucketSecs}`;\n}\n```\n\n`AnalyticsRange` picks the window and the resolution. Short ranges use per-minute buckets for a smooth line; the rest use per-hour buckets (kept for 30 days):\n\n| Range | Span | Bucket width |\n| --- | --- | --- |\n| `H1` | 1 hour | 1 minute |\n| `H6` | 6 hours | 1 minute |\n| `H12` | 12 hours | 1 hour |\n| `H24` | 24 hours | 1 hour |\n| `D3` | 3 days | 1 hour |\n| `D7` | 7 days | 1 hour |\n| `D14` | 14 days | 1 hour |\n| `D30` | 30 days | 1 hour |\n\nThe series stores **totals per bucket**, never rates. A rate is derived: `ratePerSec(i)` divides bucket `i` by its width for you, or you can compute `points[i] / bucketSecs` yourself. Most metrics you will chart are counters (`MetricId.Requests`, `MetricId.CacheHits`, and so on).\n\n## Permissions: who can read what\n\n- **Your own site.** `Analytics.self()` and `Analytics.series(...)` always read the site the request landed on. The calling domain is decided by the edge; your code cannot forge it.\n- **Cross-site reads are for `dacely.com` only.** The privileged `dacely.com` domain (the platform dashboard) may read any site with `Analytics.site(domain)`, `Analytics.siteSeries(domain, ...)`, and `Analytics.listSites(...)`. For any other caller, or an unknown domain, `site` and `siteSeries` return `null` and `listSites` returns an empty page. There is no way for a normal site to read another site's numbers.\n\n`Analytics.listSites(cursor, limit)` enumerates site names for the dashboard, one page at a time. It returns a `SiteList` with `sites: string[]` and `hasMore: bool`. When `hasMore` is true, pass the last name back as the next `cursor` to get the following page.\n\n```ts\n// Only meaningful when the calling domain is dacely.com.\nconst page = Analytics.listSites('', 100); // first 100 site names\nfor (let i = 0; i < page.sites.length; i++) {\n const other = Analytics.site(page.sites[i]); // TenantStats | null\n if (other != null) {\n // read other.requests, other.cacheRatio, ...\n }\n}\n```\n\n## Worked example: return everything\n\nThis route reads the full snapshot and maps it into a typed response. Mapping is mechanical: every getter is a `u64` and every response field is a `u64`, so there are no casts. The example groups a representative field from each area; add the rest of the getters from the catalog above the same way.\n\n```ts\nimport { RouteContext } from 'toiljs/server/runtime';\n\n/** The shape returned to the client. All fields are u64 except the one ratio. */\n@data\nexport class UsageReport {\n // requests / L1\n requests: u64 = 0;\n bytesOut: u64 = 0;\n status2xx: u64 = 0;\n status5xx: u64 = 0;\n gasUsed: u64 = 0;\n // database\n dbOps: u64 = 0;\n dbErrors: u64 = 0;\n meanDbLatencyNs: u64 = 0;\n // cache\n cacheHits: u64 = 0;\n cacheMisses: u64 = 0;\n cacheRatio: f64 = 0;\n // live gauges (current level, not a total)\n connectedStreams: u64 = 0;\n committedMemory: u64 = 0;\n // request windows (cap 0 = unlimited)\n requestsThisMinute: u64 = 0;\n requestsThisMinuteCap: u64 = 0;\n requestsToday: u64 = 0;\n requestsTodayCap: u64 = 0;\n // when the snapshot was read (edge ms)\n nowMs: u64 = 0;\n}\n\n@rest('usage')\nclass Usage {\n @get('/')\n public report(ctx: RouteContext): UsageReport {\n const s = Analytics.self();\n const out = new UsageReport();\n\n out.requests = s.requests;\n out.bytesOut = s.bytesOutL1;\n out.status2xx = s.status2xx;\n out.status5xx = s.status5xx;\n out.gasUsed = s.gasUsed;\n\n out.dbOps = s.dbOps;\n out.dbErrors = s.dbErrors;\n out.meanDbLatencyNs = s.meanDbLatencyNs;\n\n out.cacheHits = s.cacheHits;\n out.cacheMisses = s.cacheMisses;\n out.cacheRatio = s.cacheRatio;\n\n out.connectedStreams = s.connectedStreams;\n out.committedMemory = s.committedMemory;\n\n out.requestsThisMinute = s.reqMinuteUsed;\n out.requestsThisMinuteCap = s.reqMinuteCap;\n out.requestsToday = s.reqDayUsed;\n out.requestsTodayCap = s.reqDayCap;\n\n out.nowMs = s.nowMs;\n return out;\n }\n}\n```\n\nBecause this is a `@data` return type, the client gets a fully typed result from `Server.REST.usage.report()`. See [structured data types](../backend/data.md) for how `@data` works.\n\n## Local dev behavior\n\nUnder `toiljs dev`, the real per-domain metering does not exist (there is only one machine and no fleet), so the dev server returns a **fixed sample** `TenantStats` and a gentle synthetic ramp for `series(...)`. That lets you build and style a dashboard locally. Dev is also permissive: it returns sample data for any domain, because the real `dacely.com`-only check is enforced on the edge. Your code is unchanged; only the numbers differ.\n\n## Gotchas and limits\n\n- **Totals never reset.** The counter getters are lifetime running totals. To see \"requests in the last hour\", use `series(...)` and sum the buckets, not the totals.\n- **Values are `u64`.** Map them into `u64` fields with no casts. The only non-integer field is `cacheRatio` (and `Series.ratePerSec`), which are `f64`.\n- **Not per-user analytics.** These are infrastructure counters per domain. There is no way to record a custom event or attribute a number to a specific user.\n- **Cross-site reads need `dacely.com`.** `Analytics.site(...)` returns `null` for everyone else. Do not build a feature that reads another site's stats.\n- **Snapshots, not live streams.** Each call reads the current values once. `nowMs` tells you when.\n\n## Related\n\n- [Caching](./caching.md), the source of `cacheHits`, `cacheMisses`, and `cacheRatio`.\n- [ToilDB database](../database/README.md), the source of the `db*` counters and where these counters are stored.\n- [Structured data types](../backend/data.md), for the `@data` return type in the worked example.\n- [Compute tiers](../concepts/tiers.md), for what \"L1\", \"streams\", and \"daemons\" mean in the catalog.\n",
64
+ "services/caching.md": "# Caching\n\n**Caching lets the edge keep a copy of a response and hand it back instantly, without re-running your code.** You opt in per route, either with the `@cache` decorator or by calling `Response.cache(...)`.\n\n## What \"cache\" means here\n\nA **cache** is a fast, temporary store of a previous answer. When a request comes in, the edge can check: \"have I already computed this exact response recently?\" If yes, that is a **cache hit**: it returns the saved copy in microseconds and never wakes up your WebAssembly program. If no, that is a **cache miss**: it runs your handler, sends the result, and (if you asked it to) saves a copy for next time.\n\nThere are two places a response can be cached:\n\n- **The edge cache** is shared: one saved copy on an edge server answers many users. This is where the big speedups come from.\n- **The browser cache** is private to one user's browser. You control it with a standard `Cache-Control` header.\n\nYou can use one or both.\n\n## When to cache (and when not)\n\nCache a response only when it is the **same for everyone** and safe to reuse for a little while:\n\n- Good: a public leaderboard, a product catalog, a blog index, an exchange-rate snapshot, a \"server status\" endpoint.\n- Bad: anything personalized (a user's dashboard, their cart, \"hello, Alice\"). If you cached that, one user could be served another user's data. The edge has strong safety rails against this (below), but the simplest rule is: **do not cache per-user data on the shared edge.**\n\nCaching is **always opt-in**. A response you do not mark is never stored. There is no \"cache every GET\" mode, because the edge cannot tell a personalized response from a public one on its own.\n\n## How: the `@cache` decorator\n\nA **decorator** is an annotation you put right above a route method. `@cache` tells the edge how long it may reuse the response. The numbers are positional:\n\n```ts\nimport { Response } from 'toiljs/server/runtime';\n\n@rest('leaderboard')\nclass Leaderboard {\n @cache(60) // 60 MINUTES at the edge\n @get('/')\n public top(): Response {\n // ...expensive query...\n return Response.json(standings);\n }\n}\n```\n\nThe full form is `@cache(edgeTtlMinutes, browserTtlSeconds?, privateScope?, allowAuth?)`:\n\n```ts\n@cache(60) // 60 minutes at the edge, browser does not cache\n@cache(60, 300) // + tell the browser to cache for 5 minutes (300s)\n@cache(60, 300, true) // + mark it \"private\": browser only, never the shared edge\n@cache(60, 300, true, true) // + allow caching even for logged-in requests (see rails)\n```\n\n**TTL** means \"time to live\": how long a saved copy stays fresh before the edge throws it away and recomputes. Notice the units differ on purpose: the edge TTL is in **minutes**, the browser TTL is in **seconds**.\n\nEvery argument must be a plain number or `true`/`false` literal. If you pass something computed (a variable, a function call), the decorator safely degrades to \"not cached\" instead of misbehaving.\n\n## How: `Response.cache(...)`\n\n`@cache` is just a shorthand. You can do the same thing by hand on any `Response`, which is handy when you decide at runtime:\n\n```ts\npublic cache(\n edgeTtlMinutes: u16,\n browserTtlSeconds: u32 = 0,\n privateScope: bool = false,\n allowAuth: bool = false,\n): Response\n```\n\n```ts\nreturn Response.json(body).cache(60, 300);\n```\n\n`cache(...)` returns the same `Response`, so it chains. There is also a shorthand for \"edge only, no browser caching\":\n\n```ts\nreturn Response.bytes(blob).cacheFor(5); // edge-cache 5 minutes\n```\n\nUnder the hood, both set one header, `Dacely-Cache-Control`, that the edge reads and then strips (it never reaches the client).\n\n## The parameters, in detail\n\n| Parameter | What it does |\n| --- | --- |\n| `edgeTtlMinutes` | How long the shared edge may serve the saved copy. Clamped to a 24-hour maximum, no matter what you pass. `0` means no edge caching. |\n| `browserTtlSeconds` | The `max-age` sent to the browser. `0` (default) means the browser is told not to cache. |\n| `privateScope` | Marks the response `private`: it may only live in the browser's own cache, never the shared edge or a CDN. |\n| `allowAuth` | Permits caching a response to a logged-in request. Off by default (see safety rails). |\n\n## A hit vs a miss, visually\n\n```mermaid\nflowchart TD\n R[\"Request arrives<br/>at the edge\"] --> Q{\"Saved copy<br/>still fresh?\"}\n Q -->|Yes: cache HIT| H[\"Return the saved response<br/>(microseconds, no code runs)\"]\n Q -->|No: cache MISS| M[\"Run your WebAssembly handler\"]\n M --> D{\"Marked cacheable<br/>and eligible?\"}\n D -->|Yes| S[\"Save a copy for the TTL,<br/>then return it\"]\n D -->|No| X[\"Return it, save nothing\"]\n```\n\nEvery response the edge sends carries a `Dacely-Cache` header so you can see what happened while debugging:\n\n| `Dacely-Cache` value | Meaning |\n| --- | --- |\n| `HIT` | Served from the cache; your code did not run. |\n| `MISS` | Your code ran, and the response was saved (the next identical request should `HIT`). |\n| `DYNAMIC` | Your code ran, and the response was not cached (no directive, or not eligible). |\n| `BYPASS` | A fallback path (for example the compute layer was momentarily unavailable); not a cached answer. |\n\n## Safety rails (why cross-user leaks cannot happen)\n\nThe edge refuses to store certain responses even if you asked it to. These rules exist because a shared cache is exactly where one user's data could accidentally leak to another:\n\n- **5xx responses are never cached.** A server error is temporary; caching a `500` would keep serving the failure for the whole TTL. (2xx, 3xx, and 4xx are cacheable: a redirect or a `404`/`410` is a predictable function of the request.)\n- **A response with a `Set-Cookie` is never cached.** A cookie is per-user state.\n- **A response to a logged-in request is not cached** unless you pass `allowAuth = true`. This stops one user's personalized response from being handed to someone else. Even with `allowAuth = true`, such an entry is only ever served back to other **authenticated** requests, never to an anonymous one (which would skip the auth check entirely).\n- **The edge TTL is clamped to 24 hours.**\n\nThe cache is also keyed precisely: an entry is identified by the **host, method, path, and the exact request body**. So a `POST` with body `A` never returns the copy saved for body `B`, and a `GET` never returns a `POST`'s copy.\n\nBecause auth checks and body decoding run **before** the cache directive is applied, an unauthorized request is rejected with `401` before anything is stored, and a cached copy only ever comes from a handler that actually ran successfully.\n\n## ETag, Last-Modified, and 304 (static files)\n\nThe `@cache` decorator above is for **dynamic** responses (ones your code computes). Your project's **static files** (the built HTML, JS, CSS, and images) get a second, automatic layer of caching that you do not configure. It is worth understanding because it is what makes repeat page loads cheap.\n\nEvery static file the edge serves carries three headers:\n\n- **`ETag`**: a short fingerprint of the file's contents (a \"version tag\"). toiljs uses a *weak* ETag (it starts with `W/`), which survives compression by a CDN in front of the edge.\n- **`Last-Modified`**: when the file last changed.\n- **`Cache-Control`**: content-hashed assets (files whose name contains their content hash, like `app-CfvHW67Y.js`) get `immutable` with a one-year max-age, because the URL itself changes whenever the content does. Everything else (HTML, non-hashed assets) gets `must-revalidate`, so the browser always checks freshness.\n\n**Revalidation** is the trick. Instead of re-downloading a file it already has, a browser asks \"has this changed?\" by echoing the tag back in an `If-None-Match` header (or the date in `If-Modified-Since`). If the file is unchanged, the edge replies **`304 Not Modified`** with an empty body, and the browser reuses its copy. A `304` is tiny and fast.\n\n```mermaid\nsequenceDiagram\n participant B as Browser\n participant E as Dacely edge\n B->>E: GET /app.js<br/>If-None-Match: W/\"1a2b-c3d4\"\n alt file unchanged\n E-->>B: 304 Not Modified<br/>(no body)\n else file changed\n E-->>B: 200 OK<br/>(new bytes + new ETag)\n end\n```\n\nThe precedence follows the HTTP spec (RFC 7232): if the request has `If-None-Match`, that decides it; only when it is absent does `If-Modified-Since` apply. You get all of this for free; there is nothing to turn on.\n\n## Where cached bodies live (memory bounds)\n\nYou do not need this to use caching, but it explains the limits you may hit.\n\nThe edge response cache is bounded and hard-capped so it can never exhaust a node's memory:\n\n- **RAM tier**: small, short-lived responses live in memory, within a bounded budget (on the order of ~128 MB) plus a cap on the number of entries. A single response over ~256 KB does not go in RAM.\n- **Disk tier (optional)**: when the operator enables an on-disk spill directory, a **big** response (over ~256 KB) or a **long-lived** one (TTL of 10 minutes or more) is written to disk and served back with near-zero RAM, the same way static files are. If disk spill is not enabled, a big response is simply not cached (you will see `Dacely-Cache: DYNAMIC`).\n\nExpired entries are dropped on read (a stale entry is treated as a miss) and reclaimed when the cache needs room. Nothing survives a process restart.\n\n## Gotchas\n\n- **Units differ.** Edge TTL is in minutes; browser TTL is in seconds. `@cache(60)` is one hour at the edge, not one minute.\n- **Do not cache personalized data on the shared edge.** If a response depends on who is asking, either do not cache it, or use `privateScope` so it is browser-only.\n- **A `Set-Cookie` disables edge caching for that response.** If you set a cookie, that response will not be edge-cached, by design.\n- **Very large or slow-to-change bodies may not be cached** unless disk spill is enabled on the node. Check the `Dacely-Cache` header if a response you expected to cache shows `DYNAMIC`.\n- **Nothing persists across restarts.** The cache is a speed optimization, never a source of truth.\n\n## Related\n\n- [Rate limiting](./ratelimit.md): protect a route before it runs.\n- [Analytics](./analytics.md): see your cache hit ratio per site.\n- [Cookies](./cookies.md): why a `Set-Cookie` opts a response out of edge caching.\n- [Auth, sessions, and `@user`](../auth/usage.md): why logged-in responses are not cached by default.\n- [Every decorator](../concepts/decorators.md).\n",
65
+ "services/cookies.md": "# Cookies\n\n**Cookies let you store a small piece of state in the user's browser and read it back on the next request.** toiljs ships a complete cookie layer: read incoming cookies, set them on a response, and (optionally) sign or encrypt their values so they cannot be tampered with.\n\n## What a cookie is\n\nA **cookie** is a tiny named value the browser holds for your site. The browser sends it back on every request to your site (in a `Cookie` request header), and you can change or add cookies by putting a `Set-Cookie` header on your response. Cookies are how a site remembers things between requests: a theme preference, a feature flag, a session id.\n\nTwo directions to keep straight:\n\n- **Reading** happens on the `Request` (the browser sent its cookies to you).\n- **Writing** happens on the `Response` (you tell the browser to store or clear a cookie).\n\n```mermaid\nsequenceDiagram\n participant B as Browser\n participant S as Your handler\n B->>S: Request<br/>Cookie: theme=dark\n Note over S: req.cookie('theme') -> \"dark\"\n S-->>B: Response<br/>Set-Cookie: theme=light; ...\n Note over B: browser stores the new cookie<br/>and sends it next time\n```\n\nThe cookie types (`Cookie`, `Cookies`, `CookieMap`, `SecureCookies`, and the `SameSite` / `CookieEncoding` / `CookiePrefix` enums) are **ambient globals**: use them with no import, like `crypto`. They are also exported from `toiljs/server/runtime` if you prefer an explicit import.\n\n## Reading incoming cookies\n\nOn the `Request`, two methods cover almost everything:\n\n```ts\nconst theme = req.cookie('theme'); // one value: string | null\nconst jar = req.cookies(); // all of them, parsed once and cached\njar.get('theme'); // \"dark\", or null\njar.has('theme'); // true / false\n```\n\n`req.cookies()` returns a `CookieMap`, an ordered name-to-value view. It is parsed once per request and cached, so calling it repeatedly is cheap.\n\n## Setting a cookie on a response\n\nBuild a cookie with the fluent `Cookie` builder, then attach it with `setCookie`. Every setter returns the cookie, so calls chain:\n\n```ts\nimport { Response } from 'toiljs/server/runtime';\n\nreturn Response.json('{\"ok\":true}').setCookie(\n Cookie.create('theme', 'dark')\n .path('/')\n .maxAge(60 * 60 * 24 * 365) // one year, in seconds\n .sameSite(SameSite.Lax)\n .secure(),\n);\n```\n\nShorthands for the common cases:\n\n```ts\nresp.setCookieKV('theme', 'dark'); // name=value, no attributes\nresp.clearCookie('theme'); // delete it (empty value, expired)\n```\n\nEach `setCookie` adds its own `Set-Cookie` header; cookies are never folded into one header.\n\n### The attributes you will actually use\n\n| Method | Sets | What it means |\n| --- | --- | --- |\n| `path('/')` | `Path` | Which URL paths the cookie applies to. `/` means the whole site. |\n| `maxAge(seconds)` | `Max-Age` | How long the browser keeps it. `0` or negative deletes it now. |\n| `secure()` | `Secure` | Only send over HTTPS. Use it for anything that matters. |\n| `httpOnly()` | `HttpOnly` | Hide it from JavaScript in the browser (blocks theft via XSS). Use it for session cookies. |\n| `sameSite(SameSite.Lax)` | `SameSite` | Controls whether the cookie is sent on cross-site requests. `Lax` is a good default; `Strict` is tighter; `None` allows cross-site (and forces `Secure`). |\n\nYou rarely need the rest, but they exist: `domain`, `expires` (an absolute time instead of a duration), `partitioned` (CHIPS), `priority`, and `extension` for anything custom.\n\n## The `__Host-` and `__Secure-` prefixes\n\nTwo special name prefixes give the browser extra guarantees. They are not magic strings you invent; browsers recognize them and **refuse** to accept a cookie that carries the prefix without meeting its rules.\n\n- **`__Secure-`**: the cookie must be `Secure` (HTTPS only).\n- **`__Host-`**: the cookie must be `Secure`, have `Path=/`, and have **no** `Domain`. This locks the cookie to exactly your host, so a sibling subdomain cannot set or read it. It is the strongest option and the right choice for session cookies.\n\nYou do not spell the prefix yourself. Two helpers apply it and enforce the rules for you:\n\n```ts\nCookie.create('sid', 'abc123')\n .httpOnly()\n .sameSite(SameSite.Lax)\n .maxAge(3600)\n .asHostPrefixed(); // prepends __Host-, forces Secure + Path=/ + no Domain\n```\n\n`asSecurePrefixed()` is the lighter version (prepends `__Secure-` and forces `Secure`).\n\n> Under `toiljs dev`, browsers treat `http://localhost` as a secure context, so `Secure` and `__Host-` cookies work over plain HTTP locally. You do not need HTTPS to test them.\n\n## Worked example: a theme preference cookie\n\nA tiny handler that reads a `theme` cookie and lets the user flip it. This is the canonical \"remember a preference\" pattern: a plain, non-secret value.\n\n```ts\nimport { Response, RouteContext } from 'toiljs/server/runtime';\n\n@rest('prefs')\nclass Prefs {\n // GET /prefs/theme -> current theme (defaults to \"light\")\n @get('/theme')\n read(ctx: RouteContext): Response {\n const theme = ctx.request.cookie('theme');\n return Response.text((theme != null ? theme : 'light') + '\\n');\n }\n\n // POST /prefs/theme/dark -> remember \"dark\" for a year\n @post('/theme/dark')\n setDark(ctx: RouteContext): Response {\n return Response.text('saved\\n').setCookie(\n Cookie.create('theme', 'dark')\n .path('/')\n .maxAge(60 * 60 * 24 * 365)\n .sameSite(SameSite.Lax)\n .secure(),\n );\n }\n}\n```\n\nA preference like this does not need to be secret or tamper-proof (the worst a user can do is change their own theme), so a plain cookie is fine. When the value **does** matter, reach for `SecureCookies`.\n\n## Secure cookies: signing and encryption\n\nSometimes a cookie value must not be forged or read by the user. `SecureCookies` covers both, built on the `crypto` global (no extra setup):\n\n- **Signed** (`SecureCookies.signed(key)`): the value stays readable but is stamped with a signature (HMAC-SHA256) bound to the cookie's name. The user can see it but cannot change it or move it to another cookie without the signature failing. Use this for a value the client may read but must not tamper with.\n- **Encrypted** (`SecureCookies.encrypted(key)`): the value is scrambled (AES-GCM) so the user cannot read **or** change it. Use this for something confidential.\n\nThe `key` is raw secret bytes you supply (load it from a secret via [`Environment.getSecure`](./environment.md), never hard-code it):\n\n```ts\n// In a real app, load this from a secret, do not inline it.\nconst key = Uint8Array.wrap(String.UTF8.encode('0123456789abcdef0123456789abcdef'));\n\n// Signed: readable but tamper-proof.\nconst signer = SecureCookies.signed(key);\nconst sealed = signer.sign('session', 'user-42');\nconst user = signer.unsign('session', sealed); // \"user-42\", or null if tampered\n\n// Encrypted: unreadable and authenticated.\nconst box = SecureCookies.encrypted(key);\nresp.setCookie(box.seal(Cookie.create('secret', 'top-secret').httpOnly()));\nconst secret = box.open(req.cookies(), 'secret'); // \"top-secret\", or null\n```\n\nTwo important safety properties:\n\n- **Verification never crashes.** `unsign` and `decrypt` return `null` on a tampered, truncated, or wrong-key value; they never throw. That makes them safe to call directly on attacker-controlled input.\n- **Values are bound to the cookie name.** A sealed value made for cookie `a` will not verify or decrypt under cookie `b`.\n\nFor HMAC keys, use 32 or more bytes. For AES, the key must be exactly 16 or 32 bytes (a wrong length is rejected immediately). You can rotate keys by sealing with a new key while still accepting an old one:\n\n```ts\nconst signer = SecureCookies.signed(newKey).addKey(oldKey); // seal with new, open with either\n```\n\n## Encoding is not encryption\n\nOne common mix-up. **Encoding** (the `CookieEncoding` on a `Cookie`, default percent-encoding) only makes a value safe to put on the wire; anyone can reverse it. **Signing / encryption** (`SecureCookies`) is the cryptographic protection and needs a secret key. If you want a value the user cannot forge, you need `SecureCookies`, not a fancier encoding.\n\n## Relationship to auth session cookies\n\nYou do not manage login cookies by hand. The [auth system](../auth/usage.md) sets and reads its own hardened, `__Host-` prefixed, signed session cookie for you, and enforces access with the `@auth` decorator. This page is for **your own** cookies (preferences, flags, small bits of state). If you find yourself building a login cookie from scratch, use auth instead; it already does the hard, security-sensitive parts correctly.\n\n## Advanced reference: `Cookie` and `Cookies` helpers\n\nMost handlers only need the builders above. These extra members are here for lower-level work: validating a cookie before you send it, parsing a `Set-Cookie` header back into a `Cookie` (for a proxy or a test), or controlling the exact wire encoding.\n\n### More `Cookie` methods\n\n| Method | Returns | What it does |\n| --- | --- | --- |\n| `validate()` | `CookieValidation` | Check the cookie against RFC 6265bis (name is a token, name+value within the 4096-byte cap, `Path` form, prefix rules, `SameSite=None` / `Partitioned` imply `Secure`, the 400-day lifetime cap) **without** sending it. Never throws. |\n| `serialize(strict)` | `string` | Build the `Set-Cookie` value. Lenient by default (always emits a best-effort cookie); pass `serialize(true)` to **throw** on a hard violation instead. `setCookie(...)` calls this for you. |\n| `withEncoding(enc)` | `Cookie` | Choose how the value goes on the wire: `CookieEncoding.Percent` (default), `.Raw`, or `.Base64Url`. Chains like the other setters. |\n| `detectedPrefix()` | `CookiePrefix` | Report which special prefix the name carries (`CookiePrefix.Host`, `.Secure`, or `.None`), detected case-insensitively. |\n| `encodedValue()` | `string` | The value after the chosen encoding is applied, exactly as it will appear on the wire. |\n| `expiresRaw(date)` | `Cookie` | Set `Expires` to a verbatim date string (an escape hatch; it is **not** validated, unlike `expires(epochSeconds)`). |\n\n`validate()` returns a `CookieValidation`, a small result object:\n\n- `valid: bool` is `true` when there were no problems.\n- `errors: Array<string>` holds one human-readable message per problem (empty when valid).\n\n```ts\n// A __Host- cookie must be Secure with Path=/; here we forgot both.\nconst c = Cookie.create('__Host-sid', 'abc');\nconst check = c.validate();\nif (!check.valid) {\n // check.errors includes \"__Host- prefix requires the Secure attribute\"\n // (asHostPrefixed() would have set those attributes for you)\n}\n```\n\n### `Cookies` static helpers\n\n`Cookies` is the read side, plus a couple of codec shortcuts. Every method is static, so call them as `Cookies.xxx(...)`.\n\n| Call | Returns | What it does |\n| --- | --- | --- |\n| `Cookies.parse(header)` | `CookieMap` | Parse a request `Cookie` header (`a=1; b=2`) into a name-to-value map. Values are percent-decoded; malformed pairs are skipped, never thrown. |\n| `Cookies.get(header, name)` | `string \\| null` | Shorthand: parse `header` and return one value (or `null`). |\n| `Cookies.serialize(name, value)` | `string` | One-shot `Set-Cookie` value for `name=value` with no attributes. For attributes, build a `Cookie` and call `serialize()`. |\n| `Cookies.parseSetCookie(header)` | `Cookie` | Parse a `Set-Cookie` field value back into a `Cookie` (handy for proxies or tests). The value is kept verbatim (`CookieEncoding.Raw`) so re-serializing reproduces the original. |\n| `Cookies.encodeValue(raw)` | `string` | Percent-encode a value the way the default `Cookie` encoding would. |\n| `Cookies.decodeValue(enc)` | `string` | Percent-decode a value (the inverse of `encodeValue`). |\n\n```ts\n// Round-trip a Set-Cookie header (for example, inspecting an upstream response in a proxy):\nconst cookie = Cookies.parseSetCookie('sid=abc123; Path=/; HttpOnly; SameSite=Lax');\ncookie.name; // \"sid\"\ncookie.serialize(); // \"sid=abc123; Path=/; SameSite=Lax; HttpOnly\"\n```\n\n## Gotchas\n\n- **Read on the `Request`, write on the `Response`.** Setting a cookie does not change what `req.cookie(...)` returns for the current request; it takes effect on the browser's next request.\n- **`maxAge` is in seconds.** `maxAge(3600)` is one hour, not one millisecond.\n- **To delete a cookie, the `path` (and `domain`) must match** the ones you set it with. Use `clearCookie(name, path, domain)`.\n- **A `Set-Cookie` opts a response out of edge caching.** By design, a response that sets a cookie is never edge-cached, because a cookie is per-user state. See [Caching](./caching.md).\n- **Do not put secrets in a plain cookie value.** Use `SecureCookies.encrypted(...)`, and load the key from a [secret](./environment.md).\n\n## Related\n\n- [Auth, sessions, and `@user`](../auth/usage.md): the built-in, hardened session cookie.\n- [Crypto](./crypto.md): the primitives under `SecureCookies`.\n- [Environment and secrets](./environment.md): where to store your signing / encryption key.\n- [Caching](./caching.md): why setting a cookie disables edge caching.\n",
66
+ "services/crypto.md": "# Crypto\n\nA small, safe cryptography toolkit (hashing, random bytes, HMAC, symmetric encryption, key derivation, and signatures) available with no import through the ambient `crypto` global.\n\nReach for `crypto` to fingerprint a value (hashing), generate an unguessable token or ID (random bytes), prove a value has not been tampered with (HMAC or a signature), or encrypt data you store yourself (AES). Skip it for **login passwords**: use [auth](../auth/README.md), which is purpose-built and does the hard parts for you. `crypto` is also the engine under [signed cookies](./cookies.md) and auth, so most apps use it indirectly without ever calling it.\n\n## The shape of the API\n\n`crypto` mirrors the browser Web Crypto API, with one big difference: **there are no Promises**. ToilScript (the language your backend is written in) has no `async`, so every call returns its result **directly**, right away.\n\n```ts\nconst digest = crypto.sha256Text('hello'); // Uint8Array, no await\nconst id = crypto.randomUUID(); // string, no await\n```\n\nThere are two layers:\n\n- **`crypto.*`**: ergonomic one-call helpers (hash this string, give me a UUID, HMAC these bytes). Start here.\n- **`crypto.subtle`**: the full primitive surface (import a key, encrypt, sign, derive bits) for when you need more control.\n\nBoth are ambient globals (no import). The small helper classes and the `ALG_*` / `FMT_*` / `USAGE_*` / `CURVE_*` constants you pass to `subtle` are imported from the `'crypto'` module.\n\n## Quick helpers (`crypto.*`)\n\nEvery helper is synchronous and returns its value directly.\n\n| Helper | Signature | What it does |\n| --- | --- | --- |\n| `getRandomValues` | `(array: Uint8Array): void` | Fill the array with cryptographically strong random bytes. |\n| `randomUUID` | `(): string` | A random RFC 4122 v4 UUID string. |\n| `sha1` / `sha256` / `sha384` / `sha512` | `(data: Uint8Array): Uint8Array` | Hash raw bytes. |\n| `sha1Text` … `sha512Text` | `(s: string): Uint8Array` | UTF-8 encode the string, then hash. |\n| `hmacSha256` | `(key: Uint8Array, msg: Uint8Array): Uint8Array` | Keyed fingerprint (HMAC-SHA-256) of bytes. |\n| `hmacSha256Text` | `(key: Uint8Array, msg: string): Uint8Array` | HMAC-SHA-256 over a string. |\n| `toHex` | `(bytes: Uint8Array): string` | Lowercase hex string (for display or storage). |\n| `subtle` | `SubtleCrypto` | The full primitive surface (below). |\n\n**Hashing** turns any input into a fixed-size fingerprint. The same input always gives the same output, and you cannot work backward from the fingerprint to the input. Use it to compare or index a value without storing the original (for example, keying a cache by the hash of a URL).\n\n**Random bytes** come from a CSPRNG (a cryptographically secure random generator), which means the output is unpredictable and safe for tokens, session IDs, and IVs. Never use `Math.random()` for anything security-related.\n\n**HMAC** is a fingerprint computed *with a secret key*. Anyone can hash a value, but only someone who holds the key can produce the correct HMAC, so it proves a value came from you and was not altered. This is exactly what [`TwoFactor`](./email.md) tokens and [signed cookies](./cookies.md) use.\n\n## Worked example: hash a value and generate a token\n\n```ts\nimport { RouteContext } from 'toiljs/server/runtime';\n\n@rest('demo')\nclass Demo {\n @get('/')\n public example(ctx: RouteContext): string {\n // Hash a value to a hex fingerprint (safe to log or use as a key).\n const digest = crypto.sha256Text('alice@example.com');\n const fingerprint = crypto.toHex(digest); // 64 hex chars\n\n // Generate a 128-bit unguessable token as hex.\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n const token = crypto.toHex(bytes); // 32 hex chars\n\n // A ready-made unique id.\n const id = crypto.randomUUID();\n\n return `fp=${fingerprint} token=${token} id=${id}`;\n }\n}\n```\n\nTo fingerprint a value *with a secret* (so only your server can produce or check it), use HMAC:\n\n```ts\nconst key = Uint8Array.wrap(String.UTF8.encode(Environment.getSecure('SIGNING_KEY')!));\nconst mac = crypto.hmacSha256Text(key, 'order:42');\nconst macHex = crypto.toHex(mac);\n```\n\n## The primitive surface (`crypto.subtle`)\n\nUse `subtle` when the helpers are not enough: symmetric encryption, signatures, or key derivation. Every method is synchronous.\n\n| Method | Signature |\n| --- | --- |\n| `digest` | `digest(algorithm: i32, data: Uint8Array): Uint8Array` |\n| `importKey` | `importKey(format: i32, keyData: Uint8Array, algorithm: AlgorithmParams, extractable: bool, usages: i32): CryptoKey` |\n| `exportKey` | `exportKey(format: i32, key: CryptoKey): Uint8Array` |\n| `encrypt` | `encrypt(algorithm: AlgorithmParams, key: CryptoKey, data: Uint8Array): Uint8Array` |\n| `decrypt` | `decrypt(algorithm: AlgorithmParams, key: CryptoKey, data: Uint8Array): Uint8Array` |\n| `sign` | `sign(algorithm: AlgorithmParams, key: CryptoKey, data: Uint8Array): Uint8Array` |\n| `verify` | `verify(algorithm: AlgorithmParams, key: CryptoKey, signature: Uint8Array, data: Uint8Array): bool` |\n| `deriveBits` | `deriveBits(algorithm: AlgorithmParams, baseKey: CryptoKey, length: i32): Uint8Array` |\n| `deriveKey` | `deriveKey(algorithm, baseKey, lengthBits, derivedKeyAlgorithm, extractable, usages): CryptoKey` |\n\nTwo things differ from the browser API, both because of the missing-Promise design:\n\n- **`algorithm` and `format` are integer constants, not strings.** You pass `ALG_SHA_256` (not `\"SHA-256\"`) and `FMT_RAW` (not `\"raw\"`). The constants are listed below.\n- **`verify` returns a `bool`.** A signature mismatch returns `false` (it does not throw). A real error, like an invalid key, does throw.\n\n### Keys are per-request handles\n\nYou never hold raw key bytes in your code for long. `importKey` gives you back a **`CryptoKey`**, which is an opaque handle into a host-side keystore. That keystore is **wiped when the request ends**, so a `CryptoKey` is valid only within the request that created it. Never cache one across requests; import it again each time.\n\n```mermaid\nsequenceDiagram\n participant G as Your handler\n participant H as Host keystore (per request)\n G->>H: importKey(...) -> a CryptoKey handle\n G->>H: sign(params, key, data)\n H-->>G: signature bytes\n Note over H: the keystore is wiped when the request ends\n```\n\n### Algorithm parameter classes\n\nEach algorithm takes a small parameters object you build and pass in. Import the class you need from `'crypto'`:\n\n```ts\nimport { AesGcmParams, HmacImportParams, ALG_SHA_256, USAGE_ENCRYPT, USAGE_DECRYPT } from 'crypto';\n```\n\n| Class | Constructor | Used for |\n| --- | --- | --- |\n| `AesGcmParams` | `(iv, additionalData?, tagLength = 128)` | AES-GCM encrypt/decrypt |\n| `AesCbcParams` | `(iv)` | AES-CBC |\n| `AesCtrParams` | `(counter, length = 128)` | AES-CTR |\n| `HmacImportParams` | `(hash)` | importing an HMAC key |\n| `HmacParams` | `()` | HMAC sign/verify (hash comes from the key) |\n| `Pbkdf2Params` | `(hash, salt, iterations)` | deriving a key from a password |\n| `HkdfParams` | `(hash, salt, info?)` | deriving a key from key material |\n| `EcdsaParams` | `(hash)` | ECDSA sign/verify |\n| `EcKeyImportParams` | `(alg, namedCurve)` | importing an EC key |\n| `Ed25519Params` | `()` | Ed25519 sign/verify |\n| `X25519ImportParams` | `()` | importing an X25519 key |\n| `EcdhParams` | `(alg, publicKeyHandle)` | ECDH / X25519 key agreement |\n\n### Constants\n\n- **Hashes and algorithms:** `ALG_SHA_1`, `ALG_SHA_256`, `ALG_SHA_384`, `ALG_SHA_512`, `ALG_SHA3_256`, `ALG_SHA3_384`, `ALG_SHA3_512`, `ALG_AES_GCM`, `ALG_AES_CBC`, `ALG_AES_CTR`, `ALG_AES_KW`, `ALG_HMAC`, `ALG_ECDSA`, `ALG_ED25519`, `ALG_ECDH`, `ALG_X25519`, `ALG_HKDF`, `ALG_PBKDF2`.\n- **Key formats:** `FMT_RAW`, `FMT_PKCS8`, `FMT_SPKI`. (`FMT_JWK` is rejected.)\n- **Key usages (a bitmask, OR them together):** `USAGE_ENCRYPT`, `USAGE_DECRYPT`, `USAGE_SIGN`, `USAGE_VERIFY`, `USAGE_DERIVE_KEY`, `USAGE_DERIVE_BITS`, `USAGE_WRAP_KEY`, `USAGE_UNWRAP_KEY`.\n- **Named curves:** `CURVE_P256`, `CURVE_P384`. (`CURVE_P521` is not supported.)\n\nThe `digest` selector also accepts the SHA3 constants, so `crypto.subtle.digest(ALG_SHA3_256, data)` works even though there is no `sha3` quick helper.\n\n### `CryptoKey`\n\nA `CryptoKey` is a handle plus metadata: `handle: i32`, `type: string` (`\"secret\"`, `\"public\"`, or `\"private\"`), `extractable: bool`, `algorithm: i32`, and `usages: i32`, with `algorithmName()` and `hasUsage(u)` helpers. Remember it is only valid within the current request.\n\n## Example: AES-256-GCM encrypt and decrypt\n\nAES-GCM is authenticated encryption: it both hides the data and detects tampering. It needs a 32-byte key and a fresh 12-byte IV (a \"number used once\"). **Never reuse an IV with the same key**; generate a new one every time.\n\n```ts\nimport { AesGcmParams, ALG_AES_GCM, FMT_RAW, USAGE_ENCRYPT, USAGE_DECRYPT } from 'crypto';\n\n// 32-byte key and a fresh 12-byte IV.\nconst rawKey = new Uint8Array(32); crypto.getRandomValues(rawKey);\nconst iv = new Uint8Array(12); crypto.getRandomValues(iv);\n\n// Import the key once; grant it both usages so it can round-trip.\nconst key = crypto.subtle.importKey(\n FMT_RAW, rawKey, new AesGcmParams(iv), false, USAGE_ENCRYPT | USAGE_DECRYPT,\n);\n\nconst plaintext = Uint8Array.wrap(String.UTF8.encode('secret note'));\nconst ciphertext = crypto.subtle.encrypt(new AesGcmParams(iv), key, plaintext);\nconst recovered = crypto.subtle.decrypt(new AesGcmParams(iv), key, ciphertext);\n// recovered == plaintext\n```\n\nStore the `iv` next to the ciphertext (it is not secret); you need the same IV to decrypt.\n\n## Example: HMAC with `subtle`\n\nThe quick `crypto.hmacSha256` helper does this in one line, but here is the explicit form, which mirrors how [`TwoFactor`](./email.md) signs its tokens:\n\n```ts\nimport { HmacImportParams, HmacParams, ALG_SHA_256, FMT_RAW, USAGE_SIGN, USAGE_VERIFY } from 'crypto';\n\nconst key = crypto.subtle.importKey(\n FMT_RAW, rawKeyBytes, new HmacImportParams(ALG_SHA_256), false, USAGE_SIGN | USAGE_VERIFY,\n);\n\nconst mac = crypto.subtle.sign(new HmacParams(), key, message);\nconst ok: bool = crypto.subtle.verify(new HmacParams(), key, mac, message); // true\n\n// verify compares in constant time and returns false on a mismatch (it does not throw).\n```\n\n## Limitations\n\n- **No Promises.** Every call is synchronous.\n- **No RSA.** It was dropped because the only pure-Rust implementation had an unfixable timing weakness. Use ECDSA or Ed25519 for signatures.\n- **No JWK key format.** Use `FMT_RAW`, `FMT_PKCS8`, or `FMT_SPKI`.\n- **No on-host key generation.** Keys are always **imported**, never generated on the server. Generate them elsewhere and import the bytes.\n- **No P-521.** `CURVE_P256` and `CURVE_P384` are supported.\n- **Keys do not survive the request.** A `CryptoKey` is a per-request handle; re-import each request.\n- **Every call is metered.** Crypto operations cost compute (charged up front from the sizes involved), so an over-budget call fails cleanly instead of burning CPU.\n\n### Post-quantum signature verify\n\nAuth's login uses a post-quantum signature scheme (ML-DSA) that the host can **verify** (it never holds a private key). This underpins the [auth login stack](../auth/how-it-works.md); you reach it through `AuthService`, not through `crypto` directly, so it is documented there rather than here.\n\n## Related\n\n- [Auth: how it works](../auth/how-it-works.md), the post-quantum login stack built on host-side verify.\n- [Cookies](./cookies.md), signed and encrypted cookies built on `crypto`.\n- [Email](./email.md), whose `TwoFactor` codes are HMAC tokens.\n- [Environment and secrets](./environment.md), where you keep signing and encryption keys.\n",
67
+ "services/email.md": "# Email\n\nSend transactional email (verification codes, password resets, receipts) straight from a route handler.\n\nReach for email when your app needs to message one user because of something they did: confirm a signup, send a 2FA code, mail a receipt. It is built for **transactional** email (one message, triggered by an action), not bulk marketing or newsletters. There is no attachment support and no per-recipient dynamic content beyond simple placeholders (see [gotchas](#gotchas-and-limits)).\n\nEverything here is an **ambient global**: an object you can use without importing it, exactly like [`crypto`](./crypto.md) or `Environment`. A backend that never sends email pulls none of this into its compiled program.\n\n- **`EmailService`** sends one email.\n- **`EmailTemplate`** is a reusable message with `{{placeholder}}` holes (plain text and/or HTML).\n- **`emails/*.tsx`** lets you author emails as React components; the build turns each into a typed `Emails.<Name>.send(...)`.\n- **`TwoFactor`** issues and checks email verification codes with no database.\n\n## How a send works\n\nWhen your handler calls `EmailService.send(...)`, it does not talk to the email provider itself. The **edge** (the Dacely server running your code) validates the recipient, checks your sending limits, then hands the message to a single background \"mailer\" thread that talks to the provider. Your handler **suspends** (pauses) until the mailer reports a result, so a slow provider never freezes the worker that is serving other requests.\n\n```mermaid\nsequenceDiagram\n participant H as Your handler (wasm)\n participant E as Edge host\n participant M as Off-core mailer\n participant P as Email provider\n H->>E: EmailService.send(to, subject, body, purpose, html)\n E->>E: validate recipient, check caps + dedup\n E->>M: hand off the message\n Note over H: your handler pauses here\n M->>P: deliver over the network\n P-->>M: accepted / rejected\n M-->>E: status\n E-->>H: EmailStatus (Sent, Budget, ...)\n```\n\n## Send one email\n\n`EmailService.send` takes the recipient, a subject, a plain-text body, a short `purpose` tag, and an optional HTML body. It returns an `EmailStatus` telling you what happened.\n\n```ts\nimport { Response, RouteContext } from 'toiljs/server/runtime';\n\n@rest('notify')\nclass Notify {\n @post('/welcome')\n welcome(ctx: RouteContext): Response {\n const status = EmailService.send(\n 'alice@example.com', // to: exactly one address\n 'Welcome!', // subject\n 'Thanks for signing up.', // plain-text body\n 'welcome', // purpose tag (used for dedup + abuse limits)\n '<h1>Thanks for signing up.</h1>', // optional HTML body\n );\n\n return status == EmailStatus.Sent\n ? Response.text('sent\\n')\n : Response.text('could not send\\n', 503);\n }\n}\n```\n\nThe full signature is `send(to, subject, body, purpose = 'tx', html = '')`. Pass a non-empty `html` to send an HTML message; then `body` is the plain-text fallback (set both for the best deliverability, or leave `body` empty for HTML-only).\n\n`EmailStatus` is a global enum, so you can compare against it (`status == EmailStatus.Sent`) with no import. `Sent` and `Deduped` mean success; the rest tell you why the message was not delivered and whether retrying could help.\n\n| Status | Meaning | Retry? |\n| --- | --- | --- |\n| `Sent` | Accepted by the provider. | done |\n| `Deduped` | An identical recent `(recipient, purpose)` send was collapsed into one. | treat as sent |\n| `Budget` | Your per-minute (or per-day) send budget is exhausted. | yes, later |\n| `TryLater` | The mailer was momentarily saturated. | yes, back off |\n| `RecipientCapped` | This recipient hit their hourly cap. | no (this window) |\n| `BadRecipient` | The address failed validation (multiple addresses, control characters). | no |\n| `Disabled` | This site has no email configured. | no |\n| `ProviderError` | The provider rejected it, or delivery failed after retries. | no |\n\nThe `purpose` is a short, non-sensitive tag like `\"welcome\"` or `\"reset\"`. The edge folds it into two things: a **dedup key** (an identical `(site, recipient, purpose)` within about 30 seconds becomes one send, which returns `Deduped`) and the **abuse counters**. It is never logged in the clear, so keep real user data out of it.\n\nThe recipient is checked on the host: exactly one address, no carriage returns, line feeds, or angle brackets. That means a bad or hostile input can never smuggle a second recipient or inject an email header.\n\n## Configure email\n\nEmail is **off by default**. A site that has not configured it gets `Disabled` from every send. You configure it in two places: non-secret settings in `toil.config.ts`, and the provider credential as a **secret** in the [environment store](./environment.md) (never in your code or your compiled program).\n\n```ts\n// toil.config.ts\nimport { defineConfig } from 'toiljs/compiler';\n\nexport default defineConfig({\n server: {\n email: {\n provider: 'resend', // 'resend' | 'gmail' | 'smtp'\n from: 'you@example.com', // the From address (validated; single address)\n maxPerMin: 60, // per-site cap: at most 60 sends per rolling minute\n },\n },\n});\n```\n\nThe credential lives in your secrets file as a reserved `TOIL_EMAIL_*` key. These keys are **host-only**: the edge reads them in Rust, and they are stripped out of the buckets your code can read, so a tenant can never fish the API key out with `Environment.getSecure`.\n\n```bash\n# .env.secrets (gitignored; never committed, never in the .wasm)\nTOIL_EMAIL_API_KEY=re_xxxxxxxxxxxx\n```\n\nOn the production edge the same keys live in the site's secrets file (mode `0600`, kept out of the config directory the file watcher sees), so a credential is never written to a log, to `/_admin`, or into your deployed module. See [Environment and secrets](./environment.md) for how the store works.\n\n### Providers\n\nYou pick one provider. Each maps `TOIL_EMAIL_API_KEY` to the right credential.\n\n| Provider | `provider` | What `TOIL_EMAIL_API_KEY` holds | Extra keys |\n| --- | --- | --- | --- |\n| **Resend** | `resend` | A Resend API key (a JSON API). | none |\n| **Gmail** | `gmail` | A Gmail **App Password** (SMTP over `smtp.gmail.com:587`). | none |\n| **Generic SMTP** | `smtp` | The SMTP password. | `TOIL_EMAIL_SMTP_HOST` (required), optional `TOIL_EMAIL_SMTP_PORT` (defaults `587`), `TOIL_EMAIL_SMTP_USER` (defaults to `from`) |\n\nFor Gmail you need 2-Step Verification on the account, then create an App Password at `https://myaccount.google.com/apppasswords`. The username is your `from` address. For generic SMTP, port `587` uses STARTTLS and port `465` uses implicit TLS.\n\nEvery setting can also be given as a `TOIL_EMAIL_*` environment variable, and those override the `toil.config.ts` values. That means the exact same secrets file works in local dev and on the edge.\n\n### Local dev behavior\n\n`toiljs dev` runs the **full email pipeline** in Node: recipient validation, dedup, and every cap behave exactly like the edge. Once you set a provider plus `TOIL_EMAIL_API_KEY`, dev **really sends** (Resend over its API, Gmail/SMTP over nodemailer). If you have **not** configured a provider, `EmailService.send` becomes a log-only mock that returns `Sent`, so a signup flow that emails a code still works end to end without any setup.\n\n> One dev-only nuance: the dev server runs your code synchronously, so the actual network send is fire-and-forget. Validation and the caps return their exact status immediately, but a `Sent` is optimistic; the real delivery outcome (or a `ProviderError`) is logged, not returned. The programming interface is identical to the edge, so code that runs in dev runs unchanged in production.\n\n## Templates with `{{placeholders}}`\n\nWhen you send the same shape of email with different values, define an `EmailTemplate` once. `{{name}}` holes are filled from a `Map` at send time.\n\n```ts\nconst welcome = new EmailTemplate(\n 'Welcome, {{name}}!', // subject\n 'Hi {{name}}, your code is {{code}}.', // plain-text body\n '<h1>Welcome, {{name}}</h1><p>Code: <b>{{code}}</b></p>', // html (optional)\n);\n\nconst vars = new Map<string, string>();\nvars.set('name', 'Alice');\nvars.set('code', '123456');\n\nconst status = welcome.send('alice@example.com', vars, 'welcome');\n```\n\n- `{{ name }}` with surrounding spaces is accepted; an unknown placeholder renders to an empty string.\n- Omit the third argument for a plain-text-only template.\n- `template.render(vars)` returns the rendered `{ subject, body, html }` **without** sending, which is handy for previews and tests.\n\nFor anything richer than token substitution (real layout, brand styling), author the email as a React component instead.\n\n## React email templates\n\nYou can write emails as React components in an **`emails/`** folder. At `toiljs build` each one is rendered **once, at build time**, into a static string of HTML with inline styles, and its props become `{{token}}` holes. The build then generates a typed `Emails.<Name>.send(...)` for your server to call.\n\nWhy build-time? Email clients (Gmail, Outlook, Apple Mail) run **no JavaScript** and strip `<style>` blocks and external CSS. So an HTML email has to be a finished, inline-styled string. Rendering it once at build gives you React ergonomics while shipping the inbox exactly what it can display.\n\n```tsx\n// emails/Welcome.tsx\nexport const subject = 'Welcome, {{name}}!';\n\nexport default function Welcome({ name, code }: { name: string; code: string }) {\n return (\n <table\n width=\"100%\"\n style={{ fontFamily: 'Arial, sans-serif' }}>\n <tbody>\n <tr>\n <td style={{ padding: '24px' }}>\n <h1 style={{ color: '#111' }}>Welcome, {name}!</h1>\n <p>\n Your code is <b>{code}</b>.\n </p>\n </td>\n </tr>\n </tbody>\n </table>\n );\n}\n```\n\nThe generated `Emails.Welcome.send(...)` takes the recipient, then one argument per `{{token}}` **in alphabetical order**, then an optional `purpose`:\n\n```ts\n// Welcome.tsx uses {{code}} and {{name}} -> params are (code, name), alphabetical\nconst status = Emails.Welcome.send('alice@example.com', '123456', 'Alice');\n```\n\nAuthoring notes:\n\n- **Styles must end up inline.** Write inline `style={{ ... }}`, or import a stylesheet and its rules are inlined into each element for you at build. A bare CSS import on its own has no effect on the rendered email.\n- **Optional exports:** `subject` (a token template; defaults to the file name), `text` (a plain-text alternative; otherwise derived from the HTML), and `purpose`.\n- **Field substitution only.** Because the component renders once at build time, a runtime `{items.map(...)}` bakes in at build; it does not re-run per recipient. That is perfect for verification, confirmation, and receipt emails; a truly dynamic list needs a different approach.\n\nWhile `toiljs dev` runs, open **`/__toil/emails`** (the dev banner prints the link) to preview every `emails/*.tsx`, fill each `{{token}}`, and toggle the HTML and plain-text parts. It refreshes as you edit.\n\n## Verification codes with `TwoFactor`\n\n`TwoFactor` issues and checks short numeric codes (for 2FA, email confirmation, or magic-code login) with **no database**. That is possible because it is **stateless**: instead of storing the code server-side, it emails the code and returns a signed **token** that commits to the code using [HMAC](./crypto.md) (a keyed fingerprint). The code itself is only ever in the email; the token holds a fingerprint of it, not the code.\n\nTo verify, the server recomputes the fingerprint from the token plus the code the user typed and compares them in constant time. A valid `(token, code)` pair can only come from someone who **both** received the email and holds the token, and the fingerprint binds the recipient, the purpose, and an expiry so a token cannot be replayed for another address, another flow, or after it expires.\n\n```ts\n// 1. Issue and email a code. Hand the returned token to the client\n// (a cookie or a hidden form field).\nconst challenge = TwoFactor.send('alice@example.com', 'login');\n// challenge.token -> give this to the client\n// challenge.status -> the EmailStatus of the send (check it was Sent/Deduped)\n\n// 2. Later, when the user submits the code they received:\nconst ok: bool = TwoFactor.verify(challenge.token, 'alice@example.com', userEntered);\nif (ok) {\n // the code is valid and unexpired\n}\n```\n\nThe three entry points:\n\n- **`send(recipient, purpose, ttlSecs = 600, digits = 6)`** issues a code, emails it with a built-in template, and returns `{ token, status }`.\n- **`issue(recipient, purpose, ttlSecs, digits)`** returns `{ code, token }` **without** sending, so you can email `code` yourself with a branded `EmailTemplate` or `Emails.*` message.\n- **`verify(token, recipient, code)`** returns `true` only for a code issued for that recipient that has not expired.\n\nOne-time setup: call **`TwoFactor.setSecret(secret)`** once at startup in `main.ts`. This is the HMAC key that signs the tokens. It must be identical on every edge instance and must never end up in a client bundle. (It is separate from your email provider key.)\n\n> **Important limitation: not single-use.** `TwoFactor` gives you integrity and expiry, but because it stores no state, a valid code verifies **repeatedly** until its TTL runs out. Keep the TTL short. If you need true single-use, record a per-recipient \"last verified at\" (in your database or the edge store) and reject any code at or before it.\n\n## Worked example: welcome email plus a 2FA code\n\nA signup route that emails a welcome message and starts a 2FA challenge in one handler:\n\n```ts\nimport { Response, RouteContext } from 'toiljs/server/runtime';\n\n@rest('signup')\nclass Signup {\n @post('/start')\n start(ctx: RouteContext): Response {\n const email = ctx.query('email'); // in real code, validate this first\n\n // 1. Friendly welcome (fire it, but note the status for logging).\n Emails.Welcome.send(email, '123456', 'Alice');\n\n // 2. Start a 2FA challenge; the code is emailed, the token comes back.\n const challenge = TwoFactor.send(email, 'signup');\n if (challenge.status != EmailStatus.Sent && challenge.status != EmailStatus.Deduped) {\n return Response.text('could not send code\\n', 503);\n }\n\n // Hand the token to the client (here, a JSON field; a cookie also works).\n return Response.json(`{\"token\":${JSON.stringify(challenge.token)}}`);\n }\n\n @post('/confirm')\n confirm(ctx: RouteContext): Response {\n const email = ctx.query('email');\n const token = ctx.query('token');\n const code = ctx.query('code');\n\n const ok: bool = TwoFactor.verify(token, email, code);\n return ok ? Response.text('confirmed\\n') : Response.text('bad code\\n', 400);\n }\n}\n```\n\nTo make `TwoFactor` verify across restarts and across the fleet, set the secret once at startup:\n\n```ts\n// main.ts\nTwoFactor.setSecret(crypto.sha256Text(Environment.getSecure('TWOFA_SECRET')!));\n```\n\nFor where email and codes fit into a full login flow, see [extending auth](../auth/extending.md).\n\n## Gotchas and limits\n\n- **Email is opt-in.** No config means every send returns `Disabled`. Check the status.\n- **The provider key is host-only.** It never appears in your compiled module, in logs, or in `/_admin`. You cannot read it with `Environment.getSecure`; that is by design.\n- **No attachments, no dynamic per-recipient lists.** The API sends a subject plus a text and/or HTML body. React templates substitute fields; they do not re-render per recipient.\n- **`TwoFactor` is not single-use.** See the limitation above. Keep TTLs short.\n- **Caps are enforced on the host, exactly.** A per-site per-minute cap (`maxPerMin`) and an optional per-day cap, plus a per-recipient hourly cap and about 30 seconds of dedup. Over a cap you get `Budget`, `RecipientCapped`, or `Deduped`, never a silent drop. Editing the caps takes effect on the next send with no restart.\n- **Put nothing sensitive in `purpose`.** It keys dedup and abuse counters; use a short tag like `\"reset\"`.\n- **Observability:** `GET /_admin/email` returns counts by reason (submitted, sent, deduped, budget, and so on). It exposes counts only, never a recipient, subject, body, or code.\n\n## Related\n\n- [Environment and secrets](./environment.md), where `TOIL_EMAIL_API_KEY` lives.\n- [Crypto](./crypto.md), the HMAC and random primitives `TwoFactor` is built on.\n- [Rate limiting](./ratelimit.md), to protect any route that triggers an email (like \"send me a code\").\n- [Extending auth](../auth/extending.md), for email and codes inside a login flow.\n- [HTTP routes](../backend/rest.md), for `@rest` / `@post` and `RouteContext`.\n",
68
+ "services/environment.md": "# Environment and secrets\n\n**`Environment` gives your app configuration values and secrets that are set outside your code**, so your compiled backend (`.wasm`) never carries a password or an API key. You read them at runtime; you never write them from code.\n\n## Why this exists\n\nYour backend often needs two kinds of outside values:\n\n- **Config**: non-sensitive settings that can change per deployment, like a public API base URL, a region name, or a feature flag.\n- **Secrets**: sensitive values you must never leak, like a payment provider's API key.\n\nThe wrong way to handle these is to type them into your source code. Then your secret is in your git history, in your build output, and in the `.wasm` shipped to every edge node. The right way, and the way toiljs uses, is the **GitHub Actions model**: you set these values out of band (today a dashboard, backed by the edge database), and your code reads them by name at runtime. Your compiled program carries **no credentials**.\n\n`Environment` is an ambient global: you use it with no import, like `Time` or `crypto`.\n\n## The two buckets: `get` vs `getSecure`\n\nThere are two **disjoint** buckets, exactly like GitHub Actions' `vars` versus `secrets`:\n\n- **`Environment.get(key)`** reads a **plain variable** (config). Returns the string, or `null` if it is not set.\n- **`Environment.getSecure(key)`** reads a **secret**. Returns the string, or `null` if it is not set.\n\n\"Disjoint\" means the buckets never overlap: a secret is **never** returned by `get()`, and a plain var is never returned by `getSecure()`. This is a deliberate safety property. It means a secret cannot leak through a code path that logs the result of a `get()`, because `get()` can never see a secret in the first place. Keys are case-sensitive and matched exactly.\n\n```mermaid\nflowchart LR\n subgraph Store[\"Per-app environment (set out of band)\"]\n V[\"vars<br/>(config)\"]\n S[\"secrets\"]\n end\n V -->|Environment.get key| G[\"your handler\"]\n S -->|Environment.getSecure key| G\n V -. never .-x GS[\"getSecure\"]\n S -. never .-x GT[\"get\"]\n```\n\n## How: a worked example reading a secret\n\nHere a route reads a public config value and a secret, then uses the secret to authorize a call to a third party. Note what it does **not** do: it never logs the secret and never returns it to the client.\n\n```ts\nimport { Response, RouteContext } from 'toiljs/server/runtime';\n\n@rest('billing')\nclass Billing {\n @get('/status')\n show(ctx: RouteContext): Response {\n const base = Environment.get('PUBLIC_API_BASE'); // plain var, or null\n const key = Environment.getSecure('STRIPE_KEY'); // secret, or null\n\n if (key == null) {\n return Response.text('billing not configured\\n', 503);\n }\n\n // Use `key` to authorize an outbound call. NEVER log it,\n // NEVER put it in the response, NEVER copy it into the client bundle.\n // (Outbound calls are made from a daemon or service; see the links below.)\n\n return Response.text(base != null ? base : 'unset');\n }\n}\n```\n\nBoth getters return `string | null`. Always handle the `null` case: a value that is not configured comes back as `null`, not an empty string or a crash.\n\n> **Secrets are plaintext in your module at runtime.** That is the point: you need the actual bytes to call out to a third party. So the rule is simple. Do not log a secret, do not put it in a response, and do not copy it into anything the browser receives.\n\n## Where the values live\n\nYou set vars and secrets in **two separate places**, so the split is structural (the secrets store can be locked down on its own).\n\n### On the edge\n\nEach host has its own environment, backed by the edge database and, as a fallback, by two dotenv files kept **outside** your deployed project (so the config watcher never even sees a credential):\n\n```bash\n# $TOIL_ENV_DIR/<host>.env (default dir: /run/toil/env) -> Environment.get(...)\nPUBLIC_API_BASE=https://api.example.com\nREGION=eu\n\n# $TOIL_ENV_DIR/<host>.env.secrets (mode 0600) -> Environment.getSecure(...)\nSTRIPE_KEY=sk_live_xxx\n```\n\nA **dotenv file** is just `KEY=value`, one per line, with `#` comments, optional `export`, and optional quotes.\n\nFramework-reserved keys use the `TOIL_` prefix (today, the `TOIL_EMAIL_*` email provider config). These are **host-only**: resolved and used in Rust where the framework needs them, and **stripped from both guest buckets**, so your code can never read them with `get` or `getSecure`. You do not read email config yourself; see [Email](./email.md).\n\n### In local development\n\n`toiljs dev` reads two files at your project root:\n\n```bash\n# .env (plain vars)\nPUBLIC_API_BASE=http://localhost:4000\n\n# .env.secrets (secrets; mode 0600; gitignored by the scaffold)\nSTRIPE_KEY=sk_test_xxx\n```\n\nIt also overlays `process.env` as plain vars, for convenience. The behavior is identical to the edge, so code that reads env in dev reads it the same way in production. Both files are gitignored by the project scaffold so you do not commit them by accident.\n\n## Secrets never enter the `.wasm`\n\nThis is the whole point, so it is worth stating plainly. Your source code contains only the **names** you look up (`'STRIPE_KEY'`), never the values. The compiler turns your names into calls to a host function; the actual value is resolved on the trusted server side, from that host's environment, at the moment you ask. The value is never baked into the compiled module and never shipped to the browser.\n\nOn the edge, a secret is **zeroized** (wiped from memory) when the host goes cold, so it does not linger.\n\n## How caching works (and why it is safe)\n\nReading env goes through a host lookup, so the edge caches values so a busy app is not paying for a fresh lookup on every request. The cache is designed to be abuse-proof:\n\n- **Lazy**: env is loaded the first time your code reads it, not eagerly. A host that never reads env costs nothing.\n- **Shared and read-only**: the data lives in one place and is never copied per request.\n- **Bounded with idle eviction (a TTL cache)**: entries are dropped after they sit unused, and the total size is capped. So a flood of requests to many different hosts can never grow memory without bound, and secrets are wiped when a host goes cold.\n\nYou do not manage any of this. You just call `get` / `getSecure`.\n\n## Build-time config vs runtime environment\n\nDo not confuse two different things that both feel like \"configuration\":\n\n- **Build-time config** lives in your project's config file (for example `toil.config.ts`). It shapes how your app is *built and wired*: routes, tiers, which features are on. It is baked into the build. See [Configuration](../concepts/config.md).\n- **Runtime environment** (this page) is resolved *while your code runs*, per host, and is never baked in. It is where secrets and per-deployment values belong.\n\nRule of thumb: if a value is a secret, or it differs between your dev machine and production, it belongs in the runtime environment, not in build-time config.\n\n## Gotchas\n\n- **`Environment` is read-only.** There is no `set`. You configure values out of band (dashboard / dotenv files), never from the module.\n- **Handle `null`.** An unset key returns `null`. Do not assume a value is present.\n- **Never log or return a secret.** `getSecure` hands you real credentials; treat them like one.\n- **Do not put a secret in build-time config.** That would bake it into the `.wasm`, defeating the whole design.\n- **`TOIL_`-prefixed keys are invisible to your code.** They are host-only framework config; you cannot read them with `get`/`getSecure`.\n\n## Related\n\n- [Email](./email.md): configured through the reserved, host-only `TOIL_EMAIL_*` keys.\n- [Configuration](../concepts/config.md): build-time config, and how it differs from runtime env.\n- [Crypto](./crypto.md): use a secret from `getSecure` as a signing or encryption key.\n- [Cookies](./cookies.md): `SecureCookies` takes a key you can source from a secret.\n",
69
+ "services/ratelimit.md": "# Rate limiting\n\n**Rate limiting caps how often one client can call a route.** You add the `@ratelimit` decorator to a route, and the edge rejects a client that goes over the limit **before your code runs**.\n\n## Why you want it\n\nSome routes are attractive to abuse. A login route invites password guessing (a \"brute-force\" attack: trying thousands of passwords). A \"send me a code\" route invites spam. A public write endpoint invites floods. Rate limiting puts a hard ceiling on how fast any single client can hammer a route, so an attacker cannot try millions of guesses and a script cannot drown your app.\n\nBecause it runs at the edge before your handler (and before the auth check), an over-the-limit request costs you almost nothing: your WebAssembly code never even wakes up.\n\n## How: the `@ratelimit` decorator\n\nPut it above a route, alongside the verb decorator (`@get`, `@post`, and so on):\n\n```ts\nimport { Response, RouteContext } from 'toiljs/server/runtime';\n\n@rest('auth')\nclass Auth {\n // At most 5 login attempts per 60 seconds, per client.\n @ratelimit(RateLimit.SlidingWindow, 5, 60)\n @post('/login')\n login(ctx: RouteContext): Response {\n // ...only runs if the caller is under the limit...\n return Response.text('ok\\n');\n }\n}\n```\n\nThe shape is `@ratelimit(strategy, limit, window)`:\n\n- **`strategy`** is a `RateLimit` value: `RateLimit.FixedWindow`, `RateLimit.SlidingWindow`, or `RateLimit.TokenBucket`. It is an ambient global, so you do not import it.\n- **`limit`** and **`window`** are two whole numbers. Their exact meaning depends on the strategy (see the table below). For the window strategies, it reads as \"at most `limit` requests per `window` seconds\".\n\nBoth numbers must be plain integer literals. A malformed decorator emits no guard at all (it fails safe) rather than compiling something wrong, the same rule as [`@cache`](./caching.md).\n\n## What a rejected request looks like\n\nWhen a client is over the limit, the edge returns **`429 Too Many Requests`** with a **`Retry-After`** header telling the client how many whole seconds to wait before trying again. Your handler does not run.\n\n```mermaid\nflowchart TD\n R[\"Request to a<br/>rate-limited route\"] --> C{\"This client<br/>under the limit?\"}\n C -->|Yes| A[\"Run @auth, then your handler\"]\n C -->|No| L[\"429 Too Many Requests<br/>Retry-After: 30\"]\n```\n\nA `429` is the standard \"slow down\" status. A well-behaved client library will read `Retry-After` and back off automatically.\n\n## The three strategies\n\nA **window** is a slice of time the limiter counts within. The strategies differ in how they draw those slices.\n\n| Strategy | `limit`, `window` mean | Behavior |\n| --- | --- | --- |\n| `FixedWindow` | `limit` events per `window` seconds | Cheapest. Counts in fixed wall-clock buckets (for example, each minute on the clock). A caller who times a burst around a bucket boundary can briefly get up to ~2x `limit` across two adjacent buckets. |\n| `SlidingWindow` | `limit` events per `window` seconds | Smooths that boundary spike by weighting the previous window. **The best general choice** for \"N per period\". |\n| `TokenBucket` | `limit` = burst size, `window` = refill rate **per second** | Allows an initial burst of up to `limit`, then refills at a steady `window` tokens per second. Good for APIs that are bursty but must stay bounded on average. |\n\nExamples:\n\n```ts\n// 100 requests per minute, smoothed. A good default for a public API.\n@ratelimit(RateLimit.SlidingWindow, 100, 60)\n\n// Burst of 20, then 5 per second sustained.\n@ratelimit(RateLimit.TokenBucket, 20, 5)\n\n// Exactly 3 per hour, cheapest counting.\n@ratelimit(RateLimit.FixedWindow, 3, 3600)\n```\n\nIf you are unsure, use `SlidingWindow`. It behaves the way people intuitively expect \"N per period\" to behave.\n\n## How a client is identified\n\nBy default the limiter keys on the **client IP address**: specifically the network peer address the edge actually observed, not a header like `X-Forwarded-For` (which a client can forge). That is what makes this a real abuse control: a caller cannot reset their own bucket by lying in a header.\n\nThe count is **exact across all of the edge's workers**: a given IP always maps to one authoritative place that keeps the count, so the limit is global for the route, not per worker. And each rate-limited route has its **own independent limiter**: a limit on `/login` does not eat into `/signup`'s budget.\n\nOnly routes that opt in with `@ratelimit` pay any cost; everything else runs on the untouched fast path.\n\n## Per-user limits with a custom key\n\nThe `@ratelimit` decorator always keys on the client IP. Sometimes you want to limit by **account** instead: \"at most 5 password changes per user per hour,\" no matter which IP the user comes from. The runtime supports this through a callable you invoke yourself, `RateLimitService.guardKeyed`. The decorator does not yet expose keyed limiting, so this callable is how you get it today.\n\nLike `crypto` and `AuthService`, `RateLimitService` and the `RateLimit` enum are **ambient globals**: use them with no import.\n\n```ts\n// Signature (ambient global; no import):\nRateLimitService.guardKeyed(\n routeId: i32, // a stable integer naming this limiter (see below)\n strategy: i32, // a RateLimit value: FixedWindow / SlidingWindow / TokenBucket\n limit: i32,\n window: i32,\n key: string, // the identity to limit by; an empty string falls back to the peer IP\n): Response | null\n```\n\nIt returns `null` when the caller is under the limit (proceed), or a ready-made **`429`** `Response` (with a `Retry-After` header) when they are over it. You call it near the top of your handler and **early-return** the response when it is non-null:\n\n```ts\nimport { Response, RouteContext } from 'toiljs/server/runtime';\n\n@rest('account')\nclass Account {\n @auth\n @post('/change-password')\n changePassword(ctx: RouteContext): Response {\n // Key the limit on the logged-in user's stable id (hex form), so the\n // budget follows the account across devices and IP addresses.\n const key = AuthService.hasSession() ? AuthService.userId()!.toHex() : '';\n\n // At most 5 attempts per user per hour. routeId 1001 names THIS limiter.\n const limited = RateLimitService.guardKeyed(1001, RateLimit.SlidingWindow, 5, 3600, key);\n if (limited != null) return limited; // over the limit: 429, the handler stops here\n\n // ...under the limit and authenticated: do the work...\n return Response.text('password changed\\n');\n }\n}\n```\n\nA few things to know:\n\n- **`routeId` names the counter.** Each distinct `routeId` is an **independent** limiter, scoped to your app (it never collides with another tenant's). Pick a stable integer per logical limiter and keep it constant, use a different value from any other keyed limiter you add, and avoid reusing a value the `@ratelimit` decorator already assigned to a route.\n- **An empty `key` falls back to the peer IP,** so `guardKeyed(..., '')` behaves like the decorator's default. Handle the not-logged-in case explicitly (as above) rather than passing a blank string by accident.\n- **You control the ordering.** Unlike the decorator (which the compiler always places before `@auth`), a manual `guardKeyed` runs wherever you put it. If you key on the user id, call it **after** `@auth` has established the session, as in the example.\n- **Same strategies and meanings** as the decorator: see [the three strategies](#the-three-strategies) for what `strategy`, `limit`, and `window` do.\n\n## Worked example: protecting login and a write route\n\nLogin is the classic case. This mirrors what the built-in auth system does for you (`register` and `login` already carry `@ratelimit(SlidingWindow, 5, 60)`), so you rarely have to add it there yourself. See [Using auth](../auth/usage.md).\n\nFor your own sensitive routes, the pattern is the same. Here a public \"post a comment\" endpoint is both rate-limited and auth-guarded:\n\n```ts\nimport { Response, RouteContext } from 'toiljs/server/runtime';\n\n@rest('comments')\nclass Comments {\n // 10 comments per minute per client, and you must be logged in.\n @ratelimit(RateLimit.SlidingWindow, 10, 60)\n @auth\n @post('/')\n create(ctx: RouteContext): Response {\n // Runs only if the caller is under the limit AND authenticated.\n return Response.text('posted\\n');\n }\n}\n```\n\nThe order is fixed and safe: rate limiting runs **first** (so even unauthenticated floods are capped), then the `@auth` guard, then your handler. You do not manage that ordering.\n\n## Choosing a limit\n\n- **Login / signup / \"send a code\":** keep it tight. 5 to 10 per minute per client is plenty for a human and painful for a guesser.\n- **Public read APIs:** a `SlidingWindow` of 60 to 300 per minute is a reasonable starting point; raise it if legitimate clients hit it.\n- **Bursty clients (dashboards that fire several calls on load):** consider `TokenBucket` so a short burst is allowed but the sustained rate stays bounded.\n- Start conservative and loosen if you see legitimate users getting `429`s. It is easier to relax a limit than to recover from an abuse incident.\n\n## Gotchas and current limits\n\n- **Route-level only.** Put `@ratelimit` on each route you want limited. There is no controller-wide form yet (unlike `@auth`, which you can put on a whole class).\n- **Keyed on IP today.** The decorator keys on the client IP. Users behind a shared IP (a corporate network, a mobile carrier gateway) share a bucket, so do not set login limits so low that a busy office trips them. A per-user key (limiting by account instead of IP) exists in the runtime but is not yet exposed through the decorator. You can still get per-user limiting today by calling [`RateLimitService.guardKeyed`](#per-user-limits-with-a-custom-key) directly.\n- **`TokenBucket`'s second number is a rate, not a duration.** For the bucket, `window` means \"tokens refilled per second\", not \"seconds\". Re-read the table if it surprises you.\n- **Same behavior in dev.** `toiljs dev` runs a single-process mirror of all three strategies, so a limited route behaves the same locally as on the edge.\n\n## Related\n\n- [Auth, sessions, and `@user`](../auth/usage.md): `@ratelimit` runs before the `@auth` guard, so it protects the login itself.\n- [Email](./email.md): pair `@ratelimit` with email triggers (verification codes, password resets) to blunt abuse.\n- [Caching](./caching.md): the other pre-handler guard; both fail safe on a malformed decorator.\n- [Every decorator](../concepts/decorators.md).\n",
70
+ "services/README.md": "# Platform services\n\nYour toiljs backend runs on the **Dacely edge**: a fleet of servers spread around the world that sit close to your users and run your compiled backend (a small, sandboxed WebAssembly program). \"Platform services\" are the batteries the edge includes so you do not have to run your own: response caching, rate limiting, secrets, email, analytics, cryptography, cookies, and a clock.\n\nEach service is either a **route decorator** (an annotation you put above a route, like `@cache`) or an **ambient global** (an object you can use without importing it, like `Environment` or `Time`). Nothing here needs a separate server, a database you manage, or a third-party account for the basics.\n\n## What each service is (and when you reach for it)\n\n- **[Caching](./caching.md)** stores a copy of a response so the edge can hand it back instantly, without re-running your code. Reach for it when a route returns the same public data to many users (a leaderboard, a product listing, a marketing page). Skip it for anything personalized.\n- **[Rate limiting](./ratelimit.md)** caps how often one client can hit a route. Reach for it on anything abusable: login, signup, \"send me a code\", a public write API. It stops brute-force and spam before your code even runs.\n- **[Environment and secrets](./environment.md)** gives your app configuration (like a public API base URL) and secrets (like a payment provider key) that are set outside your code, so your compiled program never carries a credential. Reach for it any time you call a third party or need a value that differs between dev and production.\n- **[Email](./email.md)** sends transactional email (verification codes, password resets, receipts) through a provider you configure once. Reach for it when your app needs to email a user.\n- **[Analytics](./analytics.md)** counts requests, statuses, bytes, and cache hits per site, with no code changes. Reach for it to see traffic and cache effectiveness.\n- **[Crypto](./crypto.md)** is a small, safe cryptography toolkit (hashing, HMAC, AES, random bytes) available with no import. Reach for it to sign or encrypt your own data. It is the engine under signed cookies.\n- **[Cookies](./cookies.md)** is a complete cookie layer: read incoming cookies, set them on a response, and optionally sign or encrypt their values. Reach for it for preferences, feature flags, and any small piece of state you keep in the browser.\n- **[Time](./time.md)** is the edge's clock: the current wall-clock time, read through a single blessed API. Reach for it whenever you need a timestamp or need to compute an expiry.\n\n## Pick a service by what you need\n\n| I want to... | Use | Shape |\n| --- | --- | --- |\n| Serve the same public response fast, without re-running code | [Caching](./caching.md) | `@cache(...)` / `Response.cache(...)` |\n| Stop brute-force logins or spammy writes | [Rate limiting](./ratelimit.md) | `@ratelimit(...)` |\n| Read a config value or a secret set outside my code | [Environment](./environment.md) | `Environment.get` / `getSecure` |\n| Email a user a code or a receipt | [Email](./email.md) | `EmailService` |\n| See traffic and cache-hit numbers per site | [Analytics](./analytics.md) | `Analytics` |\n| Hash, sign, encrypt, or make random bytes | [Crypto](./crypto.md) | `crypto` |\n| Remember a preference or flag in the browser | [Cookies](./cookies.md) | `Cookie` / `Cookies` / `SecureCookies` |\n| Stamp or compare an instant in time | [Time](./time.md) | `Time.nowMillis()` / `Time.nowSeconds()` |\n| Know who is logged in and protect routes | [Auth](../auth/README.md) | `@auth` / `@user` |\n\n## How they fit together\n\nDecorators and globals compose. A single route can be rate-limited, auth-guarded, and cached at once, and it can read a secret and stamp the time inside the handler:\n\n```ts\nimport { Response, RouteContext } from 'toiljs/server/runtime';\n\n@rest('api')\nclass Api {\n @ratelimit(RateLimit.SlidingWindow, 100, 60) // at most 100/min per client IP\n @cache(1) // edge-cache for 1 minute\n @get('/status')\n status(ctx: RouteContext): Response {\n const region = Environment.get('REGION'); // config value, or null\n const now = Time.nowSeconds(); // seconds since 1970\n return Response.json(`{\"region\":${JSON.stringify(region)},\"at\":${now}}`);\n }\n}\n```\n\nThe order of guards is fixed by the framework: rate limiting runs first (so floods are rejected cheaply), then auth, then your handler, and caching is applied to whatever your handler returns. You do not have to think about that order; it is safe by construction.\n\n## Related\n\n- [Every decorator, in one place](../concepts/decorators.md)\n- [Configuration: build-time config vs runtime environment](../concepts/config.md)\n- [Compute tiers (where your code runs)](../concepts/tiers.md)\n- [Backend overview](../backend/README.md) and [HTTP routes](../backend/rest.md)\n- [Auth, sessions, and `@user`](../auth/README.md)\n",
71
+ "services/time.md": "# Time\n\n**`Time` is the edge's clock: the blessed way to read the current time in your backend.** It gives you the number of milliseconds or seconds since a fixed reference point, so you can stamp events and compute expiries.\n\n## How to use it\n\n`Time` is an ambient global (no import needed), and is also exported from `toiljs/server/runtime`:\n\n```ts\nconst ms = Time.nowMillis(); // milliseconds since the Unix epoch (a u64)\nconst s = Time.nowSeconds(); // whole seconds since the Unix epoch (a u64)\n```\n\n| Member | Returns | Description |\n| --- | --- | --- |\n| `Time.nowMillis()` | `u64` | Milliseconds since the **Unix epoch** (midnight UTC on 1 January 1970). This is the same value as JavaScript's `Date.now()`. |\n| `Time.nowSeconds()` | `u64` | Whole seconds since the epoch (`nowMillis() / 1000`). This is the unit sessions and login challenges use. |\n\nThe **Unix epoch** is just the agreed \"time zero\" that computers count from. \"Milliseconds since the epoch\" is a single number that unambiguously names an instant, with no time zone to worry about.\n\n## Why time is a \"host import\" (and what that means)\n\nYour backend runs as a **WebAssembly** module: a small, sandboxed program. A sandbox is deliberately sealed off from the outside world. It has no clock of its own, no network, no files, nothing except the memory it was given and a short list of functions the host chooses to hand it. Each of those handed-in functions is called a **host import**.\n\nThis is on purpose. WebAssembly aims to be **deterministic**: given the same inputs, the same code produces the same outputs, every time. That makes it safe to run untrusted code, easy to cache, and easy to reason about. But the current time is not deterministic: it is different every time you ask. So a WebAssembly module cannot just \"know\" the time; it has to **ask the host** for it. Reading the clock is a call across the sandbox boundary.\n\n`Time` is the thin, friendly wrapper over that call. Under the hood it uses the host's `Date.now()` binding (`env.Date.now`).\n\n```mermaid\nflowchart LR\n subgraph Sandbox[\"Your backend (WebAssembly sandbox)\"]\n C[\"Time.nowMillis()\"]\n end\n subgraph Host[\"Dacely edge host\"]\n D[\"env.Date.now\"]\n K[\"real system clock\"]\n end\n C -->|host import| D\n D --> K\n K -->|milliseconds| C\n```\n\nBoth the edge and the dev server provide this binding, so time behaves **identically** in `toiljs dev` and in production.\n\n## `Time` vs calling `Date.now()`\n\nToilScript's `Date.now()` lowers to the exact same host import, so you *can* call it directly. Prefer `Time`:\n\n- It makes the host boundary (and the single millisecond unit) explicit and easy to find in a codebase.\n- It gives you `nowSeconds()` without writing an open-coded `/ 1000` at every call site.\n\nBoth are the same clock, so timing you do in a handler lines up with timing the framework does. For example, the auth system uses `Time.nowSeconds()` for session issue and expiry times.\n\n## Correct usage: timestamps and TTLs\n\n`Time` is **wall-clock time**, exactly like a browser's `Date.now()`. It follows the system clock, which means it can occasionally **step backward** (for instance when the machine corrects its clock against a time server).\n\nDo:\n\n- **Stamp an instant.** Record when something happened, so you can compare it later: an event time, a \"last seen\" marker.\n- **Compute an expiry (a TTL).** A **TTL** (\"time to live\") is a deadline. You set it by adding a duration to *now*, and you check it by comparing *now* against the stored deadline:\n\n```ts\nconst ttlSeconds: u64 = 3600; // valid for one hour\nconst expiresAt = Time.nowSeconds() + ttlSeconds;\n\n// ...later, on another request...\nconst expired = Time.nowSeconds() >= expiresAt;\n```\n\nDo not:\n\n- **Measure elapsed time with it.** Do not use it as a stopwatch to time how long an operation took. Because the wall clock can step backward, `end - start` could come out zero or negative. `Time` answers \"what instant is it now?\", not \"how much time has passed?\".\n\n## Gotchas\n\n- **It is wall-clock, not monotonic.** It can jump backward across a clock correction. Fine for timestamps and expiries; wrong for measuring durations.\n- **Milliseconds vs seconds.** `nowMillis()` and `nowSeconds()` differ by a factor of 1000. Mixing them (comparing a millisecond stamp to a second stamp) is a common bug. Sessions and login challenges use **seconds**.\n- **UTC only.** These are epoch counts, with no time zone. Format for display on the client if you need a local time.\n\n## Related\n\n- [Auth, sessions, and `@user`](../auth/usage.md): sessions use `Time.nowSeconds()` for their issue and expiry stamps.\n- [Caching](./caching.md): TTLs are the same idea, a deadline after which a saved value is stale.\n- [Compute tiers](../concepts/tiers.md): more on the WebAssembly sandbox your backend runs in.\n",
30
72
  };