A modern, privacyβfirst, whiteboard with realtime collab, E2EE, and integrated video chat.
DrawDeck lets you sketch ideas, take notes, and collaborate live. It ships with three usage modes, strong clientβside encryption, and a lean, containerized monorepo you can selfβhost in minutes.
-
Three modes
- Solo β just open and draw. Endβtoβend encrypted (E2EE); nothing leaves your device unencrypted.
- Private (Duo) β 1:1 collaboration with peerβtoβpeer WebRTC video/audio and a lightweight WebSocket control channel. E2EE drawing payloads.
- Group β many participants with live shape broadcast over WebSocket. Drawing payloads are encrypted with the room key and relayed by the server (see Security Notes).
-
Privacy first β clientβside room keys, ephemeral sessions, zero persistence by default.
-
Fast Canvas β a custom canvas engine with selection, shapes, pencil, text, arrows, eraser, and keyboard shortcuts.
-
Rate limiting & queuing β IPβaware server limits + clientβside message queue with backoff to keep rooms smooth.
-
Video calling β builtβin WebRTC for Duo rooms.
-
Authentication β NextAuth with Google OAuth (optional for Solo, required for some org setups).
-
Modern stack β Next.js, TypeScript, Tailwind, shadcn/ui, native WebSocket (no Socket.IO), WebRTC, Docker.
-
Monorepo β
apps/DrawDeck(frontend),apps/ws(WebSocket),apps/rtc(RTC signaling).
.
ββ apps/
β ββ DrawDeck/ # Next.js frontend (client + minimal server routes)
β ββ ws/ # WebSocket server (rooms, shapes broadcast, rate limit)
β ββ rtc/ # RTC signaling server (for Duo video)
ββ packages/ # Shared packages
ββ docker/
β ββ Dockerfile.frontend
β ββ Dockerfile.websocket
β ββ Dockerfile.rtc
ββ turbo.json # Turborepo pipelines
ββ pnpm-workspace.yaml
ββ .github/workflows/ # CI/CD
Prereqs
- Node 18+ (Node 20 recommended)
- pnpm 9+
pnpm install
pnpm devBy default this will run:
- Frontend at http://localhost:3000
- WS server at ws://localhost:8080
- RTC signaling at http://localhost:8081
You can also start services individually:
pnpm --filter @app/drawdeck dev # or: cd apps/DrawDeck && pnpm dev pnpm --filter @app/ws dev # or: cd apps/ws && pnpm start pnpm --filter @app/rtc dev # or: cd apps/rtc && pnpm start
Environment variables are provided via .env.local in each app (never commit secrets). Keep a public template at .env.example.
NEXT_PUBLIC_WS_URL=
NEXT_PUBLIC_RTC_URL=
NEXT_PUBLIC_SITE_URL=
# NextAuth (optional locally)
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
NEXTAUTH_URL=
NEXTAUTH_SECRET=
PORT=
# rate limiting, secrets, etc. (implementation-specific)
PORT=
ββββββββββββββ WebRTC (P2P media) ββββββββββββββ
β Client A βββββββββββββββββββββββββββββΆβ Client B β
β (Duo) β β (Duo) β
βββββββ¬βββββββ Signaling (HTTP/WS) βββββββ¬βββββββ
β β
β Shapes/events (WS, encrypted) β
βΌ βΌ
βββββββββββββββββββββββββββββββββββββ
β WS Server (apps/ws) β
β - Rooms & membership β
β - Broadcast shapes/events β
β - Rate limit & IP cache β
βββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββ
β RTC Signaling β
β (apps/rtc) β
βββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββ
β Next.js Frontend β
β (apps/DrawDeck) β
βββββββββββββββββββββββββββββββββββββ
Next.js 15 Frontend-First β runs both client UI and lightweight server routes. Backend services (ws, rtc) are kept minimal.
No Mandatory Auth β anyone can instantly start drawing in Solo mode. Auth (Google OAuth via NextAuth) is only needed for collaboration or org setups.
Local-Only Solo Mode β drawings are stored locally in the browser and never touch a server.
Peer-to-Peer Collaboration β Duo sessions use WebRTC for direct audio/video and E2EE drawing sync.
Group Mode with Secure Broadcast β multiple participants sync shapes over a WebSocket layer; payloads are encrypted with a room key.
Ephemeral by Design β no canvas history is persisted unless you extend it. All sessions are temporary.
Encrypted Everywhere β all drawing data is encrypted client-side with room keys; servers only relay ciphertext.
Hook-based WebSocket Client β a clean React abstraction for connecting, syncing shapes, and handling backpressure.
Rate Limiting + Queueing β IP-aware rate limiting server-side; client has a message queue with retry/backoff.
- Solo β local only; drawing payloads encrypted clientβside; nothing readable serverβside.
- Private (Duo) β WebRTC for media (P2P), WebSocket for control/shapes; room key E2EE.
- Group β many participants over WS; shapes encrypted with room key and relayed by server. See notes below.
- Primitive types: rectangle, diamond, circle, arrow, line, pencil, text, eraser, select, pan/hand.
- Keyboard shortcuts:
1..0map to tools (hand/select/shapeβ¦) - Message batching & prioritization when broadcasting.
- Room keys are generated/handled clientβside and used to encrypt shape payloads.
- Solo & Duo are endβtoβend encrypted: payloads are unreadable by the server.
- Group: payloads are encrypted with the room key and relayed by the WS server. If the server never sees decrypted content, this is roomβkey encryption with relay. Depending on your threat model, treat this as E2EE if keys never leave clients; otherwise as transport+payload encryption.
- No persistence by default β sessions are ephemeral; add storage if you need history.
- Auth β NextAuth + Google OAuth when you want identity; Solo can be anonymous.
- Serverβside: IPβaware rate limiting; the server can return
rate_limit_exceededwithretryAfter. - Clientβside: a token bucket (default ~50 msgs/min per client, configurable) + a priority queue. When over limit, DrawDeck queues messages and drains gradually (every ~2s) to avoid bursts.
- Connection attempts: exponential backoff with temporary blocks after repeated failures.
Build images (from repo root):
# Frontend
docker build -f docker/Dockerfile.frontend -t acidop/drawdeck-frontend:dev .
# WebSocket
docker build -f docker/Dockerfile.websocket -t acidop/drawdeck-ws:dev .
# RTC
docker build -f docker/Dockerfile.rtc -t acidop/drawdeck-rtc:dev .Run containers:
# Frontend (port 3000)
docker run -d --name drawdeck-frontend -p port-machine:port-locally \
-e NEXT_PUBLIC_WS_URL=ws://your-host:port-ws \
-e NEXT_PUBLIC_RTC_URL=http://your-host:port-rtc \
-e NEXT_PUBLIC_SITE_URL=http://your-host:port \
-e GOOGLE_CLIENT_ID=... -e GOOGLE_CLIENT_SECRET=... \
-e NEXTAUTH_URL=http://your-host:port -e NEXTAUTH_SECRET=... \
acidop/drawdeck-frontend:dev
# WebSocket (port )
docker run -d --name drawdeck-ws -p machine-port:port-ws acidop/drawdeck-ws:dev
# RTC (port 8081)
docker run -d --name drawdeck-rtc -p machine-port:port-rtc acidop/drawdeck-rtc:dev- Each service has its own workflow (
frontend,ws,rtc). - Path filters ensure we only build/deploy when files in that app change.
- Images are tagged with commit SHA (and optionally
latest). - VM deploy script stops the old container, pulls the new image, runs it, and prunes old images.
Common root scripts:
pnpm buildβ Turborepo build for all appspnpm devβ start dev serverspnpm lintβ lint all packagespnpm check-typesβ typecheckpnpm start:clientβ start frontend (production)pnpm start:wsβ start WebSocket serverpnpm start:rtcβ start RTC signaling server
PRs and issues welcome! Please:
- Fork & branch from
main. pnpm installand runpnpm devto reproduce.- Add or update tests if applicable.
- Open a PR with a clear description and screenshots/gifs.
MIT. See LICENSE.