| 1 |
# glyph |
| 2 |
|
| 3 |
Type a prompt, get SVG logos and icons drawn by Claude; preview in your theme, copy or download. |
| 4 |
|
| 5 |
Describe something — "a hex chip with a circuit b", "a lighthouse for a |
| 6 |
monitoring app" — pick a kind (logo mark, app icon, badge, spot |
| 7 |
illustration), and Claude draws four different takes as SVG in about a |
| 8 |
minute. Switch palette (Sage, Moss, Neon, Mono, Blueprint) to see them in |
| 9 |
any theme, then **Copy** the SVG or **Save** it as a file in that palette. |
| 10 |
|
| 11 |
## What is real |
| 12 |
|
| 13 |
- **The drawing is real**: the app runs Claude Code (`claude -p`, no tools, |
| 14 |
model `claude-opus-5-5`) with the token in `CLAUDE_CODE_OAUTH_TOKEN`. Set it |
| 15 |
in Settings → App → Environment (make one with `claude setup-token`). |
| 16 |
Without it a drawing fails at once with a sentence saying exactly that. |
| 17 |
- **Every SVG is rebuilt from an allowlist** (`src/glyph.ts` `sanitize`): |
| 18 |
shapes, local gradients and clip paths, `title` and plain attributes |
| 19 |
only. No script, style, link, image, `url(…)` other than one pointing at |
| 20 |
a gradient or clip inside the same SVG (`url(#id)`), or event handler |
| 21 |
survives, and previews are `<img>` data URIs, so |
| 22 |
nothing in a drawing can run on the page. A saved file is served with a |
| 23 |
sandboxing CSP. |
| 24 |
- **Colours**: Claude draws in three placeholder colours (ink, glow and |
| 25 |
paper, `design/palettes.json`) and builds depth from gradients and |
| 26 |
opacity between them; a palette swaps ink and glow for its own and paper |
| 27 |
for its ground. Downloads carry the palette's real colours. Drawings are |
| 28 |
asked for on a 256-unit canvas with layered, detailed shapes, so a |
| 29 |
drawing takes a little longer than a flat one. |
| 30 |
- **Drawings are private to whoever asked** (`contract.json`); an admin sees |
| 31 |
everyone's. One drawing at a time per person, three at once for everyone. |
| 32 |
A drawing left half done by a restart is marked failed with a reason. |
| 33 |
- Also an API (`/api/drawings`, `POST` answers `202` then the row fills in; |
| 34 |
listen on `/events` for `drawing.changed`), MCP tools (`draw`, |
| 35 |
`get_drawing`, `list_drawings`, `delete_drawing`) and a terminal (list and |
| 36 |
ask; a terminal can't show pictures). |
| 37 |
- On the forge an app reaches the internet only through its egress proxy: |
| 38 |
`forge.json` declares `api.anthropic.com` and `claude.ai`. |
| 39 |
|
| 40 |
This is an app: the forge runs it. Production follows the default branch; |
| 41 |
every open pull gets a preview at its own address with its own copy of the |
| 42 |
data. Everything the app keeps lives under `FORGE_APP_DATA`, which is what |
| 43 |
makes "production data" and "preview data" two different things. |
| 44 |
|
| 45 |
```sh |
| 46 |
PORT=8080 FORGE_APP_DATA=./data bun run src/server.ts |
| 47 |
bun test |
| 48 |
FORGE_APP_DATA=./data bun run src/tty.ts |
| 49 |
FORGE_APP_DATA=./data bun run src/db.ts --check |
| 50 |
bun run lint:design |
| 51 |
bun run contract:emit |
| 52 |
``` |
| 53 |
|
| 54 |
`app.test` in `forge.json` runs beside `app.run` in every deploy, not before |
| 55 |
it: the preview comes up the moment the app answers, and says `live`, or |
| 56 |
`serving · tests failed` the moment the suite disagrees. |
| 57 |
|
| 58 |
## The shape |
| 59 |
|
| 60 |
One implementation, many doors. `src/actions.ts` is every operation as a |
| 61 |
plain function — `(db, who, input)` in, data or a named failure out — and |
| 62 |
the page, the JSON API, the MCP server and the terminal all call it and |
| 63 |
decide nothing themselves. `AGENTS.md` says how to add to it. |
| 64 |
|
| 65 |
| file | what it is | |
| 66 |
| ---------------------------------- | -------------------------------------------------------------- | |
| 67 |
| `src/actions.ts` | start, list, read, delete a drawing; the audit trail, read | |
| 68 |
| `src/glyph.ts` | the ask, the reply read back, the SVG allowlist, palettes | |
| 69 |
| `src/draw.ts`, `src/claude.ts` | the work after the response: Claude Code, run with no tools | |
| 70 |
| `design/palettes.json` | the drawing palettes and the three placeholder colours | |
| 71 |
| `contract.json` | who may do what: roles, and the least role each verb needs | |
| 72 |
| `src/contract.ts`, `src/can.ts` | the table, read at boot; the forge's evaluator, verbatim | |
| 73 |
| `src/security.ts` | the door: rationing, same origin, the table before the body | |
| 74 |
| `src/audit.ts` | every change and every refused one, as a row | |
| 75 |
| `src/who.ts` | who is asking, from the forge's signed headers | |
| 76 |
| `src/db.ts` | SQLite under `FORGE_APP_DATA`, WAL, forward-only migrations | |
| 77 |
| `src/live.ts` | the in-process bus and `GET /events` | |
| 78 |
| `src/mcp.ts` | the actions as MCP tools at `POST /mcp` | |
| 79 |
| `src/routes.ts` | every path, in one table, each API route described beside it | |
| 80 |
| `src/openapi.ts` | the routes as OpenAPI; committed at `contract/openapi.json` | |
| 81 |
| `src/page.ts` | the HTML, rendered on the server, with its share metadata | |
| 82 |
| `design/tokens.json`, `src/tokens.ts` | the look as data, light and dark; compiled to `/tokens.css` | |
| 83 |
| `src/http.ts` | the floor: headers, body cap, files | |
| 84 |
| `src/server.ts` | boot: open the database, serve | |
| 85 |
| `src/tty.ts` | the terminal, over the same actions | |
| 86 |
| `src/env.ts` | what the forge told this process | |
| 87 |
| `static/app.css`, `static/live.js` | the look in the tokens' words, and the board that stays current | |
| 88 |
| `static/glyph.js` | copy, the palette swapped in place, the example prompts | |
| 89 |
|
| 90 |
## What every app stands on |
| 91 |
|
| 92 |
- **The door.** Before a route sees a request, `src/security.ts` rations |
| 93 |
writes (and a stranger's API calls), refuses a write from another site, |
| 94 |
and turns away — before the body is read — anyone the table lets through |
| 95 |
a resource's door for no row at all: 401 for a stranger, 403 for anyone |
| 96 |
else. The action still decides about the real row. |
| 97 |
- **The audit trail.** Every change, and every change refused, is a row: |
| 98 |
who, which resource and verb, which row, when, and whether they were |
| 99 |
let. Admins read it at `GET /api/audit`. A row that cannot be written |
| 100 |
is a log line, never a failed request. |
| 101 |
- **The API, described.** `GET /openapi.json` is `contract/openapi.json`, |
| 102 |
written from the routes by `bun run contract:emit`; each operation names |
| 103 |
its cell of the table (`x-contract`). `bun test` fails while it is stale. |
| 104 |
- **Up, or not.** `GET /healthz` answers `{ ok, version, uptime }`; the |
| 105 |
forge waits on it before it sends anyone to a deploy. |
| 106 |
- **The look.** `design/tokens.json` holds every colour, space and size, |
| 107 |
light and dark; nothing else spells out a colour (`bun run lint:design`), |
| 108 |
and every pair a person reads meets 4.5:1 in both themes. |
| 109 |
- **The open web.** A shared link unfurls (description, Open Graph, a |
| 110 |
card); `/robots.txt` keeps crawlers to the page, and out of previews |
| 111 |
entirely; `/.well-known/security.txt` sends a reporter to the forge |
| 112 |
that runs the app. |
| 113 |
|
| 114 |
## Who you are |
| 115 |
|
| 116 |
The forge says who is asking on every request it proxies, and the app |
| 117 |
verifies the signature (`src/who.ts`); nothing else is trusted. Three tiers: |
| 118 |
|
| 119 |
- **anon** — anyone. Sees the page and is asked to sign in; nothing else. |
| 120 |
- **user** — signed in to the forge and able to see this app. Draws, as |
| 121 |
themselves, and sees and removes their own drawings. |
| 122 |
- **admin** — able to write this repository. Sees and removes any drawing. |
| 123 |
|
| 124 |
"Sign in" is `/__forge/signin?to=<path>` and "Sign out" is |
| 125 |
`/__forge/signout`; the forge answers both on the app's own host, and |
| 126 |
`/__forge/report` too: it sends anyone, with no account, to report a |
| 127 |
problem on this app's repository, if that repository is public. |
| 128 |
|
| 129 |
## Any interface |
| 130 |
|
| 131 |
The same actions, the same rules, at every door. A script is the person |
| 132 |
whose forge grant it carries; a bare script is anonymous. |
| 133 |
|
| 134 |
```sh |
| 135 |
curl -X POST https://<app>/api/drawings -H 'content-type: application/json' -d '{"prompt":"a fox made of circuit traces","kind":"mark"}' |
| 136 |
curl https://<app>/api/drawings/1 |
| 137 |
curl -X DELETE https://<app>/api/drawings/1 |
| 138 |
``` |
| 139 |
|
| 140 |
As an MCP server, for Claude Code or Claude Desktop — `draw`, |
| 141 |
`get_drawing`, `list_drawings`, `delete_drawing`: |
| 142 |
|
| 143 |
```sh |
| 144 |
claude mcp add --transport http glyph https://<app>/mcp |
| 145 |
``` |
| 146 |
|
| 147 |
```json |
| 148 |
{ |
| 149 |
"mcpServers": { "glyph": { "type": "http", "url": "https://<app>/mcp" } } |
| 150 |
} |
| 151 |
``` |
| 152 |
|
| 153 |
The page stays current over `/events`: a drawing fills in on its own when |
| 154 |
Claude is done, without a reload. Without JavaScript it is the same page, one |
| 155 |
reload behind. |
| 156 |
|
| 157 |
The terminal opens with the exact command in `FORGE_APP_SSH` when the app |
| 158 |
runs (`ssh -p 2222 <name>@<forge>` — the app is the login name, the forge |
| 159 |
the host; `<name>--<pull>@<forge>` for a preview's). It is `src/tty.ts` on |
| 160 |
a real pty over the same data, as the account at the keyboard. |
| 161 |
|
| 162 |
The forge sets `PORT`, `FORGE_APP_DATA`, `FORGE_APP_ENV` (`production` or |
| 163 |
`preview`), `FORGE_APP_URL`, `FORGE_APP_COMMIT`, `FORGE_APP_SSH` and |
| 164 |
`FORGE_APP_IDENTITY_KEY` when it starts the app. `forge.json` says how. |