# Working on this app You are one of the crew on this repository. The forge runs the app for you: production follows the default branch, and every pull you open gets a preview at its own address, deployed from your branch as you push. ## The rules - Work on the branch you were given. You cannot move the default branch; a person merges your pull once it is reviewed. - Keep state under `FORGE_APP_DATA`. Never write elsewhere at runtime. - Run `bun test` before you push. `forge.json`'s `app.test` runs it beside the app on every deploy, not before it: the preview comes up the moment the app answers and says `live`, or `serving · tests failed` the moment the suite disagrees. Every behaviour you add gets a test that would fail without it. - `src/tty.ts` is the app's terminal. The exact command that reaches it is `FORGE_APP_SSH` at runtime; print that verbatim wherever the app tells people how to connect. What a person can do on the page they can do here, and nothing more: the terminal gates its reads and writes exactly as the page does (`FORGE_APP_USER_ROLE` is the forge's word on who is there), and a write made there reaches web viewers live like any other. - Keep it dependency-free unless a dependency is clearly worth it, and say why in the pull. A dependency is code you now maintain. - When you are done, push and stop. Your last message becomes the pull's description: say what changed, what you chose not to do, and what to look at in the preview. ## The shape One implementation, many doors. Every operation is a function in `src/actions.ts` — `(db, who, input)` in, data or a named failure out — and the page (`src/routes.ts`, `src/page.ts`), the JSON API (`src/routes.ts`), the MCP server (`src/mcp.ts`) and the terminal (`src/tty.ts`) call it. A door reads its wire format and shapes the answer; it decides nothing. A rule in an action holds at every door at once; a rule anywhere else is a bug waiting for the door you forgot. Identity comes from the forge (`src/who.ts`): `anon`, `user` or `admin`, verified by signature, never by a cookie the app invents. Every request passes the door (`src/security.ts`) before a route sees it: writes are rationed, a write from another site is refused, and a request under a resource's `api` path is refused — before its body is read — when the table lets that person through that door for no row at all. The door asks the same table the actions do; it never compares roles by hand, and the action still decides about the real row. Do not route around it. Every change is written down. An action that creates, updates or deletes returns through `audited` (`src/actions.ts`), which appends a row to the `audit` table — who, the resource and verb, the row, when, and whether they were let — refused attempts included; the door records the writes it turns away. Admins read it at `GET /api/audit` (the `audit` resource in `contract.json`). Recording never fails a request: a row that cannot be written is a log line. Who may do what is a table, not code: `contract.json`. It declares the roles — each names the roles below it, and how it is held: by everyone, by the identity the forge signed, or by a row's column naming the person — and, for every resource, the least role each of create, read, update, delete and list needs. An action asks `permit(who, resource, verb, row)` (`src/contract.ts`) and obeys the answer; it never compares `who.role` itself. `src/can.ts` is the forge's evaluator, carried verbatim: do not edit it. `src/contract.test.ts` acts as every identity at every door and fails when a door and the table disagree. A new kind of person is a new role in the table, under an existing one. A change to a cell is a change of policy: say so in the pull's description. A cell loosened — anything an identity could not do before that it can after — is a wall: the forge shows the pull to a person and never lands it on its own. Do not loosen a cell unless the issue asks for exactly that; prefer a new role under an existing one, or a new resource, to widening a door that was decided. To add a concept: - **An action** — a function in `src/actions.ts` that asks `permit`, its resource's row in `contract.json`, its door in `src/contract.test.ts`, and a test in `src/actions.test.ts` for what the table cannot say. - **A migration** — append to `migrations` in `src/db.ts`. Never edit one that has run. `bun run src/db.ts --check` shows the version. - **A page** — a row in `src/routes.ts` and a function in `src/page.ts`. With a second page, move them under `src/pages/` and keep one `layout`. - **An API route** — a row in `src/routes.ts` that calls the action and answers with `answer(...)`; the status follows the failure. Give it an `api` entry beside it (a sentence, the body and answer as JSON Schema) and run `bun run contract:emit`: `contract/openapi.json` is committed, served at `/openapi.json`, and `bun test` fails while it is stale. A resource's routes live under the `api` path its `contract.json` entry names, so the door and the forge's probe find them. - **A tool** — an entry in `tools` in `src/mcp.ts`: a name, one sentence, a JSON schema, the action. - **A terminal command** — a key in `src/tty.ts` that calls the action and reports on the status line. - **Something live** — `emit(type, data)` in the action, and `data-live=""` on the region of the page that shows it. - **A colour, a space, a size** — a token in `design/tokens.json` (a colour needs its dark value in `$extensions.mode.dark`), used as `var(--…)`. `bun run lint:design` refuses a raw `#hex`, `rgb(` or `hsl(` anywhere in `src/` or `static/`, and a `var(--…)` that names no token; `src/design.test.ts` runs it, and holds every pair of colours a person reads to 4.5:1 in both themes. ## The bar - Security: every input is validated at the boundary and every output is escaped; every response carries the floor in `src/http.ts` (keep it, extend it); a POST that is not same-origin is refused; writes are rationed per person; secrets come from the environment (Settings → App → Environment), never the repository; no `eval`, no shell built from user text. A request body is capped by the bytes actually read (`readBody`), never by a Content-Length. `HEAD` answers like `GET`. A private thing needs an identity from `who`, and a cookie the app sets itself is `HttpOnly; SameSite=Lax`. - Speed: the first paint is server-rendered HTML; JavaScript is small, in `static/`, linked by content (`asset()`) and cached for a year; no layout shift; what changes arrives over `/events`, never by polling; work that can wait is done after the response. - A phone is a first-class client: the page works at 360px wide with no horizontal scroll, every control is at least 44px on a side (`--size-tap`), an input's text is never below 16px (a phone zooms into a smaller one), focus is always visible, whoever asked for less motion gets none, and the keyboard reaches everything a mouse does. - Beauty is the tokens': light and dark both, from one file. A new page uses `layout`, which carries the tokens, the description and the Open Graph and card metadata a shared link unfurls with — pass the page's path so `og:url` is its own. - Finish: the app boots, `/healthz` answers `{ ok, version, uptime }` (the forge polls it; keep it cheap and apart from the page), `/` answers, `bun test` is green, the README says how to use it and what is real versus simulated, `forge.json` says how to run it.