Self-hosted sync backend for Focus Compass, a quiet workspace for busy minds.
Focus Compass is built for entrepreneurs, freelancers, indie hackers, and ADHD-friendly multi-project minds who need visual clarity without the usual project-management overhead. It puts scattered projects on one clear map, keeps attention on the current focus and next step, and makes it easy to turn project context into AI-ready prompts.
This repository contains the optional server that powers secure realtime sync for Focus Compass. It is a small Node.js service built on Hocuspocus/Yjs with SQLite persistence, bearer-token auth, image storage, backups, an admin UI, and optional read-only MCP access.
- Realtime sync over WebSockets with Hocuspocus/Yjs
- SQLite persistence under
./databy default - Bearer-token auth for both REST and WebSocket clients
- Admin UI at
/for setup, storage, backups, and MCP token management - Image upload and serving with server-side MIME validation
- Automatic SQLite backups with restore support
- Optional read-only MCP endpoint for AI clients
- Install dependencies:
npm ci - Start the server:
npm run dev - Open http://localhost:8080.
The server starts in setup mode and the first visit to / can generate a token that is stored next to the database by default.
This public first-visit initialization is intentional: the dashboard is the
consumer setup boundary, and creating the master token is its primary first-run
action. It does not require a separate bootstrap secret or pairing step.
Operators who prefer pre-provisioned credentials can set ACCESS_TOKEN instead.
Default container image reference used by this repo:
ghcr.io/focus-compass/focus-compass-sync-server:v0.0.10
Start with the included compose file:
docker compose up -d
docker compose logs -fThe default docker-compose.yml points at the GHCR image reference above and persists runtime data in a Docker volume mounted to /app/data.
- A plain
docker compose upin this repository auto-loadsdocker-compose.override.yml, which publishes the server on the host as${HOCUSPOCUS_PORT:-8080}for directhttp://<host>:<port>access. - Behind a reverse proxy (Dokploy/Traefik, Caddy, nginx, …) the base
docker-compose.ymlonlyexposes the port on the compose network so the proxy can route to it — the host port is not bound. Deploy platforms that rundocker compose -f docker-compose.yml ...explicitly get this automatically (an explicit-fskips the override). With a baredocker compose upbehind your own proxy, deletedocker-compose.override.yml.
The public VPS installer does not clone this repository or use its override
file. It generates a pinned Compose project of its own and always binds the
backend to 127.0.0.1:18080-18099; on a clean standalone VPS it additionally
creates its own Caddy service for ports 80/443.
Use .env.example as the source of truth for environment variable names.
| Variable | Default | Purpose |
|---|---|---|
PORT |
8080 |
HTTP and WebSocket port |
ACCESS_TOKEN |
empty | Master token for REST and WebSocket auth |
DB_PATH |
./data/db.sqlite |
SQLite database path |
IMAGES_DIR |
./data/images |
Image storage directory |
BACKUP_DIR |
./data/backups |
Backup storage directory |
MCP_TOKENS_DB_PATH |
./data/mcp-tokens.sqlite |
MCP token hashes (managed from the admin UI) |
PROJECT_HISTORY_DB_PATH |
./data/project-history.sqlite |
Previous versions of projects edited in the admin UI |
PROJECT_HISTORY_LIMIT |
20 |
Previous versions kept per project |
PROJECT_MAX_BODY_BYTES |
16777216 |
Largest project editor request body (16 MiB, like MAX_WEBSOCKET_MESSAGE_BYTES) |
MAX_WEBSOCKET_MESSAGE_BYTES |
16777216 |
Maximum complete WebSocket/Yjs message after frame reassembly (16 MiB) |
MAX_UNAUTHENTICATED_QUEUE_BYTES |
262144 |
Maximum data buffered by one connection before authentication (256 KiB) |
MAX_UNAUTHENTICATED_QUEUE_MESSAGES |
32 |
Maximum messages buffered by one connection before authentication |
MAX_PENDING_DOCUMENTS |
8 |
Maximum document handshakes pending on one unauthenticated connection |
HOCUSPOCUS_TIMEOUT_MS |
30000 |
Absolute pre-authentication deadline and authenticated idle timeout |
Backup interval and retention are managed from the admin UI and stored in backup-settings.json next to the database.
See docs/hocuspocus-4-migration.md for the Hocuspocus 4 compatibility record, security limits, release procedure, and immutable-image rollback plan.
GET /health- health checkGET /- admin UIPOST /api/auth/setup- initialize a token when running in setup modeGET /api/workspace/:document- read a workspace snapshotGET /api/images- list imagesPOST /api/upload- upload an imageGET /api/mcp/tokens,POST /api/mcp/tokens,DELETE /api/mcp/tokens/:id- manage MCP tokens (master token)/api/admin/documents/:document/projects[/:id]- list, read and edit single projects (master token), see Project editorPOST /mcp- optional read-only MCP endpoint
Most /api/* routes require Authorization: Bearer <token>. Public exceptions are GET /api/auth/status, POST /api/auth/setup while the server is not initialized, and GET /api/mcp/status. WebSocket connections use the same master token. MCP uses its own tokens.
In the admin UI, opening a workspace shows its projects; each project opens on its own, with its JSON in an editor. Saving is deliberately strict:
- The JSON must parse and match the project schema exactly
(
src/yjs/projectSchema.js, derived from the app'sProjecttype). Unknown fields, UI-only fields (isNew, …), wrong types, missing required fields, duplicate ids and id changes are rejected with the path of every problem. Dangling references (a task in a missing group, …) are warnings. - You see a line diff of the stored version against yours and must confirm it.
- The save carries the version (hash) you loaded. If anyone changed the project since (the app, another admin tab), it is rejected with a conflict and you can reload.
- The write is one Yjs transaction on the live document, so connected apps get it at once and never see a half-applied project; it is then stored immediately.
- The version being replaced is kept (last
PROJECT_HISTORY_LIMITper project) and can be restored from History, again via diff and confirmation.
API (all require the master token):
| Method | Path (under /api/admin/documents/:document/projects) |
Body |
|---|---|---|
GET |
/ |
– list { id, title, description, tasks, openTasks } |
GET |
/:id |
– returns { project, version, validation } |
PUT |
/:id |
{ baseVersion, project } replace the whole project |
PATCH |
/:id |
{ baseVersion, patch } set top-level fields (null removes one) |
POST |
/:id/validate |
{ project } dry run, returns { ok, errors, warnings } |
GET |
/:id/history |
– previous versions |
GET |
/:id/history/:historyId |
– one previous version with its JSON |
POST |
/:id/history/:historyId/restore |
{ baseVersion } |
Errors: 400 malformed request, 404 unknown document/project, 409
conflict (currentVersion included), 422 invalid project (errors[] with
path and message). Nothing is written on any error.
import * as Y from "yjs";
import { HocuspocusProvider } from "@hocuspocus/provider";
const doc = new Y.Doc();
const provider = new HocuspocusProvider({
url: "ws://localhost:8080",
name: "my-document",
document: doc,
token: "your-master-token",
});MCP is off until you create an MCP token in the admin UI (/, MCP Access). You can create one token per client, optionally named (otherwise "Token N"), and revoke each one separately; the list shows the token prefix, creation date, and last use. The full token is shown only once, and the server stores only its SHA-256 hash. While at least one token is active, the server exposes a read-only Model Context Protocol endpoint at POST /mcp. This is useful for AI clients that need structured access to Focus Compass data without direct database access.
Available tools:
list_documentsget_workspacelist_projectsget_project
Example for Claude Code:
claude mcp add --transport http focus-compass http://localhost:8080/mcp \
--header "Authorization: Bearer YOUR_MCP_TOKEN"The repository also serves a skill template at /focus-compass-skill.md.
If you contribute, keep changes small, follow the existing ESM/Node.js style, and run npm run lint before opening a PR.