benjivers/notes Public Public: anyone can read this; nobody can change it.
Files 9965609c46dc660e7b5b52f91658bb8a67c37294
- contract
- design
- scripts
- src
- static
- .gitignore 37 B · ~11 tok
- AGENTS.md 7.4 KB · ~1.9k tok
- contract.json 845 B · ~282 tok
- forge.json 137 B · ~46 tok
- package.json 377 B · ~126 tok
- README.md 9.5 KB · ~2.4k 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 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",#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.
Shared folders
A shared folder is a folder of a git repository on the forge — say
docs/ of ben/brainstorm — that you and the people you invite write in
together. Everything else in the app stays yours alone: a shared folder's
notes never appear in anyone's own vault, search or graph, and its members
never see yours.
- Connect one (app admins): sidebar → Vaults → Share a repo folder,
then name, owner/repo, branch and folder. It reads the folder at once.
Use a plain repository, not an app's: every save is a commit.
- Invite: the folder's people icon → Join link. Anyone signed in (forge
or email account) who opens it joins. Turn the link off afterwards, make a new one, or remove people there. Members can leave.
- Inside: the sidebar shows the repo's folder tree. Links, backlinks,
tags, search, the graph and daily notes work as in your vault, within the
folder. Name a new note ideas/Pricing to put it in ideas/; the File
panel moves a note to another folder. Front matter (--- … ---) reads
as properties, and its tags are tags.
- Git: every note is a
.mdfile. Edits commit a few seconds after you
stop typing (one commit for a burst, naming who wrote); renames and moves
move the file; removing a note removes it. Anything pushed to the folder
(from a laptop, VS Code, Obsidian) shows up within a minute, or at once
with Sync now. A note changed in both places keeps the app's words and
saves the repository's beside it as <title> (repo copy) — nothing is
lost.
- Two people, one note: the second save is refused when the note moved
on underneath it, and the editor asks: Load theirs or Keep mine.
Commits are made with NOTES_REPO_TOKEN (Settings → App → Environment), a
forge token that may write those repositories; without it there are no
shared folders. Commits are authored as that token's owner.
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.