benjivers/notes Public Public: anyone can read this; nobody can change it.
Files 40c75dcddedb7a6a4eea93a9f587c3b91283870c
- contract
- design
- scripts
- src
- static
- .gitignore 37 B · ~11 tok
- AGENTS.md 7.4 KB · ~1.9k tok
- contract.json 758 B · ~253 tok
- forge.json 137 B · ~46 tok
- package.json 377 B · ~126 tok
- README.md 6.9 KB · ~1.8k tok
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 sign in with your forge account. Your notes are yours alone: the contract gives every verb on a note to its author, so nobody else who can open the app — admins included — can list, read or change them.
- 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",#tagortag: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-DDnote. - 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.
| 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.tsrations
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.jsoniscontract/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 /healthzanswers{ ok, version, uptime }; the
forge waits on it before it sends anyone to a deploy.
- The look.
design/tokens.jsonholds 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.