benjivers/notes Public Public: anyone can read this; nobody can change it.

Files 73f5fd914653f166957d8eef6ca6083f84887b4a
History

README.md

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:

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

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.

filewhat it is
src/actions.tsnotes: list, add, rename (relinks), pin, delete, search; audit
src/vault.tslinks, backlinks, tags, graph, search — pure, over notes
static/md.jsthe markdown renderer, shared by the server and the browser
contract.jsonwho may do what: roles, and the least role each verb needs
src/contract.ts, src/can.tsthe table, read at boot; the forge's evaluator, verbatim
src/security.tsthe door: rationing, same origin, the table before the body
src/audit.tsevery change and every refused one, as a row
src/who.tswho is asking, from the forge's signed headers
src/db.tsSQLite under FORGE_APP_DATA, WAL, forward-only migrations
src/live.tsthe in-process bus and GET /events
src/mcp.tsthe actions as MCP tools at POST /mcp
src/routes.tsevery path, in one table, each API route described beside it
src/openapi.tsthe routes as OpenAPI; committed at contract/openapi.json
src/page.tsthe HTML, rendered on the server, with its share metadata
design/tokens.json, src/tokens.tsthe look as data, light and dark; compiled to /tokens.css
src/http.tsthe floor: headers, body cap, files
src/server.tsboot: open the database, serve
src/tty.tsthe terminal, over the same actions
src/env.tswhat the forge told this process
static/app.css, static/live.jsthe look in the tokens' words, and the panels that stay current
static/app.js, static/graph.jsthe 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.

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

Keyboard shortcuts