# notes A personal linked-notes vault, in the spirit of Obsidian — markdown, `[[wikilinks]]`, backlinks, tags and a graph — kept on the forge. ## Using it Open the app and **create a vault** with an email and password, or sign in with a forge account. Your notes are **yours alone**: the contract gives every verb on a note to its author, so nobody else who uses the app — admins included — can list, read or change them. **Email accounts** (`src/accounts.ts`) are the app's own: argon2id password hashes, a random session token in an HttpOnly cookie (only its SHA-256 is stored), 30-day sessions, log-in attempts rationed per email. The account page changes the password (ending every other session) or deletes the account with all its notes. There is no email-based reset — the app sends no mail. Set `NOTES_SIGNUP_CODE` in Settings → App → Environment to require a code for new accounts (closing open sign-up without a deploy). A vault holds at most 5,000 notes. Email accounts reach the page and the JSON API; the MCP endpoint and the terminal are for forge accounts. - **Write** in markdown. It saves as you type (and with ⌘S). Edit, Split (side by side) and Read views; ⌘E flips between reading and writing. - **Link** with `[[Another note]]`, `[[Note|shown text]]` or `[[Note#Heading]]`. Typing `[[` suggests titles. A link to a note not written yet is drawn dashed; following it creates the note. - **Backlinks**: the right panel shows every note that links here, with the line it said it in, plus links out, the outline, tags and a local graph. - **Rename** a note and every link to it in your vault is rewritten. - **Tags**: `#tag`, nested as `#area/topic`. Click one to see every note carrying it (`#area` also finds `#area/topic`). - **Search**: words, `"a phrase"`, `#tag` or `tag:name`, combined. - **Graph**: every note and link, force-laid-out. Drag, zoom, click to open. - **Daily note**: the calendar button (⌘⇧D) opens today's `YYYY-MM-DD` note. - **Quick switcher**: ⌘K / Ctrl K — jump to a note, or ⇧↵ to create one. - Editor keys: ⌘B bold, ⌘I italic, ⌘⇧H highlight, ⌘↵ cycles a line through task / done / plain, Enter continues a list, Tab indents it. - Markdown: headings, emphasis, `==highlight==`, `~~strike~~`, code, fenced blocks, lists, `- [ ]` tasks (tick them in the reading view), quotes, Obsidian callouts (`> [!tip] Title`), tables, links. - **Your data**: every note downloads as `.md`; the sidebar's download button exports the whole vault as JSON. Everything is real: notes live in SQLite under `FORGE_APP_DATA`. A preview of a pull gets a copy of production data; production is the default branch. ## Claude can use it too `POST /mcp` is an MCP server with `list_notes`, `search_notes`, `read_note`, `create_note`, `update_note`, `append_to_note` (with no title: today's daily note) and `delete_note` — as whoever the forge grant belongs to: ```sh claude mcp add --transport http notes https://notes--forge-apps.benjivers.com/mcp ``` The terminal (`FORGE_APP_SSH`) lists, reads and searches the vault, adds a line to today's note (`a`) and makes new notes (`n`). ## Running it here ```sh PORT=8080 FORGE_APP_DATA=./data bun run src/server.ts # signed out without FORGE_APP_IDENTITY_KEY bun test # what forge.json's app.test runs on every push FORGE_APP_DATA=./data bun run src/tty.ts # the terminal bun run lint:design # no colour but the tokens' bun run contract:emit # rewrite contract/openapi.json from the routes ``` ## The shape One implementation, many doors. `src/actions.ts` is every operation as a plain function — `(db, who, input)` in, data or a named failure out — and the page, the JSON API, the MCP server and the terminal all call it and decide nothing themselves. `AGENTS.md` says how to add to it. | file | what it is | | ---------------------------------- | -------------------------------------------------------------- | | `src/actions.ts` | notes: list, add, rename (relinks), pin, delete, search; audit | | `src/vault.ts` | links, backlinks, tags, graph, search — pure, over notes | | `static/md.js` | the markdown renderer, shared by the server and the browser | | `contract.json` | who may do what: roles, and the least role each verb needs | | `src/contract.ts`, `src/can.ts` | the table, read at boot; the forge's evaluator, verbatim | | `src/security.ts` | the door: rationing, same origin, the table before the body | | `src/audit.ts` | every change and every refused one, as a row | | `src/who.ts` | who is asking, from the forge's signed headers | | `src/db.ts` | SQLite under `FORGE_APP_DATA`, WAL, forward-only migrations | | `src/live.ts` | the in-process bus and `GET /events` | | `src/mcp.ts` | the actions as MCP tools at `POST /mcp` | | `src/routes.ts` | every path, in one table, each API route described beside it | | `src/openapi.ts` | the routes as OpenAPI; committed at `contract/openapi.json` | | `src/page.ts` | the HTML, rendered on the server, with its share metadata | | `design/tokens.json`, `src/tokens.ts` | the look as data, light and dark; compiled to `/tokens.css` | | `src/http.ts` | the floor: headers, body cap, files | | `src/server.ts` | boot: open the database, serve | | `src/tty.ts` | the terminal, over the same actions | | `src/env.ts` | what the forge told this process | | `static/app.css`, `static/live.js` | the look in the tokens' words, and the panels that stay current | | `static/app.js`, `static/graph.js` | the editor, switcher and keys; the force-directed graph | ## What every app stands on - **The door.** Before a route sees a request, `src/security.ts` rations writes (and a stranger's API calls), refuses a write from another site, and turns away — before the body is read — anyone the table lets through a resource's door for no row at all: 401 for a stranger, 403 for anyone else. The action still decides about the real row. - **The audit trail.** Every change, and every change refused, is a row: who, which resource and verb, which row, when, and whether they were let. Admins read it at `GET /api/audit`. A row that cannot be written is a log line, never a failed request. - **The API, described.** `GET /openapi.json` is `contract/openapi.json`, written from the routes by `bun run contract:emit`; each operation names its cell of the table (`x-contract`). `bun test` fails while it is stale. - **Up, or not.** `GET /healthz` answers `{ ok, version, uptime }`; the forge waits on it before it sends anyone to a deploy. - **The look.** `design/tokens.json` holds every colour, space and size, light and dark; nothing else spells out a colour (`bun run lint:design`), and every pair a person reads meets 4.5:1 in both themes. - **The open web.** A shared link unfurls (description, Open Graph, a card); `/robots.txt` keeps crawlers to the page, and out of previews entirely; `/.well-known/security.txt` sends a reporter to the forge that runs the app.