{"id":"9cd5c41b-a353-48bd-98bf-7b2d8d45fca2","slug":"fulviodenza-selfnote","kind":"skill","name":"selfnote","summary":"Self-hosted, Notion-like workspace: real-time collaborative editing, offline-first, with web/desktop/mobile clients and optional self-hosted AI Assist.","owner":"fulviodenza","repoUrl":"https://github.com/fulviodenza/selfnote","license":"AGPL-3.0","licenseTier":"strong-copyleft","trust":"community","stars":4,"match":null,"fewStars":true,"format":"skill","createdAt":"2026-09-04T13:36:32.000Z","addedAt":"2026-10-14T11:22:59.220Z","logoUrl":"https://avatars.githubusercontent.com/u/9990423?u=521d79c6a526dfc67986a7f9163bd021c65e07e4&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["files-documents","productivity"],"tags":[],"runtime":"node","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-14T21:05:21.912Z","sample":false,"url":"https://conduits.dev/tools/fulviodenza-selfnote","language":"TypeScript","transports":["stdio"],"install":{"npm":"@selfnote/mcp"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"<p align=\"center\">\n  <img src=\"docs/brand/logo.png\" alt=\"Selfnote logo\" width=\"160\" />\n</p>\n\n<h1 align=\"center\">Selfnote</h1>\n\nA **self-hosted, Notion-like workspace** — real-time multiplayer editing, offline-first,\nwith **web, desktop, and mobile** clients that all talk to your own server. Optional\n**AI Assist** runs against a model you control (local Claude CLI, the Anthropic API, or\nOllama). Your notes, your instance, your keys.\n\nBuilt on a CRDT core (Yjs / [yrs](https://github.com/y-crdt/y-crdt)) so edits merge\ncleanly across devices and offline sessions, and a block editor\n([BlockNote](https://www.blocknotejs.org/)) shared by every client.\n\n## Features\n\n- **Real-time collaboration** — multiple cursors, live presence, conflict-free merges.\n- **Offline-first** — keep editing with no connection; changes reconcile on reconnect.\n- **One design, three clients** — a shared \"Ink & Paper\" design system with light/dark\n  mode across web, desktop (Tauri), and mobile (Expo/React Native).\n- **AI Assist** — a Claude-style chat in the editor sidebar that streams replies, is\n  grounded in the current note, and drops any answer straight into the doc. Appears only\n  when *your* server has an AI backend configured.\n- **Save from any Claude (MCP)** — an MCP server + skill so Claude in the CLI, Desktop, or\n  claude.ai can file a summary of an unrelated conversation into your notes and hand back\n  the link. See [`docs/mcp.md`](docs/mcp.md).\n- **Page tree** — nested pages, rename, archive, search.\n- **Sharing** — public read/write share links via short-lived room tokens.\n- **Self-hostable everywhere** — nothing is hardcoded: native apps ask which instance to\n  connect to on first launch; the browser client uses whatever origin serves it.\n- **Obsidian import** — bring a Markdown vault in.\n\n## Screenshots\n\n| First-launch onboarding | Documents | Dark mode |\n|---|---|---|\n| ![Onboarding](docs/screenshots/mobile-onboarding.png) | ![Documents](docs/screenshots/mobile-app.png) | ![Dark mode](docs/screenshots/mobile-dark.png) |\n\n> Shots are from the mobile client; the web and desktop clients share the same UI and\n> design system. (Add web/desktop captures under `docs/screenshots/` too.)\n\n## Architecture\n\nTwo small Rust services over Postgres, and a shared TypeScript client core:\n\n- **`server/api`** (axum + sqlx) — control plane: auth, workspaces, the page tree,\n  permissions, room-token issuance, optional AI endpoints.\n- **`server/sync`** (yrs) — a WebSocket hub speaking the Yjs sync protocol\n  (wire-compatible with stock `y-websocket`); persists updates to Postgres and compacts\n  snapshots so documents survive restarts.\n- **`packages/core`** — framework-free Yjs + sync + pluggable persistence, shared by all\n  clients (IndexedDB on web, SQLite on mobile).\n- **`packages/editor`** — the shared BlockNote collaborative editor.\n\n```\napps/web/          React + Vite web client (also the desktop frontend)\napps/desktop/      Tauri 2 native shell (bundles apps/web)\napps/mobile/       Expo / React Native app (WebView editor + SQLite)\npackages/core/     @selfnote/core — Yjs + sync + persistence (framework-free)\npackages/editor/   @selfnote/editor — BlockNote collaborative editor\nserver/api/        Rust control-plane API (auth, workspaces, docs, room tokens, AI)\nserver/sync/       Rust WebSocket sync server (yrs) + Postgres persistence\nserver/migrations/ SQL migrations\ndeploy/helm/       Helm chart (services + Ingress + CloudNativePG + MinIO)\noperator/          Go operator: Selfnote CRD + reconciler\ntools/smoketest/   automated end-to-end sync/merge/persistence tests\n```\n\n## Prerequisites\n\n- **Rust** (stable) + Cargo\n- **Node 22+** and **pnpm 11+** (`corepack enable`)\n- **Docker** (for the dev Postgres + MinIO)\n- Mobile: the **Expo** toolchain + Android Studio / Xcode. Desktop: the **Tauri**\n  prerequisites (Rust + platform webview; `cargo install tauri-cli`).\n\n## Quick start (development)\n\n```bash\n# 1. Install JS deps\npnpm install\n\n# 2. Start Postgres + MinIO\ndocker compose -f docker-compose.dev.yml up -d\n\n# 3. API server (terminal 1) — runs migrations on boot, listens on :4445\ncargo run -p selfnote-api\n\n# 4. Sync server (terminal 2) — listens on ws://0.0.0.0:4444/ws/:doc\nDATABASE_URL=postgres://selfnote:selfnote@localhost:5432/selfnote \\\n  ROOM_SECRET=dev-room-secret SYNC_REQUIRE_AUTH=1 \\\n  cargo run -p selfnote-sync\n\n# 5. Web client (terminal 3) — http://localhost:5173\npnpm --filter @selfnote/web dev\n```\n\nOpen the web app, register an account, and start editing. Open a second browser to see\nlive collaboration; use **Simulate offline**, type, then reconnect to watch edits merge.\n\n## Building & running the clients\n\n### Web\n\n```bash\npnpm --filter @selfnote/web build     # → apps/web/dist (static SPA)\n```\n\nThe browser client resolves its server from the **page origin** (`/api` for HTTP,\n`wss://<host>/ws` for sync) — so the same build works on any domain with nothing baked in,\nas long as your reverse proxy routes `/api` and `/ws` to the two services (see\n`apps/web/nginx.conf` for the reference proxy).\n\n### Desktop (Tauri)\n\n```bash\ncargo install tauri-cli --version \"^2\"      # once\ncd apps/desktop && cargo tauri build        # native app + installer under src-tauri/target/release/bundle\n# dev with hot reload:\ncd apps/desktop && cargo tauri dev\n```\n\nThe desktop app bundles the web frontend. On first launch it asks which Selfnote\n**instance** to connect to (it has no serving origin of its own). Release bundles are\nunsigned unless you provide signing keys.\n\n### Mobile (Expo / React Native)\n\nA standalone Expo project (its own `node_modules`, not part of the pnpm workspace).\n\n```bash\ncd apps/mobile\nnpm install\nnpm run typecheck\nnpm run android         # or: npm run ios   (needs Android Studio / Xcode)\n```\n\nOn first launch the app asks for your **server address** (e.g. `notes.example.com`); it\nderives `/api` + `/ws`, verifies the server, and saves it — you can change it later in\nSettings. Nothing is hardcoded in the shipped app.\n\n## Self-hosting\n\nThe browser + native clients all talk to two services (`api` on 4445, `sync` on 4444)\nplus Postgres; the web is served by an nginx that proxies `/api` and `/ws` to them (one\norigin serves the whole app).\n\n- **Helm** (Kubernetes) — `deploy/helm/selfnote` deploys api/sync/web, a WebSocket-aware\n  Ingress, in-cluster MinIO, and a CloudNativePG Postgres cluster with WAL backups.\n  Set your image registry, ingress host, and secrets in `values.yaml`\n  (`jwtSecret` / `roomSecret` / MinIO creds — change the `change-me-*` placeholders).\n- **Operator** — `operator/` provides a `Selfnote` CRD (`kubectl get sn`) whose reconciler\n  owns the workloads and composes a Postgres cluster.\n- **Backups** — `deploy/restore-drill.sh` verifies backup → restore into a fresh cluster.\n\n## AI Assist (optional)\n\n`/ai/status` + `/ai/complete` on the API expose an AI backend; the clients show the\n**✦ Assist** panel only when a provider is available. Configure one via env on the API:\n\n| Provider | Env |\n|---|---|\n| Local **Claude CLI** (uses your subscription; data stays on the box) | `claude` on `PATH` (or `SELFNOTE_AI_CMD`) + `CLAUDE_CODE_OAUTH_TOKEN` |\n| **Anthropic API** | `ANTHROPIC_API_KEY` (+ optional `SELFNOTE_AI_MODEL`) |\n| **Ollama** | `OLLAMA_HOST` |\n\nOn Kubernetes the chart has an `ai:` block that wires this for you (pick a provider,\npoint at a secret) — no manual `kubectl set env`. See\n[`docs/ai-setup.md`](docs/ai-setup.md) for env and Helm details.\nThe chat (`/ai/chat/stream`) and `/ai/complete` enforce workspace membership for the\nreferenced document.\n\n## Connecting Claude elsewhere (MCP)\n\nBeyond the in-app assistant, an [MCP server](tools/mcp-server) lets Claude running in the\n**CLI, Desktop, or claude.ai** write into this instance — most usefully, summarizing the\ncurrent conversation into a note and returning its link. You mint a personal access token\nin the app's **Connections** settings, point the server at your instance, and (optionally)\ninstall the `save-to-selfnote` skill. Full setup, local and remote, is in\n[`docs/mcp.md`](docs/mcp.md).\n\n## Testing\n\n```bash\ncargo test                 # Rust unit tests (protocol codec, etc.)\ncargo clippy -- -D warnings\npnpm -r typecheck\n\n# end-to-end: start the sync server first, then:\npnpm smoketest             # 2 clients — live propagation + offline-merge convergence\n```\n\n## License\n\nSee [`LICENSE`](LICENSE).\n"}