A self-hosted personal knowledge management app — a replacement for Roam Research that runs on your own Mac and is reached from your other devices over Tailscale.
An outliner-style notes app in the Roam mould:
- Daily notes as the home view, with an infinite scroll of days
- Nested blocks with outliner editing, block references preserved from Roam
[[page links]],#tags,Attr::attributes and namespace pages ([[AWS/SCP]]), with backlinks and unlinked references per page- Fast full-text search (SQLite FTS5)
{{[[query]]}}blocks (and/or/notover page refs)- Images and PDFs stored and served locally, content-addressed
- Live sync between open clients over a WebSocket (desktop + iPad)
- An in-app LLM assistant that can read and (with your confirmation) write your notes (see Assistant)
- Offline editing: an installable PWA with a local replica — read, edit and search your whole graph with no connection; changes sync back on reconnect (see Offline)
- One-shot importer from a Roam EDN export, preserving uids, ordering and timestamps
- Nightly backups: rotated SQLite snapshots plus a git-committed markdown export
After one online visit, each browser keeps a full local replica of the graph (SQLite compiled to WebAssembly, persisted by the browser) and a service worker caches the app itself — so a cold start with no network still boots straight into your notes.
What works offline:
- Reading everything: daily notes, pages, backlinks, unlinked references, block references
- Editing blocks — changes queue durably on the device and the header shows "Offline — N changes pending" until they reach the server
- Creating pages (from search) and daily notes
- Full-text search and
[[link]]autocomplete, served from the local replica - Images you've viewed before (a bounded cache of recently seen assets); ones you haven't show a labelled placeholder
Online-only (the UI says so rather than failing): uploading images/files,
editing the sidebar, deleting pages, and {{[[query]]}} blocks.
When edits collide (same block changed on two devices while one was
offline), the server keeps per-block last-write-wins and preserves the losing
text as a [[conflict]] block next to the winner — nothing is silently
discarded. An offline edit to a block that was meanwhile deleted is appended
to today's daily note instead of vanishing.
Limits to know about: the first visit (and login) needs a connection; the replica is per-browser, so a new device or a cleared browser profile starts online; and if the device runs out of local storage while offline, editing pauses, with a visible reason, rather than risking the loss of a change.
Cmd/Ctrl+J opens a chat panel backed by a Claude agent. Pick a model
(sonnet by default, or opus/haiku) and ask it to find, summarise or
write notes.
The agent runs on the server, not in the browser, and it has no general
tools — only the twelve pkm verbs, which reach your graph through the same
HTTP API every other client uses. Reads happen without asking. Every write
pauses for an Allow/Deny card in the chat that shows exactly which operations
it wants to apply. Conversations are held in memory only, so a reload starts
a new one.
The assistant needs a logged-in Claude subscription on the machine running the server; if it is missing, the assistant reports an error in the chat and the rest of the app is unaffected. Setup is in deploy/README.md.
Notes are a decades-long asset; the app that holds them shouldn't be a subscription service that can disappear, slow down, or hold the data hostage. This project trades Roam's collaborative/multi-user machinery (which a single-user graph never uses) for:
- Ownership — everything lives in one SQLite file plus an assets directory on a machine you control; the nightly export doubles as a plain-markdown escape hatch.
- Simplicity — server-authoritative block ops, no CRDTs, no sync protocol to debug. Per-block last-write-wins is plenty for one person.
- Longevity — boring, inspectable parts: FastAPI, SQLite, React. Block text is stored as unmodified Roam-flavoured markdown, so nothing is locked into this app either.
The design document gives the high-level architecture and the key decisions, linking through to the detailed specs and implementation plans.
server/ Python backend: FastAPI app, SQLite storage, Roam EDN importer,
markdown export, nightly backup job
web/ TypeScript frontend: React + Vite SPA, Vitest unit tests,
Playwright e2e tests
shared/ Fixtures shared between the Python and TS ref-grammar parsers,
pinning both to identical behaviour
deploy/ launchd + Tailscale Serve deployment for a Mac (see deploy/README.md)
docs/ Design docs and implementation plans
The codebase follows the functional-core / imperative-shell pattern: pure
logic and I/O live in separate files, each declaring its role in a # pattern:
header comment (see CLAUDE.md).
From the repository root:
uv sync --project server
uv run --project server pytest
uv run --project server python -m pkm.test_data.generate --out data
cd server
uv run python -m pkm.server.setup --data-dir ../data --insecure-cookie
uv run python -m pkm.server.run --data-dir ../datapkm.server.setup creates data/config.json and remains responsible for the
password and cookie settings.
If you want to replace the synthetic fixture with a Roam export, export your graph as EDN (not markdown — that loses uids and structure) and download the linked files, then:
cd server
uv run python -m pkm.importer.run /path/to/export.edn \
--files /path/to/linked-files --out ../dataEach run builds a fresh database and atomically swaps it in, so re-running is always safe. It ends with a report of everything imported, and of anything unrecognised: nothing is dropped without being reported.
Titles are cleaned up on the way in. Balanced [[/]] markers and #
markers are removed from page and ref-derived titles, collisions are merged,
and every changed spelling and merge appears in the report. Malformed marker
syntax, or a title left blank by that cleanup, aborts the import before any
output is created. Fresh databases also arrive with title canonicalization
already active — see docs/cli.md
for what that means.
# Stop the server first.
rm -f data/pkm.sqlite3 data/pkm.sqlite3-wal data/pkm.sqlite3-shm
rm -rf data/assets
uv run --project server python -m pkm.test_data.generate --out dataThis preserves your data/config.json and authentication.
cd web
pnpm install
pnpm dev # Vite dev server on http://localhost:5173The dev server proxies /api, /assets and /login to the backend on
127.0.0.1:8974 (see web/vite.config.ts), so run the server alongside it.
Other web scripts:
pnpm test # Vitest unit tests
pnpm test:coverage # unit tests with enforced coverage thresholds
pnpm typecheck # tsc
pnpm e2e # build, then Playwright end-to-end tests
pnpm verify # typecheck + coverage + Playwright (standard verification)
pnpm build # production build to web/dist
pnpm gen-types # regenerate TS API types from the server's OpenAPI schemaTo serve the built SPA from the backend itself (no Vite), build it and set
web_dist in config.json (the setup script's --web-dist flag does this).
The pkm CLI and an MCP server let scripts and LLM agents read and write the
graph from outside the browser. Both talk to the running server's HTTP API and
share one login:
cd server && uv run pkm login --url http://127.0.0.1:8974
For Claude Code, add the MCP server from the repository root:
claude mcp add pkm -- uv run --project server pkm-mcp
The full command reference, the pkm batch command language, MCP setup for
other clients, and the one-time title-canonicalization procedure are in
docs/cli.md. Every verb's --help is self-sufficient.
Production runs as launchd services on a Mac, fronted by Tailscale Serve for
HTTPS across the tailnet, with a nightly backup job (rotated SQLite snapshots
plus a git-committed markdown/assets export). deploy/install.sh sets all of
this up; deploy/README.md has the full install, update,
backup and restore procedures.
- Architecture docs — codebase orientation for new contributors (human or agent): system overview, backend + API, frontend, and the sync/offline protocol
- Design document — high-level architecture and key
decisions, linking to the detailed specs and plans in
docs/superpowers/ - CLI and MCP reference — every
pkmverb, the batch command language, MCP setup, title canonicalization - Deployment guide — install, update, backups, restore, troubleshooting
docs/superpowers/plans/— the implementation plans each phase was built from