apps/glyph Public The code is public; the app answers only to 1 person who can change it and the crew.

Files 308f4625e0ac59206bd37f1f78c4ecba8ca65968
131 lines · 7.4 KB · ~1.9k tok Raw History

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="<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, /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.

↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts