Skip to main content
Self-hosted Excalidraw with a gallery, collections, and real-time collaboration. Runs on debian-1, deployed as two swarm services that share a bind mount.

Architecture

  • excalideck-web — SvelteKit app, SQLite metadata, .excalidraw JSON + SVG thumbnails on disk
  • excalideck-collab — plain Node WebSocket server speaking the Yjs sync/awareness protocol, one room per diagram, flushes to disk every 10s and on shutdown
  • Shared state: apps/excalideck/data/ bind mount holds excalideck.db, diagrams/, thumbnails/
  • Auth: single-user, session cookies, scrypt passwords, rate-limited
  • Collab auth: web mints short-lived HMAC-signed tokens scoped to (userId, diagramId); collab verifies with the shared COLLAB_SECRET — no DB access needed

Secrets

COLLAB_SECRET in apps/excalideck/.env. Rotating it invalidates in-flight collab sessions but does not log users out. Generate with openssl rand -base64 48; must be identical on both services (env_file: .env on both handles this).

Password reset

CID=$(docker service ps -q --filter desired-state=running excalideck_excalideck-web | head -1 \
  | xargs docker inspect --format '{{.Status.ContainerStatus.ContainerID}}')
docker exec $CID pnpm reset-password <email>              # random password printed to stdout
docker exec $CID pnpm reset-password <email> <password>   # specific password
Invalidates all existing sessions for that user.

MCP server (AI diagram generation)

Excalideck hosts a token-authed MCP server at https://excalideck.augustin.ai/mcp (Streamable HTTP, stateless). It’s the replacement for the closed-source glyph MCP — diagrams are generated from a high-level spec and saved straight into the gallery, with no client-side “visual mode” dependency. Add it to any MCP client (Claude Code/Desktop/Cursor) with a personal access token:
claude mcp add --transport http --scope user excalideck \
  https://excalideck.augustin.ai/mcp \
  --header "Authorization: Bearer <token>"
Tools:
  • create_diagram{ name, spec, collectionId? } → builds a laid-out Excalidraw diagram (longest-path layering, text-sized boxes, edge-bound arrows), validates it, saves it with an SVG thumbnail, and returns the URL.
  • update_diagram{ id, spec, name? } — replace an existing diagram’s contents.
  • list_diagrams{ limit? } — recent diagrams with URLs.
The spec is structural: { layout: "TB"|"LR", nodes: [{ id, label, shape?, color? }], edges: [{ from, to, label? }] }. The server owns all geometry. Layout/validation/SVG live in the @excalideck/diagram package; the route uses the official @modelcontextprotocol/sdk web-standard transport behind a bearer check.

Tokens

Personal access tokens (exd_…) are stored as sha256 hashes in the api_tokens table. Mint one with the create-token CLI (operator tool, like reset-password):
EXCALIDECK_DATA_DIR=/home/user/apps/excalideck/data pnpm --filter @excalideck/web create-token <email> <name>
The homelab’s token lives in apps/excalideck/.env as EXCALIDECK_MCP_TOKEN.

REST ingest

The MCP server is built on a plain REST surface that’s also usable directly with session auth: POST /api/diagrams (create) + PUT /api/diagrams/:id ({ file, thumbnail? }, replace contents). Diagrams are per-account.

Upgrades

Images are built from amarcin/excalideck and pushed to the local registry. See apps/excalideck/AGENTS.md for the full build + redeploy recipe. Migrations run automatically on web boot.

Health

curl https://excalideck.augustin.ai/health
curl https://excalideck-collab.augustin.ai/health