A single-user, self-hosted note-taking web app
Heavily inspired by The Archive and designed to be used in concert with it. Notes are plain Markdown files on the filesystem with no database. Designed to be accessed from any browser after authenticating with a single password. Uses syncthing in the background to keep notes synced with a local copy on your desktop.
This is the software that I want to use; it is probably not the software that you want to use.
- Plain text notes —
.mdfiles withYYYYMMDDHHMM Title.mdnaming convention, fully compatible with The Archive - Full-text search — in-memory Flexsearch index with structured
#tag/#referencefilters and search-bar autocomplete - Wiki-links, tags, and references —
[[note links]],#tags, and Markdown reference citations with autocomplete - Live sync — Syncthing keeps notes in sync between VPS and Mac; SSE pushes external changes to the browser in real time
- Conflict detection — etag-based optimistic concurrency prevents overwrites from concurrent edits
- CodeMirror 6 editor — Markdown syntax highlighting, auto-save, faded formatting marks, inline link decorations
- Preview mode — rendered Markdown with Edit / Preview / Split toggle
- Backlinks and tags panel — see what links to the current note and browse all tags
- Keyboard-driven — quick open, back/forward navigation, search focus, and more
| Layer | Technology |
|---|---|
| Backend | Node.js 20, Fastify, TypeScript (tsx) |
| Frontend | React 18, Vite, CodeMirror 6, Tailwind CSS, Zustand |
| Auth | bcrypt + HTTP-only session cookie |
| Search | Flexsearch (server-side, in-memory) |
| Sync | Syncthing (VPS ↔ Mac) |
| Production | DigitalOcean VPS, Caddy (HTTPS), PM2 |
- Node.js 20+
- npm
npm install
# Set the login password (first time only)
npm run setupStart the backend and frontend in separate terminals:
# Terminal 1 — backend (auto-restarts on change)
NOTES_DIR=~/Documents/TestNotes SESSION_SECRET=devsecretdevsecretdevsecretdevsecret PORT=3001 npm run dev:server
# Terminal 2 — frontend (HMR)
npm run devOpen http://localhost:5173. Vite proxies /api/* to the backend on port 3001.
Point NOTES_DIR at any folder containing .md files — a dedicated test folder is recommended.
npm run build
NODE_ENV=production NOTES_DIR=/path/to/notes SESSION_SECRET=$(openssl rand -hex 32) npm start
# Open http://localhost:3000# Backend API tests (vitest)
npm test
# End-to-end tests (Playwright)
npm run test:e2eThe search box supports plain words, exact phrases, negation, and structured hashtag filters:
leadership "effective executive" NOT meeting #drucker1967
- Plain words are ANDed across filename, title, and body.
- Quoted phrases match exact text case-insensitively.
NOT wordexcludes notes containing that word.#keymatches a normal tag, a reference definition like[#drucker1967]: ..., or a citation like[54][#drucker1967].- While typing a
#...token in the search box, autocomplete suggests both tags and reference keys; choosing one replaces only the current token, soleadership #drucan becomeleadership #drucker1967.
Deployment is automated with Terraform (infrastructure) and Ansible (provisioning + deploys), orchestrated via a Makefile. See SPEC.md Section 18 for full details.
# 1. Create the droplet + cloud firewall
make infra-init
make infra-apply
# 2. Provision the VPS (first time only)
make provision
# 3. Deploy the app
make deployRequired environment variables: DIGITALOCEAN_TOKEN, TF_VAR_ssh_key_name, SESSION_SECRET. Copy deploy.sh.example to deploy.sh and fill in values.
server/ # Fastify backend (auth, routes, file store, search index, SSE)
src/ # React frontend (components, store, hooks, editor)
ansible/ # Provision + deploy playbooks, Caddyfile template
terraform/ # DigitalOcean droplet + firewall IaC
test/ # Backend API tests (vitest)
e2e/ # End-to-end tests (Playwright)
After deploying, open Settings (gear icon or Cmd+,) and scroll to the Sync (Syncthing) section.
- On your Mac, install Syncthing:
brew install syncthing brew services start syncthing
- Open http://localhost:8384 to access the Syncthing GUI on your Mac.
- In Annex Settings, copy the server's Device ID (click "Copy").
- In the Mac Syncthing GUI, go to Add Remote Device and paste the server's Device ID.
- In the Mac Syncthing GUI, find your Mac's Device ID under Actions > Show ID.
- Back in Annex Settings, paste your Mac's Device ID and click Pair device. This adds the Mac as a trusted device and shares the notes folder automatically.
- In the Mac Syncthing GUI, accept the incoming folder share when prompted. Set the local path to your notes folder (e.g.,
~/Documents/Zettelkasten).
- The connection status dot in Settings shows green when connected, yellow when paired but not currently connected.
- Syncthing runs in the background — changes sync within 1-2 seconds.
- iCloud syncs from your Mac to iPhone/iPad as before.
The Sync section will show "Syncthing is not configured on this server." Run make provision (or FIRST_RUN=1 make provision for a new droplet) to install Syncthing on the VPS first.
