This file is for agentic coding agents working in this repo. Focus Compass Server is a small Node.js (ESM) WebSocket + REST server built on Hocuspocus/Yjs with SQLite persistence.
- Runtime: Node.js >= 24 (see
package.json), ESM ("type": "module") - Entrypoint:
src/server.js - Persistence: SQLite file at
DB_PATH(default./data/db.sqlite) - Images: files under
IMAGES_DIR(default./data/images) plus*.meta.json - Auth: Bearer token (
ACCESS_TOKEN) for WebSocket + all REST endpoints
Local (no build step; plain Node runtime):
- Install deps:
npm ci(preferred) ornpm install - Dev (watch):
npm run dev(node --watch src/server.js) - Start:
npm start(node src/server.js) - Lint:
npm run lint(fix:npm run lint:fix)
Notes:
- Requires Node.js >= 24.
- npm is the expected package manager (
package-lock.jsonis present).
Docker:
- Start:
docker compose up -d(logs:docker compose logs -f) - Stop:
docker compose down(wipe volume:docker compose down -v, destructive)
Smoke checks:
curl http://localhost:8080/healthcurl -H "Authorization: Bearer $ACCESS_TOKEN" http://localhost:8080/api/images- Admin UI:
http://localhost:8080/
- No unit-test suite.
tests/holds integration/e2e smoke tests for the published image and the setup guides; they need Docker and/or a running server:npm run test:e2e— app-equivalent Hocuspocus client against a live servernpm run test:docker-guide— pulls and runs the published GHCR imagenpm run test:install-script— exercises thecurl | bashinstallernpm run test:install-script:local— same guide test inside a disposable Podman/Docker container against the sibling app installer (Windows-safe Node launcher; rootless Podman uses a user firewall drop-in so compose can publish 127.0.0.1:PORT)
- For a quick local check without Docker:
node --check src/server.js(syntax) plusnpm run lint.
- ESLint v9 flat config in
eslint.config.js(@eslint/jsrecommended). - No formatter is configured; keep formatting changes local to the code you touch.
- Lint a single file:
npx eslint src/server.js(add--fixto auto-fix).
src/server.js: entry point — Hocuspocus server +onRequestrouter that dispatches tosrc/routes/*; also sets CORS/security headers and auth hookssrc/routes/*.js: REST handlers (auth,images,admin,workspace,projects,static,mcp,mcpAdmin)src/lib/*.js: shared helpers (auth,http,responses,db,fs,env, …)src/yjs/*.js: Yjs document decoding/inspection helperssrc/services/backup.js:BackupServiceused byonStoreDocumentsrc/routes/projects.js,src/services/projectEditor.js: admin project editor API (/api/admin/documents/:doc/projects/*); writes go through a Hocuspocus direct connection in one Yjs transaction with a baseVersion checksrc/services/projectHistory.js: previous project versions (PROJECT_HISTORY_DB_PATH)src/yjs/projectSchema.js: strict project schema; keep it in sync with the app'sProjecttype (focus-compass-app/src/types/index.ts)src/yjs/projectSync.js: minimal-diff JSON → Y.Map/Y.Array sync andprojectVersionsrc/index.html: static admin UI served by the serversrc/mcp/server.js: MCP server factory (createMcpServer)src/mcp/tools.js: MCP tool definitions (read-only)src/routes/mcp.js: MCP HTTP handler (POST /mcp, stateless Streamable HTTP)src/routes/mcpAdmin.js: MCP admin API (/api/mcp/*)src/focus-compass-skill.md: Claude Code skill template (served as static)eslint.config.js: ESLint v9 flat configdocker-compose.yml,Dockerfile: production containerizationCLAUDE.md: repository-specific operational notes (keep consistent)
Dokploy and other consumers follow main / the pinned image tag, so main must
never reference an image that is not on GHCR yet. Always release with:
scripts/release.sh 0.0.10 --dry-run # preview every step, changes nothing
scripts/release.sh 0.0.10 # real release (add --skip-tests only if CI already passed)Prerequisites: clean tree on main equal to origin/main, gh logged in, and a
## X.Y.Z - Unreleased (or ## Unreleased) section in CHANGELOG.md.
Order enforced by the script (main is only touched in the last step):
- Create
release/vX.Y.Z; bumppackage.json/lock, thevX.Y.Zimage refs in.env.example,docker-compose.yml,README.md,.github/workflows/guides-e2e.yml,tests/test-docker-guide.sh; date the CHANGELOG section. npm test,npm run lint,npm audit --omit=dev(same gates as CI).- Commit
release: prepare vX.Y.Z, push the branch and tagvX.Y.Z. (publish-docker.ymlruns onmainandv*tags only, so the release branch itself triggers nothing; the GitHub release job runs for tags only.) - Wait for the tag's "Publish Docker Image" run (
gh run watch --exit-status). - Check the
vX.Y.Zmanifest on GHCR returns 200 (anonymous pull token). - Fast-forward
origin/mainto the release commit (no force) and delete the release branch.
If anything fails before step 6 the script stops, main is unchanged, and it
prints the exact local/remote cleanup commands (delete the pushed tag and branch
before retrying). Never push a version bump straight to
main.
- Use
.env.exampleas the source of truth for env var names. - Do not commit secrets:
.envis gitignored (see.gitignore). - Do not commit runtime data:
data/,*.sqlite, backups, images are ignored. - Auth constraints (enforced in
src/server.js):- If
ACCESS_TOKENis not set, the server starts in setup mode and the first visit to/can generate a token that is persisted toAUTH_FILE_PATH(default./data/auth.json). - Locked product decision: public first-visitor token creation is the intended consumer onboarding model, not a vulnerability or missing hardening. Do not add a bootstrap secret, pairing code, SSH confirmation, IP allowlist, localhost-only initialization, or takeover warning unless the project owner explicitly revisits this decision. Security review should instead cover atomic one-time initialization, token quality, persistence, authenticated use, rotation, and secret handling.
- If
These guidelines reflect existing patterns and are designed to keep changes coherent and ESLint-clean.
- Use ESM (
import/export); do not introducerequire(). - Use
node:specifiers for built-ins (e.g.node:fs/promises,node:path). - Include file extensions for local imports (
./services/backup.js) in ESM. - Prefer named imports; keep imports at top; group roughly: side-effect imports, third-party, Node built-ins, local modules.
- Match the file's existing style first; for new/major edits prefer:
- 2-space indentation; semicolons; double quotes
- trailing commas in multi-line literals/args
- Keep functions small; avoid deep nesting by using early exits.
camelCasefor variables and functions; verb phrases for functions.PascalCasefor classes;UPPER_SNAKE_CASEfor constants/regexes.- Boolean names start with
is/has/should/can.
- This repo is JavaScript-only today; use runtime validation for untrusted inputs.
- Use JSDoc for non-obvious shapes and exported APIs (
@param,@returns). - Avoid adding TypeScript without an explicit reason and repo-wide agreement.
Response helpers use a named sentinel — RESPONSE_SENT, a Symbol exported from
src/lib/responses.js — to short-circuit the router chain:
json(),text(), andnoContent()write the response thenthrow RESPONSE_SENT.- After calling them, do not continue execution.
- In
catchblocks meant for real errors, rethrow the sentinel first:if (err === RESPONSE_SENT) throw err; - The top-level
onRequestcatch translatesRESPONSE_SENTinto the falsythrow nullHocuspocus expects (a truthy throw would crash the process). - Avoid double-writing responses (Node errors if headers are already sent).
Practical implications:
- If you add a
try/catcharound route code, make sure the sentinel still escapes. - If you stream a response manually (e.g.
pipeline(createReadStream(...), res)), end bythrow RESPONSE_SENTso later routes do not run.
- All
/api/*routes require Bearer token auth (seecheckAuth()).- Respond
401with{ error: "Unauthorized" }+WWW-Authenticate: Bearer.
- Respond
- CORS + security headers are set at the top of
onRequest; keep it that way. - Handle preflight with
OPTIONS->noContent(). - Use explicit status codes and stable JSON errors; prefer
Cache-Control: no-store.
Caching in this codebase:
- JSON helpers default to
Cache-Control: no-store. - Images are served with an ETag and long-lived
Cache-Control.
- Never compare auth tokens with
===; reusesafeEqual()and token normalization. - Validate path-like identifiers with an allowlist regex before
join(). - Do not trust client-provided MIME types; detect/verify server-side.
- Avoid logging sensitive values (tokens, raw auth headers, user-provided paths).
Common validation patterns:
- Image IDs must match
IMAGE_ID_RE(src/routes/images.js) before any filesystem access. - Document names are decoded and length-capped via
decodeDocName()(src/lib/db.js).
- Use
node:fs/promisesandasyncIO; keep operations best-effort when safe. - Treat
ENOENTas expected in cleanup paths and optional metadata reads. - Prefer atomic/idempotent patterns (e.g.
COPYFILE_EXCLfor uploads). - Keep
data/as the only mutable on-disk area for runtime state.
For uploads/metadata:
- Temp uploads go under
IMAGES_DIR/.tmpand are cleaned up best-effort. - MIME metadata is written to
*.meta.jsonas a best-effort optimization.
- WebSocket auth is enforced via
onAuthenticate; keep REST + WS auth aligned. onStoreDocumentis used for periodic DB backups; keep it fast and safe.YJS_GCtoggles history retention; default istrue(smaller docs, no full Yjs history).
When changing Yjs/Hocuspocus behavior:
- Prefer small, explicit changes; this server is intentionally minimal.
- Avoid expensive synchronous work inside request hooks.
- Served as a static file; no bundler and no external dependencies.
- Keep it self-contained (inline CSS/JS) and avoid adding frameworks.
- When changing API payload shapes, update both server handlers and this UI.