toiljs 0.0.112 → 0.0.114

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 (46) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/CHANGELOG.md +10 -0
  3. package/build/backend/.tsbuildinfo +1 -1
  4. package/build/cli/.tsbuildinfo +1 -1
  5. package/build/cli/index.js +145 -27
  6. package/build/client/.tsbuildinfo +1 -1
  7. package/build/compiler/.tsbuildinfo +1 -1
  8. package/build/compiler/index.js +1 -1
  9. package/build/compiler/pages.js +1 -13
  10. package/build/compiler/prerender.d.ts +1 -0
  11. package/build/compiler/prerender.js +39 -2
  12. package/build/compiler/toil-docs.generated.js +4 -4
  13. package/build/devserver/.tsbuildinfo +1 -1
  14. package/build/devserver/analytics/index.d.ts +2 -0
  15. package/build/devserver/analytics/index.js +15 -9
  16. package/build/devserver/daemon/host.d.ts +2 -1
  17. package/build/devserver/daemon/host.js +12 -12
  18. package/build/devserver/daemon/index.js +3 -3
  19. package/build/io/.tsbuildinfo +1 -1
  20. package/build/logger/.tsbuildinfo +1 -1
  21. package/build/shared/.tsbuildinfo +1 -1
  22. package/docs/background/daemons.md +49 -3
  23. package/docs/cli/README.md +4 -2
  24. package/docs/getting-started/installation.md +18 -0
  25. package/docs/services/analytics.md +28 -17
  26. package/examples/basic/server/routes/Analytics.ts +6 -5
  27. package/package.json +15 -15
  28. package/src/cli/create.ts +1 -0
  29. package/src/cli/diagnostics.ts +74 -0
  30. package/src/cli/doctor.ts +105 -27
  31. package/src/cli/update.ts +30 -3
  32. package/src/cli/updates.ts +23 -0
  33. package/src/compiler/index.ts +11 -2
  34. package/src/compiler/pages.ts +1 -22
  35. package/src/compiler/prerender.ts +57 -3
  36. package/src/compiler/toil-docs.generated.ts +4 -4
  37. package/src/devserver/analytics/index.ts +40 -17
  38. package/src/devserver/daemon/host.ts +29 -19
  39. package/src/devserver/daemon/index.ts +12 -5
  40. package/test/analytics-dev.test.ts +8 -8
  41. package/test/daemon-emulation.test.ts +44 -2
  42. package/test/doctor.test.ts +28 -0
  43. package/test/fixtures/daemon-app.ts +9 -4
  44. package/test/prerender.test.ts +64 -2
  45. package/test/update.test.ts +15 -1
  46. package/tsconfig.base.json +0 -1
@@ -16,10 +16,10 @@ export const TOIL_DOCS: Record<string, string> = {
16
16
  "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",
17
17
  "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",
18
18
  "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",
19
- "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/daemons.md": "# Daemons (`@daemon` / `@scheduled`)\n\nA `@daemon` is a single, long-lived background worker for your whole app. You mark a class `@daemon`, add `@scheduled` methods that fire on a timer, and the Dacely edge keeps exactly **one** copy of it running worldwide, restarting it elsewhere if the machine it is on fails.\n\n## What a daemon is\n\nThe word \"daemon\" (say \"DEE-mon\") is an old computing term for a program that runs quietly in the background, not tied to any single user. That is exactly what this is.\n\nCompare the three ways your server code can run:\n\n| Kind | How many run | Lives for |\n| ------------------------------- | ------------------------------------ | ----------------------------- |\n| [Request handler](../backend/rest.md) (`@rest`) | a fresh one per request | one request |\n| [Stream box](../realtime/streams.md) (`@stream`) | one per open connection | one connection |\n| **Daemon** (`@daemon`) | **exactly one for the whole app** | as long as it holds the lease |\n\nBecause it is a single, resident instance, its fields persist across scheduled runs (a request handler forgets everything after each request; a daemon does not). It is the right home for work that must happen **once globally on a cadence**, not once per user and not once per server.\n\n```ts\n@daemon\nclass Jobs {\n @scheduled('1h')\n hourly(): void {\n // Runs once an hour, on the one elected worker. Put recurring background\n // work here: rollups, cleanup, polling an upstream, and so on.\n }\n}\n```\n\n## `@scheduled`: run on a cadence\n\nA `@scheduled` method fires on a schedule. The single string argument is the cadence, and it comes in two flavours.\n\n### Interval schedules\n\nAn **interval** fires every fixed span of time. Write a number followed by a unit: `s` (seconds), `m` (minutes), `h` (hours), or `d` (days). The number must be at least 1, and the span may not exceed 7 days.\n\n```ts\n@scheduled('30s') everyHalfMinute(): void { /* ... */ }\n@scheduled('5m') everyFiveMinutes(): void { /* ... */ }\n@scheduled('1h') hourly(): void { /* ... */ }\n@scheduled('1d') daily(): void { /* ... */ }\n```\n\n### Cron schedules\n\nA **cron** expression fires at wall-clock times (\"every weekday at 9:15\", \"midnight on the first of the month\"). Use it when you care about the actual time of day, not just a repeating gap. A cron spec is five fields separated by spaces, in this order:\n\n```\nminute hour day-of-month month day-of-week\n```\n\ntoiljs recognises a cron spec by the spaces in it (an interval has none).\n\n```ts\n@scheduled('15 9 * * 1-5') // 09:15, Monday to Friday\nweekdayMorning(): void { /* ... */ }\n\n@scheduled('0 0 1 * *') // 00:00 on the 1st of every month\nmonthlyReset(): void { /* ... */ }\n```\n\nCron times are evaluated in **UTC** and are **minute-granular** (the smallest cron step is one minute). A `*` means \"every value\" for that field.\n\n### Rules\n\n- A `@scheduled` method takes **no arguments and returns `void`**.\n- A daemon class may have **several** `@scheduled` methods, each on its own cadence.\n- Because only the one elected worker fires them, a task runs **once per tick for the whole app**, never once per server.\n\n### `onStart`: run once at boot\n\nA daemon may also declare a plain `onStart(): void` method (not decorated). It runs **once**, when the daemon box first starts on the elected worker. Use it to set up state or kick off a long-running loop.\n\n```ts\n@daemon\nclass Jobs {\n onStart(): void {\n // one-time setup when this daemon becomes active\n }\n\n @scheduled('1h')\n hourly(): void { /* ... */ }\n}\n```\n\n## No backfill: missed runs are skipped, not replayed\n\nThis is the single most important thing to understand about scheduling.\n\nIf the daemon is down when a tick was due (say the leader failed and a standby is still taking over), or if the clock jumps forward, toiljs does **not** go back and run all the ticks you missed. It simply fires the **next** due run and moves on. This is called a **no-backfill** policy.\n\nTwo practical consequences:\n\n1. **Design tasks to be safe to skip.** \"Recompute the summary\" is fine to miss (the next run fixes it). \"Charge every user once\" is not, unless you make it idempotent.\n2. **Make tasks idempotent where a missed run matters.** Idempotent means running it twice (or catching up later) has the same effect as running it once. For example, \"set yesterday's total to X\" is idempotent; \"add 1 to a counter\" is not.\n\n## One global worker, with safe failover\n\nThere is exactly **one** daemon running for your app at any moment. A second machine keeps a **warm standby** ready but idle. If the active worker's hold on the job expires (it crashed, lost the network, or was shut down), the standby takes over and fires the following runs.\n\nThe mechanism is a **lease**. Think of the lease as a \"who is in charge\" token that only one worker can hold at a time, and that has to be renewed to keep. Only the worker holding the lease (the **leader**) runs the schedule.\n\n```mermaid\nsequenceDiagram\n participant A as Worker A (leader)\n participant L as Lease\n participant B as Worker B (standby)\n A->>L: hold the lease, renew it\n Note over A: A fires @scheduled ticks\n Note over B: B stays idle, watching\n A--xL: A crashes, stops renewing\n Note over L: lease expires\n B->>L: acquire the lease\n Note over B: B is now leader, fires the next ticks\n```\n\nThe important guarantee: **two workers never run the same tick at the same time.** This is called **at-most-once** scheduling. The trade-off is the no-backfill behaviour above: to be sure a tick is never run twice, the edge would rather skip the in-flight tick when a leader is lost than risk running it on two machines. You never start, stop, or place the daemon yourself; the edge elects the leader and drives it.\n\n## Leadership fencing: side effects only run on the leader\n\nA subtle risk with a warm standby is a \"split brain\": for a brief moment, two workers might both think they are the leader. To make that harmless, toiljs **fences** every side effect behind a leadership check. A **side effect** is anything that changes the outside world:\n\n- **Database writes** (creating, patching, deleting rows, adding to counters, appending events, publishing views).\n- **Outbound HTTP calls** (`http_call`, described below).\n\nThese run **only** on the confirmed leader. If code that is not the leader tries one, the edge refuses it (a \"not leader\" error) rather than let it happen twice. Plain **reads** and computation are not fenced (they are safe to do anywhere). So even if two workers briefly overlap, only one of them can actually write or call out. You do not write the fencing yourself; it is automatic. The upshot: put your writes and outbound calls in `@scheduled` methods freely, and trust that they happen once.\n\n## `daemon.*` host calls\n\nA daemon has a small set of host abilities beyond ordinary computation:\n\n- **Database access.** A daemon reads and writes [ToilDB](../database/README.md) with the same collection handles you use in a route or a derive (`.get`, `.add`, `.append`, `.publish`, and so on). Writes are leader-fenced as described above.\n- **Outbound HTTP (`http_call`).** A daemon can call an external service (to poll an API, post to a webhook). This is the one place your server code reaches out to the internet, so it is deliberately restricted:\n - It is **leader-only** (fenced, like any side effect).\n - It is **SSRF-bounded**. SSRF (server-side request forgery) is an attack where code is tricked into calling internal addresses it should not reach. The edge resolves the target host and blocks private or internal addresses, so a daemon cannot use `http_call` to poke around inside the network.\n - It is **metered**: making many calls or pulling huge responses costs budget, which caps abuse.\n- **Leadership info** (`is_leader`, `current_epoch`). A daemon can check whether it is currently the leader, which is useful for guarding a long `onStart` loop.\n\n> **Note:** In `toiljs dev` (the single-process local emulator), the daemon is always the leader (there is nothing to fail over to), and `http_call` is stubbed to return a \"call failed\" result rather than make real network requests. Everything else, including the schedule and your database writes, runs exactly as it does on the edge.\n\n### The `Daemon` global\n\nInside a `@daemon` class, `Daemon` is an ambient global (no import), like `Analytics`. It needs `toilscript` 0.1.60 or newer.\n\n| Member | Description |\n| --- | --- |\n| `Daemon.isLeader(): bool` | Whether this worker currently holds the lease. A snapshot: re-check it inside a long task rather than trusting the value it started with. |\n| `Daemon.epoch(): i64` | The monotonic fencing token, bumped on every (re)acquire, or `-1` when this worker is not the leader. Stamp it into work that must not outlive the lease that authorized it. |\n| `Daemon.taskCount(): i32` | How many `@scheduled` tasks are registered. |\n| `Daemon.nextFireMs(taskId: i32): i64` | The next fire time for a task, in epoch milliseconds, or `-1` when the id is unknown or the cron never fires again. |\n| `Daemon.yieldNow(): DaemonError` | Give the edge a chance to observe a lost lease. `DaemonError.None` means you are still the leader. |\n| `Daemon.sleep(ms: i64): DaemonError` | Park the task. The edge clamps this to 3 seconds. `LeaseLost` means the lease went away while you slept, and the task should stop. |\n| `Daemon.httpCall(request, responseCap?)` | Make an outbound HTTP request. Returns a `DaemonHttpResponse`, or `null` on failure. |\n| `Daemon.lastError(): DaemonError` | The failure recorded by the most recent `Daemon` call. |\n\n### Outbound HTTP\n\n`Daemon.httpCall` performs the leader-fenced, SSRF-bounded, metered call described above. Its usage shows up in the `Analytics` counters `daemonHttpCallAttempts` / `daemonHttpCallFailures`.\n\n```ts\n@daemon\nclass Jobs {\n @scheduled('5m')\n poll(): void {\n const req = new DaemonHttpRequest('POST', 'https://api.example.com/events');\n req.header('content-type', 'application/json');\n req.body = Uint8Array.wrap(String.UTF8.encode('{\"ping\":true}'));\n\n const res = Daemon.httpCall(req);\n if (res == null) {\n // Daemon.lastError(): NotLeader, CallFailed, ResponseTooLarge, BadEnvelope\n return;\n }\n if (res.status == 200) App.events.add(new Event(res.text()));\n }\n}\n```\n\nA `null` result is never ambiguous: read `Daemon.lastError()`.\n\n| `DaemonError` | Meaning |\n| --- | --- |\n| `NotLeader` | This worker does not hold the lease. The edge refused without touching the network. |\n| `LeaseLost` | The lease went away mid-call. |\n| `CallFailed` | The request did not complete: blocked by the SSRF guard, timed out, or the transport failed. |\n| `ResponseTooLarge` | The response did not fit your buffer. **The call already happened**, so do not blindly retry it: pass a larger `responseCap` instead. |\n| `BadEnvelope` | Your request broke a host cap (nothing was sent), or the response could not be parsed. |\n\nThe caps the edge enforces, which `httpCall` checks in the guest so an over-cap request never costs you a round trip: the method is 1 to 16 bytes, the URL at most 8 KiB, at most 64 headers of at most 8 KiB each, and a request body of at most 256 KiB. Responses are truncated at 1 MiB; `responseCap` defaults to 64 KiB.\n\nIn `toiljs dev` the emulator returns `CallFailed` for every `httpCall` rather than reaching the network, so a daemon that polls an external service does nothing locally. Everything else behaves as it does on the edge.\n\n## The `main.daemon.ts` file (a separate tier)\n\nLike streams, daemons live in their **own entry file**, `server/main.daemon.ts`, and compile into their **own artifact**, `build/server/release-cold.wasm`. Importing your `@daemon` module there pulls it into that artifact.\n\n```ts\n// server/main.daemon.ts\nimport { revertOnError } from 'toiljs/server/runtime/abort/abort';\n\nimport './daemon/Jobs'; // add each @daemon module here\n\n// NOTE: unlike main.ts / main.stream.ts, the daemon entry does NOT re-export the\n// request runtime. A daemon artifact exposes its schedule hooks, not the request handler.\nexport function abort(message: string, fileName: string, line: u32, column: u32): void {\n revertOnError(message, fileName, line, column);\n}\n```\n\n`toiljs build` produces `release-cold.wasm` automatically when your project declares a `@daemon` surface:\n\n```sh\n$ ls build/server/*.wasm\nbuild/server/release.wasm # L1 request (@rest / @service)\nbuild/server/release-stream.wasm # L2/L3 stream (@stream)\nbuild/server/release-cold.wasm # L4 daemon (@daemon)\n```\n\nThere is **at most one** `@daemon` class per project, and it is a compile error to put a `@daemon` in the request build. See [Compute tiers](../concepts/tiers.md) for how one source tree becomes three artifacts.\n\n## Worked example: a periodic rollup\n\nA common daemon job: every hour, read a running total and store a small summary so pages can show it with one cheap read. This reads a [counter](../database/counters.md) and writes a summary [document](../database/documents.md); the write only takes effect on the leader.\n\n```ts\n// server/models/StatKey.ts and Summary.ts (@data types)\n@data\nclass StatKey {\n name: string = 'signups';\n constructor(name: string = 'signups') { this.name = name; }\n}\n\n@data\nclass Summary {\n total: u64 = 0;\n updatedAt: u64 = 0;\n}\n\n// server/data/StatsDb.ts\n@database\nclass StatsDb {\n @collection static signups: Counter<StatKey>;\n @collection static summary: Documents<StatKey, Summary>;\n}\n\n// server/daemon/Jobs.ts\n@daemon\nclass Jobs {\n @scheduled('1h')\n rollup(): void {\n const key = new StatKey('signups');\n const total = StatsDb.signups.get(key); // a read (allowed anywhere)\n\n const s = new Summary();\n s.total = total;\n s.updatedAt = <u64>(Date.now() / 1000);\n StatsDb.summary.patch(key, s); // a write: runs only on the leader\n }\n}\n```\n\nNow any request handler can serve the summary with a single keyed read, and it is never more than an hour stale. Because the write is fenced to the leader and the rollup is idempotent (it *sets* the total rather than adding to it), a missed or failed-over tick is harmless: the next hourly run simply refreshes it.\n\n## When not to use a daemon\n\n- **When a user is waiting for the result.** Do that work in a [route](../backend/rest.md) or [RPC](../backend/rpc.md), on the request path.\n- **When you need a fast read-view kept in sync with data changes.** That is a [`@derive`](./derive.md), which runs on every write rather than on a timer.\n- **When you need exactly-once, never-skipped execution.** Scheduling is at-most-once with no backfill, so make tasks idempotent or safe to skip. A daemon is not a guaranteed job queue.\n\n## Related\n\n- [Background overview](./README.md): daemons versus `@derive`, and which to reach for.\n- [Derived views (`@derive`)](./derive.md): keep a read-view in sync on every write.\n- [Compute tiers (L1 to L4)](../concepts/tiers.md): the daemon runs on the L4 global tier.\n- [Counters](../database/counters.md), [Documents](../database/documents.md), [Events](../database/events.md): the data a daemon reads and writes.\n",
20
20
  "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",
21
21
  "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",
22
- "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
+ "cli/README.md": "# The toiljs CLI\n\nThe `toiljs` command is how you scaffold, run, build, self-host, and diagnose a toiljs app. This page lists every command and every flag, with copy-pasteable examples.\n\n## What it is\n\nWhen you install the `toiljs` package, it adds one executable to your project: `toiljs`. Everything you do day to day (start the dev server, produce a production build, check your setup) goes through it.\n\nYou almost never type the full path. A freshly scaffolded project already has npm scripts that call it, so you run `npm run dev` and `npm run build`. When you want a command that has no script (like `doctor` or `db`), run it with `npx`:\n\n```bash\nnpx toiljs doctor\n```\n\n`toiljs` needs **Node.js 24 or newer** (older versions will not run it).\n\n## Command overview\n\n| Command | What it does |\n| --- | --- |\n| `toiljs create [name]` | Scaffold a brand new toiljs app in a new folder. |\n| `toiljs dev` | Start the local development server with hot reload. |\n| `toiljs build` | Produce the optimized production build (client bundle + server WebAssembly). |\n| `toiljs start` | Self-host the built app on a fast production HTTP server. |\n| `toiljs configure` | Turn styling features (Sass/Less/Stylus, Tailwind) on or off in an existing project. |\n| `toiljs doctor` | Diagnose your project setup and (with `--fix`) repair common wiring. |\n| `toiljs update` | Check npm for newer dependency versions and apply the ones you pick. |\n| `toiljs db <action>` | Inspect, reset, snapshot, or restore the local dev database. |\n| `toiljs help` | Print the built-in help. Also `--help` or `-h`. |\n| `toiljs --version` | Print the installed toiljs version. Also `-v`. |\n\nRun `toiljs help` any time to see this list in your terminal.\n\n## Global behavior\n\nA few things apply to every command.\n\n- **`--root <dir>`**: run the command against a project in another directory instead of the current one. Every command accepts it.\n- **Automatic update check**: on every run (including `npm run dev`, which calls the CLI under the hood), toiljs quietly asks the npm registry whether a newer `toiljs` exists and prints a one-line notice on stderr if you are behind. It never blocks or slows the command in a meaningful way (the answer is cached for an hour and the network call is capped at two seconds). To turn it off, set the environment variable `TOILJS_NO_UPDATE_CHECK=1`. It also respects the common `NO_UPDATE_NOTIFIER` and `CI` variables.\n\n```bash\n# Run any command against a project in another folder.\ntoiljs build --root ./apps/marketing\n\n# Silence the \"newer version available\" notice.\nTOILJS_NO_UPDATE_CHECK=1 toiljs dev\n```\n\n## `toiljs create`\n\nScaffolds a new project into a new folder. By default it is interactive: it asks a short series of questions (project name, template, styling, and so on), then writes the files and installs dependencies. Pass flags to skip questions, or `-y` to accept every default and run with no questions at all.\n\n```bash\n# Interactive: answer the prompts.\nnpx toiljs create\n\n# Give it a name up front.\nnpx toiljs create my-app\n\n# Fully non-interactive (great for scripts and CI).\nnpx toiljs create my-app --yes --template app --style css\n```\n\n### What it sets up\n\nEvery new project comes wired for you: the enforced TypeScript, ESLint, and Prettier presets, file-based routing, a `toil.config.ts`, a `toilconfig.json` (the server compiler settings), a `.gitignore`, and the editor settings that make the toilscript language plugin work. It also scaffolds a `server/migrations/` folder (where ToilDB schema migrations live) and, unless you opt out, a set of AI assistant helper files.\n\nThe scaffolded `package.json` includes these scripts:\n\n| Script | Runs |\n| --- | --- |\n| `npm run dev` | `toiljs dev` |\n| `npm run build` | `toiljs build` |\n| `npm run build:server` | `toiljs build --server` |\n| `npm run lint` | `eslint client` |\n| `npm run typecheck` | `tsc --noEmit` |\n| `npm run format` | `prettier --write ...` |\n\n### Generated docs and AI-assistant pointers\n\nEvery project carries a full copy of this documentation set at `.toil/docs/`. You do not maintain it: toiljs regenerates it from the installed toiljs version on every `toiljs dev` and `toiljs build`, so it always matches the version you are on. Do not edit those files by hand (your changes are overwritten on the next dev or build).\n\nUnless you opt out, `toiljs create` also writes small **pointer files** at the project root that tell AI coding assistants to read `.toil/docs/` before touching the project: `CLAUDE.md` (Claude Code), `AGENTS.md` (Codex and others), `.cursor/rules/toiljs.mdc` (Cursor), and `.github/copilot-instructions.md` (GitHub Copilot). These are written once, committed, and yours to edit. Control them with `--ai` / `--no-ai` (when you pass neither, `create` asks).\n\n### create options\n\n| Flag | Meaning |\n| --- | --- |\n| `[name]` | The project folder name (a positional argument). If omitted, you are asked. |\n| `-t, --template <app\\|minimal>` | `app` is the full starter (landing page, layout, styles, demo routes). `minimal` is just a layout and a home route. Default `app`. |\n| `--style <css\\|sass\\|less\\|stylus>` | Which CSS flavor to set up. Default `css` (plain CSS). |\n| `--tailwind` / `--no-tailwind` | Add or skip Tailwind CSS (v4). Off by default. |\n| `--ai` / `--no-ai` | Include or skip AI assistant files (like `CLAUDE.md`). When omitted, you are asked. |\n| `--images` / `--no-images` | Enable or skip build-time image optimization. On by default. |\n| `--git` / `--no-git` | Initialize a git repository. When omitted, you are asked (default yes). |\n| `--install` / `--no-install` | Install dependencies after scaffolding. When omitted, you are asked (default yes). |\n| `--pm <npm\\|pnpm\\|yarn\\|bun>` | Which package manager to install with. Default `npm`. |\n| `-y, --yes` | Accept all defaults and skip every prompt. |\n\nFor a walkthrough, see [Create a project](../getting-started/create-project.md).\n\n## `toiljs dev`\n\nStarts the local development server with hot reload, so you edit a file and the browser updates in place. This is the command you leave running while you build.\n\n```bash\nnpm run dev\n# or, to pick a port:\nnpx toiljs dev --port 4000\n```\n\n### What the dev server emulates\n\nThe whole point of the dev server is to run your app locally the same way the real Dacely edge runs it in production, so what you see locally is what you ship. It emulates three things:\n\n1. **The edge.** For a project with a server (any project with a `toilconfig.json`), a small local HTTP server takes the public port and dispatches incoming requests into your compiled `server.wasm` using the exact same request envelope the production edge uses. Anything your server does not claim (page routes, static assets, the hot-reload websocket) is proxied to Vite behind the scenes. So your React frontend and your WebAssembly backend run together, on one URL, exactly like production.\n2. **The database.** ToilDB runs in-process as a local emulator. Every family (documents, views, unique, events, counters, membership, capacity) works, and the data is written to `.toil/devdata.json` so it survives restarts. You manage that file with [`toiljs db`](#toiljs-db).\n3. **The host functions.** The platform services your server code calls (email, environment variables and secrets, time, crypto, rate limiting, auth) are wired up locally so they behave like the edge. Email actually sends if you configure a provider (see [Email](../services/email.md)).\n\nA **client-only** project (no `toilconfig.json`) just gets the plain Vite dev server on your port, unchanged.\n\n### How hot reload works\n\n```mermaid\nflowchart TD\n E[\"You edit a file\"] --> Q{\"Which file?\"}\n Q -->|\"client/ (React, CSS)\"| V[\"Vite hot-swaps it in the browser\"]\n Q -->|\"server/ (a @rest / @data / @service file)\"| S[\"toilscript recompiles the server\"]\n S --> G[\"shared/server.ts is regenerated\"]\n G --> V2[\"Vite hot-swaps the new typed client\"]\n S --> W[\"the dev server hot-swaps the new server.wasm\"]\n V --> B[\"Browser updates, no full reload\"]\n V2 --> B\n W --> B\n```\n\nClient edits go straight through Vite's hot module replacement. Server edits trigger a toilscript rebuild: toiljs recompiles your backend, regenerates `shared/server.ts` (the typed client the browser imports to call your server), and hot-swaps the recompiled WebAssembly, all without you touching the browser. Rebuilds are debounced (grouped over about 150 milliseconds) so a \"save all\" or a formatter pass does not trigger a storm of builds.\n\nIf your project has an `emails/` folder, the dev server also prints an email-preview URL at `/__toil/emails`.\n\n### dev options\n\n| Flag | Meaning |\n| --- | --- |\n| `--port <n>` | Port to listen on. Default `3000` (or `client.port` from your config). |\n| `--root <dir>` | Run against a project in another directory. |\n\nPress `Ctrl+C` to stop. toiljs restores your terminal and force-exits even if a native listener is slow to close, so you never end up with an orphaned dev server rebuilding in the background.\n\n## `toiljs build`\n\nProduces the optimized production build. Run this before you deploy or before `toiljs start`.\n\n```bash\nnpm run build\n# or build only the server:\nnpx toiljs build --server\n```\n\n### What it produces\n\nA full build runs in a careful order so the pieces line up:\n\n```mermaid\nflowchart LR\n A[\"Your project\"] --> S[\"1. Compile the server<br/>(toilscript)\"]\n S --> W[\"build/server/release.wasm\"]\n S --> R[\"shared/server.ts<br/>(typed client, regenerated)\"]\n R --> C[\"2. Bundle the client<br/>(Vite)\"]\n C --> O[\"build/client/<br/>(HTML, JS, CSS, assets)\"]\n C --> X[\"3. Prerender + SEO<br/>(SSG pages, robots.txt,<br/>sitemap.xml, llms.txt)\"]\n```\n\n1. **The server is built first.** toilscript compiles every decorated server file (not just the entry) into `build/server/release.wasm`, and regenerates `shared/server.ts`. Doing this first means the client always bundles against a current, correct typed server client. A project that also declares `@stream` or `@daemon` surfaces compiles those into their own artifacts (`release-stream.wasm`, `release-cold.wasm`).\n2. **The client is bundled** by Vite into `build/client/` (your HTML, JavaScript, CSS, and optimized assets). The dev toolbar and error overlay are stripped out of the production bundle.\n3. **Static pages and SEO files are generated**: any route that opts into static generation is prerendered to HTML, and if you configured `client.seo`, toiljs writes `robots.txt`, `sitemap.xml`, and `llms.txt`. Routes that opt into server-side rendering get their HTML-with-holes templates baked for the edge.\n\n### build options\n\n| Flag | Meaning |\n| --- | --- |\n| `--server` | Build **only** the server (recompile the wasm and regenerate `shared/server.ts`), and skip the client bundle. Fast when you only touched backend code. This is what `npm run build:server` runs. |\n| `--root <dir>` | Run against a project in another directory. |\n\nA client-only project (no `toilconfig.json`) skips step 1 and just bundles the client.\n\n## `toiljs start`\n\nSelf-hosts the app you just built, on a fast production HTTP server (hyper-express, backed by uWebSockets.js). It serves your static client, runs your `server.wasm` for dynamic requests, does server-side rendering, supports daemons, and exposes a `/_toil` websocket channel. Use it to run your app on your own machine or server instead of deploying to the Dacely edge.\n\n```bash\nnpm run build # start needs a build to serve\nnpx toiljs start\nnpx toiljs start --port 8080 --host 0.0.0.0 --threads 4\n```\n\n`start` fails fast if there is no build yet (it looks for `build/client/index.html`), so run `toiljs build` first.\n\n### start options\n\n| Flag | Meaning |\n| --- | --- |\n| `--port <n>` | Port to listen on. Default `3000` (or `client.port`). |\n| `--host <host>` | Address to bind. Default `127.0.0.1` (loopback only). Pass `0.0.0.0` to accept connections from other machines. |\n| `--threads <n>` | Number of HTTP worker processes. Default is automatic (one per available CPU). Pass `1` to disable the worker pool. `--workers` is an accepted alias. This can also be set as `server.threads` in your config. |\n| `--root <dir>` | Run against a project in another directory. |\n\n## `toiljs configure`\n\nToggles a project's client styling features (the CSS preprocessor and Tailwind) after the fact, on an existing app. It detects your current setup, asks what you want, then rewrites the stylesheets and your app entry's imports, edits `package.json`, and syncs `node_modules` so removed packages are actually uninstalled.\n\n```bash\n# Interactive.\nnpx toiljs configure\n\n# Non-interactive: switch to Sass and turn Tailwind on.\nnpx toiljs configure --style sass --tailwind\n```\n\n### configure options\n\n| Flag | Meaning |\n| --- | --- |\n| `--style <css\\|sass\\|less\\|stylus>` | Switch the CSS preprocessor. |\n| `--tailwind` / `--no-tailwind` | Turn Tailwind on or off. |\n| `--images` / `--no-images` | Turn build-time image optimization on or off (sets `client.images` in your config). |\n| `--no-install` | Edit the files but do not run the package manager. You then run install yourself. |\n| `--root <dir>` | Run against a project in another directory. |\n\nPassing any of `--style`, `--tailwind`, or `--images` makes the command non-interactive (it skips the prompts you did not answer with a flag). See [Styling](../frontend/styling.md) for the full picture.\n\n## `toiljs doctor`\n\nRead-only project diagnostics. It gathers facts from disk (your `package.json`, lockfiles, the resolved config, your app entry, `index.html`, your routes, and the server target), runs a set of checks, and prints a grouped report. It never changes anything unless you pass `--fix`, and it never crashes on a partial or non-toiljs project (missing pieces just become warnings or failures). It exits with a non-zero status when any check **fails** (warnings do not fail), so it is safe to run in CI.\n\n```bash\n# Human-readable report.\nnpx toiljs doctor\n\n# Machine-readable, for CI.\nnpx toiljs doctor --json\n\n# Auto-repair the common wiring.\nnpx toiljs doctor --fix\n```\n\n### What it checks\n\nThe report is grouped:\n\n| Group | Example checks |\n| --- | --- |\n| **Environment** | Node.js version, that `toiljs` and its peer dependencies (React and so on) are installed and new enough, that your TypeScript is a supported 6.x rather than the unsupported native 7.x, that a lockfile exists, and that your scripts do not wrap `toiljs` in a stray `npx`. |\n| **Project + routing** | The `client/` and `routes/` folders exist, `index.html` has a `<div id=\"root\">`, your app entry calls `mount(...)` with the `slots` argument, at least one route exists, no two routes collide on the same URL, and no asset paths are written in a way that 404s on nested routes. |\n| **Config + assets** | Your `toil.config` loads, the base path is well formed, `client.seo` has a `url` if SEO is configured, and your styling packages are actually installed. |\n| **Server / WASM** | The `toilconfig.json` and its entry files exist, `toilscript` is installed, a compiled `.wasm` exists, the typed-RPC wiring is in place, your `@rest` controllers are actually dispatched, the Prettier and editor plugins are wired, and a `migrations/` folder exists. |\n| **Security** | If your server uses auth, whether `AUTH_SESSION_SECRET` is set (an unset secret means sessions fall back to a published dev key, which is forgeable). |\n\n### What `--fix` repairs\n\n`--fix` pins an unsupported TypeScript (the native 7.x, which ships no compiler API) back to `^6.0.3` in any project. The rest only touch a server project (one with a `toilconfig.json`), repairing the wiring that is easy to get wrong or that older projects predate:\n\n- adds `--rpcModule shared/server.ts` to your server build scripts,\n- adds `shared` and the `shared/*` path alias to `tsconfig.json`,\n- adds `shared/server.ts` to `.gitignore`,\n- lifts the `toilscript` version floor if it is too old,\n- adds the `toiljs/prettier-plugin` to your Prettier config (so Prettier does not choke on server decorators),\n- adds the toilscript language-service plugin to your server `tsconfig.json` and points VS Code at the workspace TypeScript (so the editor stops false-flagging `@database` collections and `@data` members),\n- refreshes the editor-only server globals declaration file.\n\nIt is idempotent: it only writes files it actually needs to change, and it tells you which ones changed and which need a manual edit (for example a `tsconfig.json` that contains comments). If it changed `package.json`, run your installer afterward.\n\n### doctor options\n\n| Flag | Meaning |\n| --- | --- |\n| `--json` | Emit machine-readable JSON instead of the human report (the banner is suppressed so stdout stays valid JSON). |\n| `--fix` | Repair the server wiring in place, as above. |\n| `--root <dir>` | Run against a project in another directory. |\n\n## `toiljs update`\n\nA friendly wrapper over `npm-check-updates`. It checks the registry for newer versions of your dependencies, groups them by how big the jump is (major, minor, patch), lets you pick which to apply (or `-y` to apply all), bumps `package.json`, and runs your package manager's install. It also makes sure your `server/migrations/` folder exists (older projects predate it). `npm-check-updates` runs via `npx`, so it never becomes a permanent dependency of your project.\n\nUpgrades into a major toiljs does not support are held back and listed separately, so neither the picker nor `-y` can install one. Today that means **TypeScript 7**, the native port, which ships no JavaScript compiler API (see [Installation](../getting-started/installation.md)). Bumps inside TypeScript 6 are still offered.\n\n```bash\n# Interactive picker.\nnpx toiljs update\n\n# Apply everything, non-interactively.\nnpx toiljs update --yes\n\n# Only patch-level updates.\nnpx toiljs update --target patch\n```\n\n### update options\n\n| Flag | Meaning |\n| --- | --- |\n| `-y, --yes` | Apply all available updates without the picker. |\n| `--target <latest\\|minor\\|patch\\|newest\\|greatest>` | How far to bump. Default `latest`. |\n| `--root <dir>` | Run against a project in another directory. |\n\n## `toiljs db`\n\nManages the local dev database: the on-disk ToilDB store your dev server writes to `.toil/devdata.json`. Use it to inspect data, wipe a corrupt state, save a snapshot to share as a fixture, or restore one. The snapshot is exactly the JSON the dev database uses, so an exported file imports cleanly.\n\n```bash\n# See what is stored.\nnpx toiljs db status\n\n# Wipe all dev data.\nnpx toiljs db reset\n\n# Save a snapshot (to a file, or to stdout if you omit the file).\nnpx toiljs db export fixture.json\n\n# Restore a snapshot.\nnpx toiljs db import fixture.json\n\n# Print the on-disk path (scriptable).\nnpx toiljs db path\n```\n\n### db actions\n\n| Action | What it does |\n| --- | --- |\n| `status` (alias `info`) | Show the database path, its size, and per-family row counts. |\n| `reset` (alias `purge`) | Delete all dev data (removes `devdata.json`). |\n| `export [file]` | Write a formatted snapshot to `file`, or to stdout if you omit it (pipe-friendly). |\n| `import <file>` | Replace the dev database with the snapshot in `file`. It refuses a file that is not a valid snapshot. |\n| `path` | Print the `devdata.json` path and nothing else. |\n\nThe dev database is per-project and lives under `.toil/`, which is gitignored. See [Database setup](../database/setup.md) for what actually populates it.\n\n## The dev, build, and deploy flow\n\nHere is how the commands fit together across your workflow.\n\n```mermaid\nflowchart LR\n subgraph Develop\n D[\"toiljs dev<br/>(hot reload, local edge + DB)\"]\n end\n subgraph Ship\n B[\"toiljs build<br/>(client bundle + server.wasm)\"]\n end\n subgraph Run\n ST[\"toiljs start<br/>(self-host locally)\"]\n DP[\"deploy to the Dacely edge\"]\n end\n D --> B\n B --> ST\n B --> DP\n```\n\n- **Develop** with `toiljs dev`. Everything runs locally and reloads as you type.\n- **Ship** with `toiljs build`. This produces the artifacts: the client bundle in `build/client/` and the server WebAssembly in `build/server/`.\n- **Run** the build two ways: `toiljs start` self-hosts it on your own machine, or you deploy the same build to the Dacely edge to serve it worldwide.\n\n## Gotchas\n\n- **`start` needs a build.** `toiljs start` serves what is in `build/`. Run `toiljs build` first, or it exits with an error.\n- **Run `toiljs`, not `npx toiljs`, inside npm scripts.** Under `npm run`, `node_modules/.bin` is already on your PATH, so an extra `npx` layer is redundant and can leave your terminal in a broken input mode after `Ctrl+C`. `doctor` warns about this; the scaffold gets it right.\n- **`--host` and `--threads` are `start`-only.** `toiljs dev` always binds locally and does not take `--host` or `--threads`.\n- **`--server` builds the backend only.** Use it while iterating on server code, but run a full `toiljs build` before you deploy so the client bundle is current.\n- **The update notice is not an error.** The \"newer toiljs available\" line is informational and prints to stderr. Set `TOILJS_NO_UPDATE_CHECK=1` to hide it.\n\n## Related\n\n- [Installation](../getting-started/installation.md) and [Create a project](../getting-started/create-project.md)\n- [Project structure](../getting-started/project-structure.md)\n- [Configuration reference (`toil.config.ts`)](../concepts/config.md)\n- [Styling](../frontend/styling.md)\n- [Database setup](../database/setup.md) and the [database overview](../database/README.md)\n- [Compute tiers (L1 to L4)](../concepts/tiers.md)\n",
23
23
  "concepts/ai-guide.md": "# Writing toiljs correctly\n\nThis is a fast, high-signal cheat-sheet for writing toiljs and toilscript code that compiles and behaves. It is aimed at AI assistants generating code (and at humans in a hurry): the patterns to follow, the mistakes to avoid, and a link to the deep page whenever you need more. It does not re-teach the framework; each section points you at the authoritative doc.\n\n## Project shape\n\nA toiljs project has three top-level folders, and knowing which one a file belongs to tells you which rules apply:\n\n- **`client/`**: your React app that runs in the browser. Pages live under `client/routes/`, one file per URL, and each route file `export default`s its component. See [Frontend](../frontend/README.md) and [Routing](../frontend/routing.md).\n- **`shared/`**: a generated typed bridge (`shared/server.ts`) that lets the browser call the backend with full types. It is regenerated on every build, so **never hand-edit it**.\n- **`server/`**: your backend, written in TypeScript but compiled by **toilscript** to WebAssembly. It runs on the Dacely edge, not in Node or the browser. See [Backend](../backend/README.md).\n\nMost code runs on the L1 request tier. Long-lived connections (`@stream`) and scheduled work (`@daemon`) are opt-in tiers in their own entry files. See [Compute tiers](./tiers.md).\n\n## Client rules (the browser, React side)\n\n**Call the backend only through the generated `Server` client.** This is the number one rule. Use `Server.REST.<controller>.<route>(args)` for `@rest` HTTP controllers, or `Server.<service>.<method>(args)` / `Server.<remote>(args)` for RPC. Raw `fetch` to your own backend throws away the type safety, the argument and result decoding, and the binary wire codec, and it silently breaks when a route is renamed. `fetch` is only for third-party URLs (an external API, a CDN). See [Fetching data](../frontend/data-fetching.md).\n\n```ts\n// WRONG: raw fetch to your own backend\nawait fetch('/account/session', { method: 'POST' });\n\n// RIGHT: the typed client (renames become compile errors; args + result typed)\nawait Server.REST.account.session();\n```\n\n**Use the ambient `Toil.*` globals with no import.** `Toil` is the `toiljs/client` package exposed as a global and typed via a generated `toil-env.d.ts`, so it autocompletes with no import: `Toil.Link` / `Toil.NavLink` for navigation, `Toil.useParams`, `Toil.useLoaderData`, `Toil.Image`, `Toil.useHead` / `Toil.Head`, `Toil.Form` / `Toil.useAction`, and more. `Server` and `parseError` are also bare globals; `FastMap`, `FastSet`, `DataWriter`, and `DataReader` are bare globals too (write `new DataWriter()`, not `new Toil.DataWriter()`). Full index: [The Toil global](../frontend/toil-global.md).\n\n**Load page data with a `loader`, not a `useEffect` fetch.** A route file exports a `loader`, and the component reads its result with `Toil.useLoaderData`. The loader runs on navigation in parallel with the route chunk, integrates with `loading.tsx`, caching, and SSR hydration. A `useEffect` fetch runs only after mount (slower, invisible to the server).\n\n```tsx\n// client/routes/blog/[id].tsx\nexport const loader = async ({ params }: Toil.LoaderArgs) => {\n return Server.REST.blog.get({ params: { id: params.id } });\n};\n\nexport default function BlogPost() {\n const post = Toil.useLoaderData<typeof loader>();\n return <article><h1>{post.title}</h1></article>;\n}\n```\n\n**Mutate with `Toil.useAction` or `<Toil.Form>`, then revalidate.** Both track pending and error state and refetch the affected loader data on success, so the page updates without a manual refetch. Use `<Toil.Form>` for form submits, `useAction` for anything else (a delete button, a toggle). Reach for `Toil.revalidate()` / `router.revalidate()` after a write that these do not cover.\n\n**Per-route SEO with `export const metadata`** (or `Toil.useHead` / `<Toil.Head>` from inside a component). Opt a route into edge SSR with `export const ssr = true`. See [Metadata and SEO](../frontend/metadata.md) and [Rendering and SSR](../frontend/rendering.md).\n\n**Typed hrefs.** `Toil.Link` `href`, `Toil.navigate`, and `router.push` are all type-checked against your real routes, so a typo is a compile error. Use `Toil.href(str)` only when a URL is assembled from data and TypeScript cannot prove it is a real route. See [Navigation](../frontend/navigation.md).\n\n**`getUser()` on the client is display-only.** It reads a readable session cookie with no network call, so it is instant but **forgeable**. Use it to render \"logged in as ...\", never to gate real access. The authoritative check is a server route guarded with `@auth`. See [Auth usage](../auth/usage.md).\n\n## Server rules (toilscript, compiles to WASM)\n\nThe decorators, each doing one job (full list in [Decorators](./decorators.md)):\n\n- **`@data`** on a class: generates the binary and JSON codec so the value can cross the wire and the database. Every field needs a default; field order **is** the binary layout, so add new fields at the end. See [Data types](../backend/data.md).\n- **`@rest('name')`** on a class mounts an HTTP controller at `/name`; **`@get`/`@post`/`@put`/`@del`/`@patch`** on its methods declare routes (`@del`, not `@delete`, which is reserved). See [REST](../backend/rest.md).\n- **`@service`** + **`@remote`** expose typed RPC callable from your own frontend as `Server.<service>.<method>()`. See [RPC](../backend/rpc.md).\n- **`@user`** declares the authenticated-user shape and enables `AuthService.getUser()`. **Exactly one `@user` per project.** **`@auth`** guards a route or a whole `@rest` class. See [Auth](../auth/README.md).\n- **`@ratelimit(strategy, limit, window)`** caps how often a caller may hit a route, rejected at the edge before your code runs. See [Security](./security.md).\n- **`@database`** + **`@collection`** declare a ToilDB schema (below).\n\n```ts\n@rest('players')\nclass Players {\n @get('/:id')\n public get(ctx: RouteContext): Response {\n const id = ctx.param('id');\n return Response.json(`{\"id\":\"${id}\"}`);\n }\n}\n```\n\n**`@auth` rejects with 401 before your handler runs.** So inside an `@auth`-guarded handler, `AuthService.getUser()!` is guaranteed non-null and safe to assert. Without `@auth`, `getUser()` can be null, so null-check it instead. Either way the server re-verifies the signed session, so it (not the client cookie) is the real authorization boundary.\n\n```ts\n@auth\n@get('/settings')\npublic settings(): Response {\n const user = AuthService.getUser()!; // safe: @auth guarantees a session\n return Response.text('hi ' + user.username);\n}\n```\n\n**Server code is a strict, WASM-targeted dialect (AssemblyScript), not full TypeScript.** The real constraints, verified against the toilscript standard library:\n\n- **Use explicit value types**, never `number`: `i32` / `u32` / `i64` / `u64` / `f64` (and `i8`/`u8`/`i16`/`u16`/`f32`, plus `u128`..`u256`), `bool`, `string`, `Uint8Array`. Plain `number` resolves to `f64`, which is wrong for ids and counts. Integer math **wraps** on overflow (it never throws), and integer `/` truncates. See [The type system](./types.md).\n- **No `any`.** Every value has a concrete type; that is what lets it compile to WASM.\n- **No arbitrary npm.** Only the toilscript standard library plus the toiljs host APIs.\n- **Use `null`, not `undefined`** for \"no value\" (`T | null`, narrowed with `if (x != null)` or a `!` assertion).\n- **No usable built-in `RegExp`** (it is a host stub that throws). Parse strings by hand.\n- **Structured values are `@data` classes, not object literals.** Encode and decode raw binary with `DataWriter` / `DataReader` (imported on the server from the `data` module: `import { DataWriter, DataReader } from 'data';`). For dynamic JSON, use the ambient `JSON` value tree.\n- **Read config with `Environment.get(key)` and secrets with `Environment.getSecure(key)`** (each returns `string | null`; the two buckets are disjoint so a secret can never leak through `get`). See [Environment](../services/environment.md).\n- **Return a runtime `Response`** (`Response.json` / `.text` / `.html` / `.bytes` / `.notFound` / ...), or return a `@data` value and let toiljs serialize it.\n\n**ToilDB has seven collection families; pick the one that matches the job** (do not force everything into one). Declare each as a `static` `@collection` field on a `@database` class, typed by its family, and reach it statically (`AppDb.users.get(...)`). See [The database](../database/README.md) and [Setup](../database/setup.md).\n\n- **`Documents<K,V>`**: the default record store, looked up by id (users, posts, orders).\n- **`Unique<K,V>`**: a globally one-of-a-kind claim (usernames, emails, slugs).\n- **`Counter<K>`**: a running total many callers bump at once (likes, views); `add` a delta only.\n- **`Events<K,V>`**: an append-only log kept in order (feeds, audit trails).\n- **`Capacity<K>`**: a limited quantity handed out without overselling (tickets, seats).\n- **`Membership<K,M>`**: sets of who belongs to what (followers, tags, room members).\n- **`View<K,V>`**: a precomputed read-optimized snapshot (leaderboards, home pages).\n\n**Data access is gated by function kind.** A `@get` route is a read-only **Query**; a `@post` route is an **Action** (may write); a plain `@remote` defaults to read-only Query, so add **`@action`** to let it write. Scans (`events.latest`, `membership.list`) are barred from request handlers: do them in a `@derive` and have the request read the resulting `View`. The compiler enforces this and the edge re-checks it.\n\n## Common mistakes\n\nA short do-not list. Every one of these is a real, avoidable error:\n\n- **Raw `fetch` to your own backend.** Go through `Server.*` instead.\n- **Importing `Toil.*`.** They are ambient globals; importing them is wrong.\n- **Fetching page data in a `useEffect`.** Use a route `loader` + `Toil.useLoaderData`.\n- **A plain `<a>` for in-app links.** Use `Toil.Link` (a bare `<a>` triggers a full reload).\n- **More than one `@user`.** Exactly one per project.\n- **Trusting client `getUser()` for authorization.** It is display-only and forgeable; enforce on the server with `@auth`.\n- **`any`, `number`, or `RegExp` in server code.** Use explicit value types; there is no usable `RegExp`.\n- **Reordering `@data` fields** (or changing a field type) on a stored type. Field order is the layout; add at the end and use `@migrate` to evolve. See [Data types](../backend/data.md).\n- **Writing from a plain `@remote` or `@get`.** Reads are the default; a write needs `@action`.\n- **Calling a scan (`events.latest`, `membership.list`) from a request handler.** Do it in a `@derive`.\n- **Hand-editing `shared/server.ts` or `.toil/`.** They are generated; rebuild instead.\n\n## Where to go deeper\n\n- Concepts: [Decorators](./decorators.md), [Type system](./types.md), [Security](./security.md), [Compute tiers](./tiers.md).\n- Backend: [Overview](../backend/README.md), [REST](../backend/rest.md), [RPC](../backend/rpc.md), [Data types](../backend/data.md).\n- Frontend: [Overview](../frontend/README.md), [Fetching data](../frontend/data-fetching.md), [Routing](../frontend/routing.md), [Navigation](../frontend/navigation.md), [The Toil global](../frontend/toil-global.md).\n- Database: [ToilDB overview](../database/README.md), [Setup](../database/setup.md).\n- Auth: [Overview](../auth/README.md), [Usage](../auth/usage.md).\n- Services: [Environment](../services/environment.md), [Rate limiting](../services/ratelimit.md).\n</content>\n</invoke>\n",
24
24
  "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",
25
25
  "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",
@@ -50,7 +50,7 @@ export const TOIL_DOCS: Record<string, string> = {
50
50
  "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",
51
51
  "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",
52
52
  "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",
53
- "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",
53
+ "getting-started/installation.md": "# Installation\n\nInstall the toiljs command-line tool (CLI) so you can create and run projects. This takes a couple of minutes.\n\n## Why and when\n\nYou need the toiljs CLI once, before you create your first project. The CLI is the single tool you use to scaffold, run, build, and check toiljs apps. After a project exists, the same CLI is also installed inside that project, so day-to-day you can run it through your package scripts (`npm run dev`) without a global install.\n\n## Prerequisites\n\nYou need **Node.js version 24.0.0 or newer**. Node.js is the JavaScript runtime that powers the toiljs CLI, the dev server, and the build. (Your backend does not run on Node.js, but the tools that build it do.)\n\nCheck your version:\n\n```sh\nnode --version\n```\n\nIf it prints `v24.0.0` or higher, you are set. If it is older or the command is not found, install a current Node.js from [nodejs.org](https://nodejs.org), or use a version manager like [nvm](https://github.com/nvm-sh/nvm) or [fnm](https://github.com/Schniz/fnm):\n\n```sh\n# with nvm\nnvm install 24\nnvm use 24\n```\n\nYou also need a package manager. **npm** comes with Node.js, so you already have it. toiljs also supports **pnpm**, **yarn**, and **bun** if you prefer one of those.\n\n### TypeScript 6, not 7\n\ntoiljs requires **TypeScript 6** (`>=6.0.0 <7.0.0`). TypeScript 7 is not supported yet.\n\nTypeScript 7 is the native (Go) port of the compiler. It ships a much faster `tsc`, but its package no longer exports the JavaScript compiler API, whose main entry is now just `{ version, versionMajorMinor }`. toiljs reads each route's static `metadata` export through that API to bake your SEO tags into the built HTML, and the `toiljs/eslint` preset loads it too. On TypeScript 7 the metadata baking silently stops (your built pages lose their tags) and typescript-eslint fails to load at all.\n\nPin TypeScript in your `package.json`:\n\n```json\n{\n \"devDependencies\": {\n \"typescript\": \"^6.0.3\"\n }\n}\n```\n\n`toiljs doctor` flags an unsupported TypeScript, and `toiljs doctor --fix` pins it back for you. `toiljs update` will not upgrade you into TypeScript 7. Support will land once the tools toiljs builds on can read the new `typescript/unstable/*` API.\n\n## Install the CLI\n\nThe package is called `toiljs`, and it provides a command named `toiljs`. You have two ways to use it.\n\n### Option A: run it on demand with npx (no install)\n\n`npx` comes with npm and runs a package without installing it globally. This is the quickest way to create your first project:\n\n```sh\nnpx toiljs create my-app\n```\n\n### Option B: install it globally\n\nIf you plan to create projects often, install it once so `toiljs` is always on your path:\n\n```sh\nnpm install -g toiljs\n```\n\nThen you can run `toiljs` directly:\n\n```sh\ntoiljs create my-app\n```\n\nBoth options end up at the same place. The rest of these docs write `toiljs <command>`; if you did not install globally, just put `npx` in front (`npx toiljs <command>`).\n\n## Verify it works\n\nCheck the version:\n\n```sh\ntoiljs --version\n```\n\nYou should see a version number printed (for example, `0.0.86`).\n\nTo see every command and flag, run help:\n\n```sh\ntoiljs --help\n```\n\nInside a project, there is a deeper health check called **doctor**. It inspects your setup and dependencies and tells you exactly what to fix. You will use it after you create a project, but it is good to know it exists:\n\n```sh\ntoiljs doctor\n```\n\n`toiljs doctor` reads your project (its `package.json`, config, routes, and build output), runs a series of checks, and prints a grouped report. One of the first checks is your Node.js version against the required `>=24.0.0`, so if your Node is too old, doctor will say so. Add `--fix` to let it repair the things it can (such as the typed-RPC wiring), or `--json` for machine-readable output in a CI pipeline.\n\n## How the tooling fits together\n\nThe `toiljs` CLI is a friendly front end over two underlying tools. You rarely call them directly, but it helps to know they exist.\n\n```mermaid\nflowchart LR\n CLI[\"toiljs CLI\"] --> TS[\"toilscript<br/>(compiles server/ to wasm)\"]\n CLI --> VITE[\"Vite<br/>(bundles client/ React)\"]\n TS --> W[\"build/server/release.wasm\"]\n VITE --> B[\"build/client/\"]\n```\n\nWhen you create a project, both `toiljs` and `toilscript` are added to it as dependencies, so everything is pinned to versions that work together. You do not install `toilscript` separately.\n\n## Gotchas and notes\n\n- **\"command not found: toiljs\"** after a global install usually means npm's global bin folder is not on your `PATH`. Either fix your `PATH` or just use `npx toiljs ...` instead.\n- **Node too old** is the most common first-time failure. WebAssembly tooling and the build depend on features in Node 24 and up. Upgrade before creating a project.\n- **Every command checks for updates.** On each run, the CLI quietly checks npm for a newer toiljs and prints a note if you are behind. It never blocks the command. To turn it off, set the environment variable `TOILJS_NO_UPDATE_CHECK=1`.\n- You do **not** need to install a database, a Docker container, or any cloud account to develop locally. The dev server includes a local ToilDB so your data-backed features run out of the box.\n\n## Related\n\n- [Create a project](./create-project.md)\n- [The CLI reference](../cli/README.md)\n- [Getting started overview](./README.md)\n",
54
54
  "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",
55
55
  "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",
56
56
  "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",
@@ -65,7 +65,7 @@ export const TOIL_DOCS: Record<string, string> = {
65
65
  "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",
66
66
  "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",
67
67
  "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",
68
- "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",
68
+ "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 meters\n\nA few fields are not lifetime totals. They still read as `u64` (except the derived `sustainedRpsCap`, an `f64`).\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 meters** show where you sit against your plan. The model is a **sustained-rate (\"unmetered pipe\")** one, not a fixed per-minute ceiling. Your plan has a **sustained request rate** `X` (requests per second), measured over a **3-minute rolling window**. Two important consequences:\n\n- **Instantaneous spikes are free.** There is no per-tier cap on how fast you may burst inside a window; you may spike right up to the box's own hardware ceiling (the `firewall.global_pps` DDoS backstop, shared across all tenants). Nothing throttles you for a brief peak.\n- **Only sustained overuse throttles.** You get an HTTP `429` once your fleet-global request count over the *trailing* 3 minutes exceeds `X * 180` (the sustained rate integrated over the window), or once your 30-day quota is exhausted. \"Fleet-global\" means the count is summed across every edge location, not just the one machine serving you.\n\nBecause the window **rolls**, a burst ages out gradually over the following 3 minutes rather than being forgiven all at once on a boundary. The `Retry-After` on a `429` tells you exactly how long until enough of it has decayed.\n\nThe fields:\n\n- `reqBurstUsed` / `reqBurstCap`: fleet-global requests in the **trailing 3-minute rolling window**, and its allowance. This is exactly the number the edge compares against, so it never disagrees with why you were throttled. The allowance is `X * 180` (the window is `BURST_WINDOW_SECS = 180` seconds). A cap of `0` means **unmetered** (no sustained cap).\n- `req30dUsed` / `req30dCap`: fleet-global requests in the **current 30-day window**, and the 30-day quota. This one still tumbles, because it is a billing period rather than a rate. A quota of `0` means **unmetered**.\n- `sustainedRpsCap`: a convenience `f64` derived from the burst cap, `reqBurstCap / BURST_WINDOW_SECS`. This is your plan's sustained rate `X` in requests/second (`0.0` when unmetered), so you can show \"1000 rps sustained\" without doing the division yourself.\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: nearly every getter is a `u64` and maps to a `u64` field with no cast; the only `f64` values are the derived `cacheRatio` and `sustainedRpsCap`. 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 two derived f64s (cacheRatio, sustainedRpsCap). */\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 meters (cap 0 = unmetered)\n reqBurstUsed: u64 = 0; // requests in the current 3-minute sustained-rate bucket\n reqBurstCap: u64 = 0; // per-bucket cap = sustained_rps * 180\n req30dUsed: u64 = 0; // requests in the current 30-day window\n req30dCap: u64 = 0; // 30-day quota\n sustainedRpsCap: f64 = 0; // derived: reqBurstCap / 180 (the plan's sustained rps)\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.reqBurstUsed = s.reqBurstUsed;\n out.reqBurstCap = s.reqBurstCap;\n out.req30dUsed = s.req30dUsed;\n out.req30dCap = s.req30dCap;\n out.sustainedRpsCap = s.sustainedRpsCap;\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 non-integer fields are `cacheRatio` and `sustainedRpsCap` (and `Series.ratePerSec`), which are `f64`.\n- **Spikes are free; only sustained overuse throttles.** The request meters follow a sustained-rate model: you may burst up to the box's hardware ceiling, and throttling (`429`) starts only when the current 3-minute bucket passes `sustainedRpsCap * 180`, or when the 30-day quota runs out. A cap or quota of `0` means unmetered.\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",
69
69
  "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",
70
70
  "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') returns \"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",
71
71
  "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",