skill
selfnote
Self-hosted, Notion-like workspace: real-time collaborative editing, offline-first, with web/desktop/mobile clients and optional self-hosted AI Assist.
Set up
Skills are installed by placing their folder where your agent loads skills.
Install instructions come from the README
README
synced Oct 14, 2026A self-hosted, Notion-like workspace — real-time multiplayer editing, offline-first, with web, desktop, and mobile clients that all talk to your own server. Optional AI Assist runs against a model you control (local Claude CLI, the Anthropic API, or Ollama). Your notes, your instance, your keys.
Built on a CRDT core (Yjs / yrs) so edits merge cleanly across devices and offline sessions, and a block editor (BlockNote) shared by every client.
Features
- Real-time collaboration — multiple cursors, live presence, conflict-free merges.
- Offline-first — keep editing with no connection; changes reconcile on reconnect.
- One design, three clients — a shared "Ink & Paper" design system with light/dark mode across web, desktop (Tauri), and mobile (Expo/React Native).
- AI Assist — a Claude-style chat in the editor sidebar that streams replies, is grounded in the current note, and drops any answer straight into the doc. Appears only when your server has an AI backend configured.
- Save from any Claude (MCP) — an MCP server + skill so Claude in the CLI, Desktop, or
claude.ai can file a summary of an unrelated conversation into your notes and hand back
the link. See
docs/mcp.md. - Page tree — nested pages, rename, archive, search.
- Sharing — public read/write share links via short-lived room tokens.
- Self-hostable everywhere — nothing is hardcoded: native apps ask which instance to connect to on first launch; the browser client uses whatever origin serves it.
- Obsidian import — bring a Markdown vault in.
Screenshots
| First-launch onboarding | Documents | Dark mode |
|---|---|---|
Shots are from the mobile client; the web and desktop clients share the same UI and design system. (Add web/desktop captures under
docs/screenshots/too.)
Architecture
Two small Rust services over Postgres, and a shared TypeScript client core:
server/api(axum + sqlx) — control plane: auth, workspaces, the page tree, permissions, room-token issuance, optional AI endpoints.server/sync(yrs) — a WebSocket hub speaking the Yjs sync protocol (wire-compatible with stocky-websocket); persists updates to Postgres and compacts snapshots so documents survive restarts.packages/core— framework-free Yjs + sync + pluggable persistence, shared by all clients (IndexedDB on web, SQLite on mobile).packages/editor— the shared BlockNote collaborative editor.
apps/web/ React + Vite web client (also the desktop frontend)
apps/desktop/ Tauri 2 native shell (bundles apps/web)
apps/mobile/ Expo / React Native app (WebView editor + SQLite)
packages/core/ @selfnote/core — Yjs + sync + persistence (framework-free)
packages/editor/ @selfnote/editor — BlockNote collaborative editor
server/api/ Rust control-plane API (auth, workspaces, docs, room tokens, AI)
server/sync/ Rust WebSocket sync server (yrs) + Postgres persistence
server/migrations/ SQL migrations
deploy/helm/ Helm chart (services + Ingress + CloudNativePG + MinIO)
operator/ Go operator: Selfnote CRD + reconciler
tools/smoketest/ automated end-to-end sync/merge/persistence tests
Prerequisites
- Rust (stable) + Cargo
- Node 22+ and pnpm 11+ (
corepack enable) - Docker (for the dev Postgres + MinIO)
- Mobile: the Expo toolchain + Android Studio / Xcode. Desktop: the Tauri
prerequisites (Rust + platform webview;
cargo install tauri-cli).
Quick start (development)
# 1. Install JS deps
pnpm install
# 2. Start Postgres + MinIO
docker compose -f docker-compose.dev.yml up -d
# 3. API server (terminal 1) — runs migrations on boot, listens on :4445
cargo run -p selfnote-api
# 4. Sync server (terminal 2) — listens on ws://0.0.0.0:4444/ws/:doc
DATABASE_URL=postgres://selfnote:selfnote@localhost:5432/selfnote \
ROOM_SECRET=dev-room-secret SYNC_REQUIRE_AUTH=1 \
cargo run -p selfnote-sync
# 5. Web client (terminal 3) — http://localhost:5173
pnpm --filter @selfnote/web dev
Open the web app, register an account, and start editing. Open a second browser to see live collaboration; use Simulate offline, type, then reconnect to watch edits merge.
Building & running the clients
Web
pnpm --filter @selfnote/web build # → apps/web/dist (static SPA)
The browser client resolves its server from the page origin (/api for HTTP,
wss://<host>/ws for sync) — so the same build works on any domain with nothing baked in,
as long as your reverse proxy routes /api and /ws to the two services (see
apps/web/nginx.conf for the reference proxy).
Desktop (Tauri)
cargo install tauri-cli --version "^2" # once
cd apps/desktop && cargo tauri build # native app + installer under src-tauri/target/release/bundle
# dev with hot reload:
cd apps/desktop && cargo tauri dev
The desktop app bundles the web frontend. On first launch it asks which Selfnote instance to connect to (it has no serving origin of its own). Release bundles are unsigned unless you provide signing keys.
Mobile (Expo / React Native)
A standalone Expo project (its own node_modules, not part of the pnpm workspace).
cd apps/mobile
npm install
npm run typecheck
npm run android # or: npm run ios (needs Android Studio / Xcode)
On first launch the app asks for your server address (e.g. notes.example.com); it
derives /api + /ws, verifies the server, and saves it — you can change it later in
Settings. Nothing is hardcoded in the shipped app.
Self-hosting
The browser + native clients all talk to two services (api on 4445, sync on 4444)
plus Postgres; the web is served by an nginx that proxies /api and /ws to them (one
origin serves the whole app).
- Helm (Kubernetes) —
deploy/helm/selfnotedeploys api/sync/web, a WebSocket-aware Ingress, in-cluster MinIO, and a CloudNativePG Postgres cluster with WAL backups. Set your image registry, ingress host, and secrets invalues.yaml(jwtSecret/roomSecret/ MinIO creds — change thechange-me-*placeholders). - Operator —
operator/provides aSelfnoteCRD (kubectl get sn) whose reconciler owns the workloads and composes a Postgres cluster. - Backups —
deploy/restore-drill.shverifies backup → restore into a fresh cluster.
AI Assist (optional)
/ai/status + /ai/complete on the API expose an AI backend; the clients show the
✦ Assist panel only when a provider is available. Configure one via env on the API:
| Provider | Env |
|---|---|
| Local Claude CLI (uses your subscription; data stays on the box) | claude on PATH (or SELFNOTE_AI_CMD) + CLAUDE_CODE_OAUTH_TOKEN |
| Anthropic API | ANTHROPIC_API_KEY (+ optional SELFNOTE_AI_MODEL) |
| Ollama | OLLAMA_HOST |
On Kubernetes the chart has an ai: block that wires this for you (pick a provider,
point at a secret) — no manual kubectl set env. See
docs/ai-setup.md for env and Helm details.
The chat (/ai/chat/stream) and /ai/complete enforce workspace membership for the
referenced document.
Connecting Claude elsewhere (MCP)
Beyond the in-app assistant, an MCP server lets Claude running in the
CLI, Desktop, or claude.ai write into this instance — most usefully, summarizing the
current conversation into a note and returning its link. You mint a personal access token
in the app's Connections settings, point the server at your instance, and (optionally)
install the save-to-selfnote skill. Full setup, local and remote, is in
docs/mcp.md.
Testing
cargo test # Rust unit tests (protocol codec, etc.)
cargo clippy -- -D warnings
pnpm -r typecheck
# end-to-end: start the sync server first, then:
pnpm smoketest # 2 clients — live propagation + offline-merge convergence
License
See LICENSE.