> ## Documentation Index
> Fetch the complete documentation index at: https://docs.augustin.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Excalideck

Self-hosted [Excalidraw](https://excalidraw.com/) with a gallery, collections, and real-time collaboration. Runs on `debian-1`, deployed as two swarm services that share a bind mount.

* Web: [excalideck.augustin.ai](https://excalideck.augustin.ai)
* Collab WS: [excalideck-collab.augustin.ai](https://excalideck-collab.augustin.ai) (browser-only; the web app sends the URL to clients)

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

```bash theme={null}
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](https://modelcontextprotocol.io) 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:

```bash theme={null}
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`):

```bash theme={null}
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](https://github.com/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

```bash theme={null}
curl https://excalideck.augustin.ai/health
curl https://excalideck-collab.augustin.ai/health
```
