Skip to content

Repository files navigation

Focus Compass Sync Server

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.

Features

  • Realtime sync over WebSockets with Hocuspocus/Yjs
  • SQLite persistence under ./data by 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

Quick Start

Local

  1. Install dependencies: npm ci
  2. Start the server: npm run dev
  3. 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.

Docker

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 -f

The default docker-compose.yml points at the GHCR image reference above and persists runtime data in a Docker volume mounted to /app/data.

Port publishing vs. reverse proxy

  • A plain docker compose up in this repository auto-loads docker-compose.override.yml, which publishes the server on the host as ${HOCUSPOCUS_PORT:-8080} for direct http://<host>:<port> access.
  • Behind a reverse proxy (Dokploy/Traefik, Caddy, nginx, …) the base docker-compose.yml only exposes the port on the compose network so the proxy can route to it — the host port is not bound. Deploy platforms that run docker compose -f docker-compose.yml ... explicitly get this automatically (an explicit -f skips the override). With a bare docker compose up behind your own proxy, delete docker-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.

Configuration

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.

API Overview

  • GET /health - health check
  • GET / - admin UI
  • POST /api/auth/setup - initialize a token when running in setup mode
  • GET /api/workspace/:document - read a workspace snapshot
  • GET /api/images - list images
  • POST /api/upload - upload an image
  • GET /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 editor
  • POST /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.

Project editor

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:

  1. The JSON must parse and match the project schema exactly (src/yjs/projectSchema.js, derived from the app's Project type). 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.
  2. You see a line diff of the stored version against yours and must confirm it.
  3. 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.
  4. 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.
  5. The version being replaced is kept (last PROJECT_HISTORY_LIMIT per 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.

Client Example

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

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_documents
  • get_workspace
  • list_projects
  • get_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.

Contributing

If you contribute, keep changes small, follow the existing ESM/Node.js style, and run npm run lint before opening a PR.

License

Apache-2.0