| 1 |
# Working on this app |
| 2 |
|
| 3 |
You are one of the crew on this repository. The forge runs the app for you: |
| 4 |
production follows the default branch, and every pull you open gets a |
| 5 |
preview at its own address, deployed from your branch as you push. |
| 6 |
|
| 7 |
## The rules |
| 8 |
|
| 9 |
- Work on the branch you were given. You cannot move the default branch; |
| 10 |
a person merges your pull once it is reviewed. |
| 11 |
- Keep state under `FORGE_APP_DATA`. Never write elsewhere at runtime. |
| 12 |
- Run `bun test` before you push. `forge.json`'s `app.test` runs it beside |
| 13 |
the app on every deploy, not before it: the preview comes up the moment |
| 14 |
the app answers and says `live`, or `serving · tests failed` the moment |
| 15 |
the suite disagrees. Every behaviour you add gets a test that would fail |
| 16 |
without it. |
| 17 |
- `src/tty.ts` is the app's terminal. The exact command that reaches it is |
| 18 |
`FORGE_APP_SSH` at runtime; print that verbatim wherever the app tells |
| 19 |
people how to connect. What a person can do on the page they can do here, |
| 20 |
and nothing more: the terminal gates its reads and writes exactly as the |
| 21 |
page does (`FORGE_APP_USER_ROLE` is the forge's word on who is there), and a |
| 22 |
write made there reaches web viewers live like any other. |
| 23 |
- Keep it dependency-free unless a dependency is clearly worth it, and say |
| 24 |
why in the pull. A dependency is code you now maintain. |
| 25 |
- When you are done, push and stop. Your last message becomes the pull's |
| 26 |
description: say what changed, what you chose not to do, and what to look |
| 27 |
at in the preview. |
| 28 |
|
| 29 |
## The shape |
| 30 |
|
| 31 |
One implementation, many doors. Every operation is a function in |
| 32 |
`src/actions.ts` — `(db, who, input)` in, data or a named failure out — and |
| 33 |
the page (`src/routes.ts`, `src/page.ts`), the JSON API (`src/routes.ts`), |
| 34 |
the MCP server (`src/mcp.ts`) and the terminal (`src/tty.ts`) call it. A |
| 35 |
door reads its wire format and shapes the answer; it decides nothing. A |
| 36 |
rule in an action holds at every door at once; a rule anywhere else is a |
| 37 |
bug waiting for the door you forgot. |
| 38 |
|
| 39 |
Identity comes from the forge (`src/who.ts`): `anon`, `user` or `admin`, |
| 40 |
verified by signature, never by a cookie the app invents. |
| 41 |
|
| 42 |
Every request passes the door (`src/security.ts`) before a route sees it: |
| 43 |
writes are rationed, a write from another site is refused, and a request |
| 44 |
under a resource's `api` path is refused — before its body is read — when |
| 45 |
the table lets that person through that door for no row at all. The door |
| 46 |
asks the same table the actions do; it never compares roles by hand, and |
| 47 |
the action still decides about the real row. Do not route around it. |
| 48 |
|
| 49 |
Every change is written down. An action that creates, updates or deletes |
| 50 |
returns through `audited` (`src/actions.ts`), which appends a row to the |
| 51 |
`audit` table — who, the resource and verb, the row, when, and whether |
| 52 |
they were let — refused attempts included; the door records the writes it |
| 53 |
turns away. Admins read it at `GET /api/audit` (the `audit` resource in |
| 54 |
`contract.json`). Recording never fails a request: a row that cannot be |
| 55 |
written is a log line. |
| 56 |
|
| 57 |
Who may do what is a table, not code: `contract.json`. It declares the |
| 58 |
roles — each names the roles below it, and how it is held: by everyone, by |
| 59 |
the identity the forge signed, or by a row's column naming the person — and, |
| 60 |
for every resource, the least role each of create, read, update, delete and |
| 61 |
list needs. An action asks `permit(who, resource, verb, row)` |
| 62 |
(`src/contract.ts`) and obeys the answer; it never compares `who.role` |
| 63 |
itself. `src/can.ts` is the forge's evaluator, carried verbatim: do not |
| 64 |
edit it. `src/contract.test.ts` acts as every identity at every door and |
| 65 |
fails when a door and the table disagree. |
| 66 |
|
| 67 |
A new kind of person is a new role in the table, under an existing one. A |
| 68 |
change to a cell is a change of policy: say so in the pull's description. |
| 69 |
A cell loosened — anything an identity could not do before that it can |
| 70 |
after — is a wall: the forge shows the pull to a person and never lands it |
| 71 |
on its own. Do not loosen a cell unless the issue asks for exactly that; |
| 72 |
prefer a new role under an existing one, or a new resource, to widening a |
| 73 |
door that was decided. |
| 74 |
|
| 75 |
To add a concept: |
| 76 |
|
| 77 |
- **An action** — a function in `src/actions.ts` that asks `permit`, its |
| 78 |
resource's row in `contract.json`, its door in `src/contract.test.ts`, |
| 79 |
and a test in `src/actions.test.ts` for what the table cannot say. |
| 80 |
- **A migration** — append to `migrations` in `src/db.ts`. Never edit one |
| 81 |
that has run. `bun run src/db.ts --check` shows the version. |
| 82 |
- **A page** — a row in `src/routes.ts` and a function in `src/page.ts`. |
| 83 |
With a second page, move them under `src/pages/` and keep one `layout`. |
| 84 |
- **An API route** — a row in `src/routes.ts` that calls the action and |
| 85 |
answers with `answer(...)`; the status follows the failure. Give it an |
| 86 |
`api` entry beside it (a sentence, the body and answer as JSON Schema) |
| 87 |
and run `bun run contract:emit`: `contract/openapi.json` is committed, |
| 88 |
served at `/openapi.json`, and `bun test` fails while it is stale. A |
| 89 |
resource's routes live under the `api` path its `contract.json` entry |
| 90 |
names, so the door and the forge's probe find them. |
| 91 |
- **A tool** — an entry in `tools` in `src/mcp.ts`: a name, one sentence, |
| 92 |
a JSON schema, the action. |
| 93 |
- **A terminal command** — a key in `src/tty.ts` that calls the action and |
| 94 |
reports on the status line. |
| 95 |
- **Something live** — `emit(type, data)` in the action, and |
| 96 |
`data-live="<types>"` on the region of the page that shows it. |
| 97 |
- **A colour, a space, a size** — a token in `design/tokens.json` (a |
| 98 |
colour needs its dark value in `$extensions.mode.dark`), used as |
| 99 |
`var(--…)`. `bun run lint:design` refuses a raw `#hex`, `rgb(` or |
| 100 |
`hsl(` anywhere in `src/` or `static/`, and a `var(--…)` that names no |
| 101 |
token; `src/design.test.ts` runs it, and holds every pair of colours a |
| 102 |
person reads to 4.5:1 in both themes. |
| 103 |
|
| 104 |
## The bar |
| 105 |
|
| 106 |
- Security: every input is validated at the boundary and every output is |
| 107 |
escaped; every response carries the floor in `src/http.ts` (keep it, |
| 108 |
extend it); a POST that is not same-origin is refused; writes are |
| 109 |
rationed per person; secrets come from the environment (Settings → App → |
| 110 |
Environment), never the repository; no `eval`, no shell built from user |
| 111 |
text. A request body is capped by the bytes actually read (`readBody`), |
| 112 |
never by a Content-Length. `HEAD` answers like `GET`. A private thing |
| 113 |
needs an identity from `who`, and a cookie the app sets itself is |
| 114 |
`HttpOnly; SameSite=Lax`. |
| 115 |
- Speed: the first paint is server-rendered HTML; JavaScript is small, in |
| 116 |
`static/`, linked by content (`asset()`) and cached for a year; no |
| 117 |
layout shift; what changes arrives over `/events`, never by polling; |
| 118 |
work that can wait is done after the response. |
| 119 |
- A phone is a first-class client: the page works at 360px wide with no |
| 120 |
horizontal scroll, every control is at least 44px on a side |
| 121 |
(`--size-tap`), an input's text is never below 16px (a phone zooms into |
| 122 |
a smaller one), focus is always visible, whoever asked for less motion |
| 123 |
gets none, and the keyboard reaches everything a mouse does. |
| 124 |
- Beauty is the tokens': light and dark both, from one file. A new page |
| 125 |
uses `layout`, which carries the tokens, the description and the Open |
| 126 |
Graph and card metadata a shared link unfurls with — pass the page's |
| 127 |
path so `og:url` is its own. |
| 128 |
- Finish: the app boots, `/healthz` answers `{ ok, version, uptime }` |
| 129 |
(the forge polls it; keep it cheap and apart from the page), `/` |
| 130 |
answers, `bun test` is green, the README says how to use it and what is |
| 131 |
real versus simulated, `forge.json` says how to run it. |