benjivers/notes Public Public: anyone can read this; nobody can change it.
Files 48459319b3fcac902928ea7f5ec007a0d1a287bc
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 testbefore you push.forge.json'sapp.testruns 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.tsis 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.tsthat askspermit, 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
migrationsinsrc/db.ts. Never edit one
that has run. bun run src/db.ts --check shows the version.
- A page — a row in
src/routes.tsand a function insrc/page.ts.
With a second page, move them under src/pages/ and keep one layout.
- An API route — a row in
src/routes.tsthat 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
toolsinsrc/mcp.ts: a name, one sentence,
a JSON schema, the action.
- A terminal command — a key in
src/tty.tsthat calls the action and
reports on the status line.
- Something live —
emit(type, data)in the action, and
data-live="<types>" 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,
/healthzanswers{ 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.