| 1 |
# notes |
| 2 |
|
| 3 |
A personal linked-notes vault, in the spirit of Obsidian — markdown, |
| 4 |
`[[wikilinks]]`, backlinks, tags and a graph — kept on the forge. |
| 5 |
|
| 6 |
## Using it |
| 7 |
|
| 8 |
Open the app and **create a vault** with an email and password, or sign in |
| 9 |
with a forge account. Your notes are **yours alone**: the contract gives |
| 10 |
every verb on a note to its author, so nobody else who uses the app — |
| 11 |
admins included — can list, read or change them. |
| 12 |
|
| 13 |
**Email accounts** (`src/accounts.ts`) are the app's own: argon2id password |
| 14 |
hashes, a random session token in an HttpOnly cookie (only its SHA-256 is |
| 15 |
stored), 30-day sessions, log-in attempts rationed per email. The account |
| 16 |
page changes the password (ending every other session) or deletes the |
| 17 |
account with all its notes. There is no email-based reset — the app sends |
| 18 |
no mail. Set `NOTES_SIGNUP_CODE` in Settings → App → Environment to require |
| 19 |
a code for new accounts (closing open sign-up without a deploy). A vault |
| 20 |
holds at most 5,000 notes. Email accounts reach the page and the JSON API; |
| 21 |
the MCP endpoint and the terminal are for forge accounts. |
| 22 |
|
| 23 |
- **Write** in markdown. It saves as you type (and with ⌘S). Edit, Split |
| 24 |
(side by side) and Read views; ⌘E flips between reading and writing. |
| 25 |
- **Link** with `[[Another note]]`, `[[Note|shown text]]` or |
| 26 |
`[[Note#Heading]]`. Typing `[[` suggests titles. A link to a note not |
| 27 |
written yet is drawn dashed; following it creates the note. |
| 28 |
- **Backlinks**: the right panel shows every note that links here, with the |
| 29 |
line it said it in, plus links out, the outline, tags and a local graph. |
| 30 |
- **Rename** a note and every link to it in your vault is rewritten. |
| 31 |
- **Tags**: `#tag`, nested as `#area/topic`. Click one to see every note |
| 32 |
carrying it (`#area` also finds `#area/topic`). |
| 33 |
- **Search**: words, `"a phrase"`, `#tag` or `tag:name`, combined. |
| 34 |
- **Graph**: every note and link, force-laid-out. Drag, zoom, click to open. |
| 35 |
- **Daily note**: the calendar button (⌘⇧D) opens today's `YYYY-MM-DD` note. |
| 36 |
- **Quick switcher**: ⌘K / Ctrl K — jump to a note, or ⇧↵ to create one. |
| 37 |
- Editor keys: ⌘B bold, ⌘I italic, ⌘⇧H highlight, ⌘↵ cycles a line |
| 38 |
through task / done / plain, Enter continues a list, Tab indents it. |
| 39 |
- Markdown: headings, emphasis, `==highlight==`, `~~strike~~`, code, |
| 40 |
fenced blocks, lists, `- [ ]` tasks (tick them in the reading view), |
| 41 |
quotes, Obsidian callouts (`> [!tip] Title`), tables, links. |
| 42 |
- **Your data**: every note downloads as `.md`; the sidebar's download |
| 43 |
button exports the whole vault as JSON. |
| 44 |
|
| 45 |
Everything is real: notes live in SQLite under `FORGE_APP_DATA`. A preview |
| 46 |
of a pull gets a copy of production data; production is the default branch. |
| 47 |
|
| 48 |
## Shared folders |
| 49 |
|
| 50 |
A **shared folder** is a folder of a git repository on the forge — say |
| 51 |
`docs/` of `ben/brainstorm` — that you and the people you invite write in |
| 52 |
together. Everything else in the app stays yours alone: a shared folder's |
| 53 |
notes never appear in anyone's own vault, search or graph, and its members |
| 54 |
never see yours. |
| 55 |
|
| 56 |
- **Connect one** (app admins): sidebar → *Vaults* → *Share a repo folder*, |
| 57 |
then name, `owner/repo`, branch and folder. It reads the folder at once. |
| 58 |
Use a plain repository, not an app's: every save is a commit. |
| 59 |
- **Invite**: the folder's people icon → *Join link*. Anyone signed in (forge |
| 60 |
or email account) who opens it joins. Turn the link off afterwards, make a |
| 61 |
new one, or remove people there. Members can leave. |
| 62 |
- **Inside**: the sidebar shows the repo's folder tree. Links, backlinks, |
| 63 |
tags, search, the graph and daily notes work as in your vault, within the |
| 64 |
folder. Name a new note `ideas/Pricing` to put it in `ideas/`; the File |
| 65 |
panel moves a note to another folder. Front matter (`---` … `---`) reads |
| 66 |
as properties, and its `tags` are tags. |
| 67 |
- **Git**: every note is a `.md` file. Edits commit a few seconds after you |
| 68 |
stop typing (one commit for a burst, naming who wrote); renames and moves |
| 69 |
move the file; removing a note removes it. Anything pushed to the folder |
| 70 |
(from a laptop, VS Code, Obsidian) shows up within a minute, or at once |
| 71 |
with *Sync now*. A note changed in both places keeps the app's words and |
| 72 |
saves the repository's beside it as `<title> (repo copy)` — nothing is |
| 73 |
lost. |
| 74 |
- **Two people, one note**: the second save is refused when the note moved |
| 75 |
on underneath it, and the editor asks: *Load theirs* or *Keep mine*. |
| 76 |
|
| 77 |
Commits are made with `NOTES_REPO_TOKEN` (Settings → App → Environment), a |
| 78 |
forge token that may write those repositories; without it there are no |
| 79 |
shared folders. Commits are authored as that token's owner. |
| 80 |
|
| 81 |
## Claude can use it too |
| 82 |
|
| 83 |
`POST /mcp` is an MCP server with `list_notes`, `search_notes`, |
| 84 |
`read_note`, `create_note`, `update_note`, `append_to_note` (with no |
| 85 |
title: today's daily note) and `delete_note` — as whoever the forge grant |
| 86 |
belongs to: |
| 87 |
|
| 88 |
```sh |
| 89 |
claude mcp add --transport http notes https://notes--forge-apps.benjivers.com/mcp |
| 90 |
``` |
| 91 |
|
| 92 |
The terminal (`FORGE_APP_SSH`) lists, reads and searches the vault, adds a |
| 93 |
line to today's note (`a`) and makes new notes (`n`). |
| 94 |
|
| 95 |
## Running it here |
| 96 |
|
| 97 |
```sh |
| 98 |
PORT=8080 FORGE_APP_DATA=./data bun run src/server.ts |
| 99 |
bun test |
| 100 |
FORGE_APP_DATA=./data bun run src/tty.ts |
| 101 |
bun run lint:design |
| 102 |
bun run contract:emit |
| 103 |
``` |
| 104 |
|
| 105 |
## The shape |
| 106 |
|
| 107 |
One implementation, many doors. `src/actions.ts` is every operation as a |
| 108 |
plain function — `(db, who, input)` in, data or a named failure out — and |
| 109 |
the page, the JSON API, the MCP server and the terminal all call it and |
| 110 |
decide nothing themselves. `AGENTS.md` says how to add to it. |
| 111 |
|
| 112 |
| file | what it is | |
| 113 |
| ---------------------------------- | -------------------------------------------------------------- | |
| 114 |
| `src/actions.ts` | notes: list, add, rename (relinks), pin, delete, search; audit | |
| 115 |
| `src/vault.ts` | links, backlinks, tags, graph, search — pure, over notes | |
| 116 |
| `static/md.js` | the markdown renderer, shared by the server and the browser | |
| 117 |
| `contract.json` | who may do what: roles, and the least role each verb needs | |
| 118 |
| `src/contract.ts`, `src/can.ts` | the table, read at boot; the forge's evaluator, verbatim | |
| 119 |
| `src/security.ts` | the door: rationing, same origin, the table before the body | |
| 120 |
| `src/audit.ts` | every change and every refused one, as a row | |
| 121 |
| `src/who.ts` | who is asking, from the forge's signed headers | |
| 122 |
| `src/db.ts` | SQLite under `FORGE_APP_DATA`, WAL, forward-only migrations | |
| 123 |
| `src/live.ts` | the in-process bus and `GET /events` | |
| 124 |
| `src/mcp.ts` | the actions as MCP tools at `POST /mcp` | |
| 125 |
| `src/routes.ts` | every path, in one table, each API route described beside it | |
| 126 |
| `src/openapi.ts` | the routes as OpenAPI; committed at `contract/openapi.json` | |
| 127 |
| `src/page.ts` | the HTML, rendered on the server, with its share metadata | |
| 128 |
| `design/tokens.json`, `src/tokens.ts` | the look as data, light and dark; compiled to `/tokens.css` | |
| 129 |
| `src/http.ts` | the floor: headers, body cap, files | |
| 130 |
| `src/server.ts` | boot: open the database, serve | |
| 131 |
| `src/tty.ts` | the terminal, over the same actions | |
| 132 |
| `src/env.ts` | what the forge told this process | |
| 133 |
| `static/app.css`, `static/live.js` | the look in the tokens' words, and the panels that stay current | |
| 134 |
| `static/app.js`, `static/graph.js` | the editor, switcher and keys; the force-directed graph | |
| 135 |
|
| 136 |
## What every app stands on |
| 137 |
|
| 138 |
- **The door.** Before a route sees a request, `src/security.ts` rations |
| 139 |
writes (and a stranger's API calls), refuses a write from another site, |
| 140 |
and turns away — before the body is read — anyone the table lets through |
| 141 |
a resource's door for no row at all: 401 for a stranger, 403 for anyone |
| 142 |
else. The action still decides about the real row. |
| 143 |
- **The audit trail.** Every change, and every change refused, is a row: |
| 144 |
who, which resource and verb, which row, when, and whether they were |
| 145 |
let. Admins read it at `GET /api/audit`. A row that cannot be written |
| 146 |
is a log line, never a failed request. |
| 147 |
- **The API, described.** `GET /openapi.json` is `contract/openapi.json`, |
| 148 |
written from the routes by `bun run contract:emit`; each operation names |
| 149 |
its cell of the table (`x-contract`). `bun test` fails while it is stale. |
| 150 |
- **Up, or not.** `GET /healthz` answers `{ ok, version, uptime }`; the |
| 151 |
forge waits on it before it sends anyone to a deploy. |
| 152 |
- **The look.** `design/tokens.json` holds every colour, space and size, |
| 153 |
light and dark; nothing else spells out a colour (`bun run lint:design`), |
| 154 |
and every pair a person reads meets 4.5:1 in both themes. |
| 155 |
- **The open web.** A shared link unfurls (description, Open Graph, a |
| 156 |
card); `/robots.txt` keeps crawlers to the page, and out of previews |
| 157 |
entirely; `/.well-known/security.txt` sends a reporter to the forge |
| 158 |
that runs the app. |
| 159 |
|