Skip to content

Add Excalidraw - #73

Merged
max-tet merged 1 commit into
mainfrom
feat/add-excalidraw
Sep 14, 2026
Merged

max-tet merged 1 commit into
mainfrom
feat/add-excalidraw

Conversation

@ClaydeCode

@ClaydeCode ClaydeCode commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Excalidraw — Add to Freeshard app store

Full self-hosted Excalidraw stack (collaborative whiteboard), added alongside WBO — WBO is untouched. Fit was pre-approved (RECOMMEND): a polished, fully-MIT whiteboard that keeps server-persisted, returnable, real-time-collaborative boards.

License: MIT (all three application images) · Min size: s · Access: public (link-based, no accounts) · Preview zip (after CI): https://storageaccountportab0da.blob.core.windows.net/app-store/preview/feat/add-excalidraw/all_apps/excalidraw/excalidraw.zip

Services and images

Service Role Image (digest-pinned)
excalidraw (entrypoint) in-compose nginx reverse proxy nginx:1.27-alpine
excalidraw-frontend Excalidraw editor, runtime-configurable alswl/excalidraw@sha256:f605184e5c9a0c2b1247ffa23dacf50e7b2f86004d25dfc7f64eaf1a7018e2cd (tag v0.18.1-fork-b2, Excalidraw 0.18.1)
excalidraw-room real-time collaboration (socket.io) excalidraw/excalidraw-room@sha256:2fe999f9be4379e3ee282fc45d75d84a691a6383dde33544514cc395287c7a70 (tag sha-03ff435)
excalidraw-storage board persistence (Keyv HTTP API) alswl/excalidraw-storage-backend@sha256:ca35be07c0513e1f36be44fc40bffee4062ab8341e29f4133eafc48718acd88e (tag v2023.11.11)
excalidraw-redis Keyv store (AOF-persisted) redis:7-alpine

The single-subdomain routing (the hard part) — solved and verified

Freeshard gives one browser-facing entrypoint = one subdomain, but Excalidraw's browser talks to three origins: the editor, the room websocket, and the storage HTTP API. Solution: an in-compose nginx reverse proxy is the single entrypoint (excalidraw, port 80), routing under the one subdomain:

  • /socket.io/excalidraw-room:80 (with websocket upgrade)
  • /api/excalidraw-storage:8080 (backend global prefix /api/v2)
  • /excalidraw-frontend:80

The frontend is pointed at same-origin URLs so no cross-origin config is needed: VITE_APP_WS_SERVER_URL=https://excalidraw.{{ portal.domain }} (socket.io transport lands on /socket.io/, default namespace), and the storage URLs are relative (/api/v2/scenes/, /api/v2). The frontend image injects these into window._env_ at container start (launcher.py), so no rebuild is needed. Only the nginx entrypoint joins portal; the other four services sit on the internal excalidraw network (agents.md rules 1–3, 9).

Verified locally (isolated compose, rendered template, throwaway bridge network): frontend /→200 with my window._env_ config injected; /socket.io/ returns a real socket.io handshake advertising upgrades:[websocket]; POST /api/v2/scenes/→201 then GET→200 reading the exact bytes back; redis AOF persisted to the volume. docker compose config parses the rendered template with zero unset-variable warnings (the $$-escaped nginx runtime vars are correct).

Integration decisions

  • Access public. Collaboration is by shared link to people who are not paired to the shard, so pairing-gated private would defeat it. Board data on collaboration links is end-to-end encrypted; the link is the key. Same model as the existing mirotalk.
  • Storage backend = Keyv over redis, not sqlite. Both community storage images run node on a musl (alpine) base and their sqlite3 native binding fails to load (crash on boot — caught in the local run). Redis is Keyv's native backend with a pure-JS client; redis-server --appendonly yes on a volume gives durable, returnable boards. The official redis entrypoint chowns its volume, so no bind-mount EACCES.
  • Digest pinning (agents.md rule 6). The three application images publish only mutable tags, so each is pinned by digest exactly as the merged mirotalk precedent (re-point mirotalk to the upstream public image (digest-pinned) #60). app_version = 0.18.1 is the frontend's Excalidraw version (from the image tag), not a literal tag match. update_check.py raises OptOut (a digest pin defeats update.py's string-replace bump), with the manual-bump recipe in the reason string — same pattern as mirotalk.
  • Telemetry off: VITE_APP_DISABLE_TRACKING=true, VITE_APP_FIREBASE_CONFIG={}, and no analytics IDs set.
  • Lifecycle: idle_time_for_shutdown: 3600 so a paused live room is not torn down mid-session.
  • is_featured: false. The curation note recommends featuring Excalidraw / replacing WBO, but that is a product decision left to Max; this PR only adds the app alongside WBO.

FLAGS for review + the shard smoke-test

  • Shard smoke-test is the real gate. The local run validated routing on a plain bridge; it did not exercise Freeshard's outer Traefik. The one thing to confirm on a real shard is websocket upgrade end-to-end (Traefik → this nginx → room) and an actual two-participant live board. Everything up to nginx is proven.
  • amd64-only. All three application images are linux/amd64 single-arch (no arm64 manifest). Fine for amd64 shards; flag if any shard host is arm64.
  • Storage images are old (2022–2023) though MIT and API-stable; the editor (alswl fork) is current (Excalidraw 0.18.1, May 2026). The room server's newest build is 2023.
  • External shape-library CDN. The editor's built-in library browser still points at libraries.excalidraw.com (upstream default, fires only when a user opens the library panel). Left as-is; can be blanked via VITE_APP_LIBRARY_URL/VITE_APP_LIBRARY_BACKEND if a fully-offline posture is wanted.
  • No admin board recovery. Boards are random-ID, client-encrypted links; a lost link cannot be recovered by an admin (documented in the store hint). This is the one axis where WBO's named boards are nicer.

Licenses verified (raw LICENSE files)

  • excalidraw/excalidraw — MIT · excalidraw/excalidraw-room — MIT · alswl/excalidraw-storage-backend (MIT fork of kiliandeca) — MIT.

Checklist (agents.md)

  • Docker images identified and digest-pinned
  • docker-compose.yml.template set (multi-service, internal network, single entrypoint)
  • app_meta.json set (app_version, name, pretty_name, icon, entrypoints, paths, lifecycle, store_info, minimum_portal_size)
  • upstream_repo set
  • update_check.py present (OptOut, digest-pinned)
  • Icon present (official Excalidraw logo)
  • minimum_portal_size: "s"
  • Telemetry disabled
  • docker compose config passed; stack brought up and routing verified locally
  • Shard smoke-test (see FLAGS) — reviewer to run

Recommended reading order

  1. apps/excalidraw/docker-compose.yml.template — the stack and the nginx routing
  2. apps/excalidraw/app_meta.json — entrypoint, access, store info
  3. apps/excalidraw/update_check.py — digest-pin OptOut

Full self-hosted Excalidraw stack behind a single subdomain: an in-compose
nginx reverse proxy fronts the runtime-configurable frontend (alswl/excalidraw,
Excalidraw 0.18.1), the excalidraw-room collaboration server (socket.io) and
the excalidraw-storage-backend (Keyv over redis), routing / to the frontend,
/socket.io/ to the room and /api/ to the storage backend. The frontend is
configured with same-origin URLs so everything works under the one entrypoint.

All three application images are MIT and pinned by digest (they publish only
mutable tags); redis and nginx are pinned to versioned tags. Telemetry is
disabled via VITE_APP_DISABLE_TRACKING. Storage persists to a redis AOF volume;
the images' sqlite backend does not load its native binding on their musl base.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaNxkVRvaJrvmCwKn3fch8
@max-tet
max-tet merged commit 3dce16d into main Sep 14, 2026
2 checks passed
@max-tet
max-tet deleted the feat/add-excalidraw branch September 14, 2026 16:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants