A real-time collaborative whiteboard β think self-hostable Excalidraw with room-based drawing, auth, and persistence. Built as a Turborepo monorepo with a Vite + TanStack Router frontend, a Bun HTTP API, and a separate Bun WebSocket server.
Live: codraw.nerdev.in Β· Source: github
Most collaborative whiteboards either give up the hand-drawn Rough.js aesthetic for performance, or lock you into a closed cloud. Existing open-source alternatives like Excalidraw are client-side only β no real-time sync, no auth, no persistence. CoDraw fills that gap: it keeps the organic sketchy rendering, supports full real-time collaboration over WebSockets, and runs on modest infrastructure without depending on a vendor.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Nginx :80 / :443 β
βββββββββ¬βββββββββββββββββββββββ¬ββββββββββββββββββββββββββββ
β β
ββββββΌβββββ ββββββΌβββββ
β Frontendβ β HTTP β
β Vite+ β β Backend β
β TanStackβ β :3001 β
β :5173 β ββββββ¬βββββ
ββββββ¬βββββ β
β ββββββββΌβββββββββββ
β β PostgreSQL β
β β (Neon) β
β βββββββββββββββββββ
β
ββββββΌβββββββββββββββββββββ
β WebSocket Backend β
β :8080 β
βββββββββββββββββββββββββββ
Three independent services behind Nginx:
- Frontend (
apps/frontend) β Vite + TanStack Router + React 19. Canvas rendering with Rough.js, local state management, WebSocket client, auto-save, export. Type-safe file-based routing viasrc/routes/. - HTTP Backend (
apps/http-backend) β Bun + native Web API. Auth, room CRUD, session management, shape persistence with optimistic concurrency. - WebSocket Backend (
apps/ws-backend) β Bun + WebSocket server. Real-time shape diff broadcasting, cursor sync, chat, room management.
All three run under PM2 on a single EC2 instance (t3.small), with Neon providing managed PostgreSQL. CI/CD is a two-stage GitHub Actions pipeline: CI runs typecheck, build on every PR and push to main; on success, the deploy workflow ships the prebuilt Vite dist/ artifact to EC2 via scp, pulls latest source, installs deps, runs migrations, and restarts PM2 β no rebuild on the instance.
| Factor | Decision | Rationale |
|---|---|---|
| Bundle size | Vite | No SSR overhead, smaller client bundle for canvas-heavy app |
| Routing | TanStack Router | Type-safe file-based routing, first-class search params, loaders |
| Dev experience | Vite | Faster HMR, native ESM, simpler config |
| Canvas workload | Client-only | No SSR needed for canvas; avoids hydration mismatches |
| Bundle analyzer | Vite plugin | Built-in rollup-plugin-visualizer support |
| Factor | Decision | Rationale |
|---|---|---|
| Performance | Bun | 2-3x faster cold starts, native TypeScript, built-in test runner |
| WebSocket | Bun.serve | Native WebSocket server with pub/sub, no external deps |
| SQLite/Postgres | Bun.sql | Zero-config driver, faster than node-postgres |
| Package manager | bun install | 10-20x faster than npm/yarn |
| Factor | Decision | Rationale |
|---|---|---|
| Aesthetic | Hand-drawn | Organic, approachable feel β differentiates from sterile corporate tools |
| Performance | Canvas-based | GPU-accelerated, scales with zoom, no DOM overhead |
| Extensibility | Plugin system | Custom shapes, renderers, exporters |
| Factor | Decision | Rationale |
|---|---|---|
| Build caching | Remote/local | Shared cache across CI and local |
| Workspace deps | workspace:* |
Single bun install, version sync |
| Pipeline | turbo run |
Parallel, topological task execution |
| Factor | Decision | Rationale |
|---|---|---|
| Serverless | Auto-scale | No connection pooling needed, scales to zero |
| Branching | Schema migrations | Preview deployments with isolated DB branches |
| Managed | Zero ops | Backups, PITR, read replicas included |
User draws on canvas
β Frontend computes shape diff
β WebSocket sends shape-diff message
β WS Backend broadcasts to all users in room
β Each frontend applies diff to local state
β Debounced auto-save persists to Postgres via HTTP API
The frontend computes minimal diffs (added, modified, deleted shape IDs) β not full state β and sends only the delta over WebSocket. Each receiving client applies the diff immediately. Persistence is separate: the frontend debounces saves (1.5s) and sends the full state to the HTTP backend, which writes to Postgres with optimistic concurrency control.
On reconnection, the server sends the full authoritative state and the client replaces its local state. No delta replay needed.
- Tools: Select, Pencil, Rectangle, Ellipse, Diamond, Arrow, Line, Text, Image, Eraser
- Styling: Stroke color, fill color, roughness, opacity, stroke width
- Operations: Undo/Redo (delta-based stack, cap 100), Copy/Paste, Group/Ungroup, rubber-band multi-select
- Export: PNG, SVG, JSON export/import
- Room-based: create/join rooms via slug
- Real-time sync: diff-based WebSocket broadcasting
- Cursor sync: see other users' cursors with names
- Chat: per-room text chat
- Session-based auth: bcrypt password hashing, httpOnly cookies, server-side sessions
- Token revocation: logout invalidates session immediately
- WebSocket auth: short-lived WS tokens via
/auth/ws-token, heartbeat withre_auth - CORS: locked to
ALLOWED_ORIGINS - Rate limiting: sliding window on auth endpoints
- Input validation: room slug regex
[a-zA-Z0-9_-]
- Dirty-rect rendering: only redraws changed regions
- Layer caching: static / active / overlay layers
- Zoom-aware strokes: Rough.js stroke scales with zoom
- Debounced auto-save: 1.5s debounce with version checking
- Optimistic concurrency: 409 on version mismatch, client-side merge
- Theme: light/dark mode with localStorage persistence
- Design system: shared
@repo/uiprimitives β island surfaces (SURFACE/PANEL), filled active states, Excalidraw-style hand-drawn chrome - Touch: pinch-to-zoom, two-finger pan, double-tap text edit
- Keyboard shortcuts: V/H/R/O/T/E/A/D/P/I for tools, Ctrl+Z/Ctrl+Shift+Z, Escape, Delete
- Error boundaries: graceful fallback on canvas errors
- WebSocket reconnection: exponential backoff, 1s initial / 30s max
- CI/CD: GitHub Actions β typecheck, lint, build on every PR; deploy to EC2 on merge to main
- Infrastructure: Nginx reverse proxy, PM2 process manager, Neon Postgres
- Incident documentation: root-cause analysis for production issues
- Bun 1.2+
- PostgreSQL (local or Neon)
bun installcp .env.example .env| Variable | Description |
|---|---|
DATABASE_URL |
PostgreSQL connection string |
JWT_SECRET |
JWT signing secret (β₯ 32 chars) |
ALLOWED_ORIGINS |
Comma-separated CORS origins |
NEXT_PUBLIC_HTTP_BACKEND |
HTTP API base URL |
NEXT_PUBLIC_WS_URL |
WebSocket URL |
bun run dev- Frontend:
http://localhost:5173 - HTTP backend:
http://localhost:3001 - WebSocket backend:
ws://localhost:8080
bun run build| Command | Description |
|---|---|
bun run dev |
Start all apps in development mode |
bun run build |
Build all packages and apps |
bun run lint |
Run ESLint across all workspaces |
bun run format |
Format code with Prettier |
codraw/
βββ apps/
β βββ frontend/
β β βββ src/
β β β βββ routes/ # TanStack Router file-based routes
β β β β βββ __root.tsx # Root layout + providers
β β β β βββ index.tsx # Landing page
β β β β βββ signin.tsx # Sign in page
β β β β βββ signup.tsx # Sign up page
β β β β βββ canvas/
β β β β βββ $roomId.tsx # Canvas page (dynamic route)
β β β βββ components/ # React UI components
β β β βββ draw/ # Canvas engine, rendering, input handling
β β β βββ lib/ # Auth context, utilities
β β β βββ main.tsx # App entry point
β β β βββ routeTree.gen.ts # Auto-generated type-safe route tree
β β βββ index.html # Vite entry HTML
β β βββ vite.config.ts # Vite + TanStack Router plugin
β β βββ package.json
β βββ http-backend/
β β βββ src/ # Auth, room CRUD, middleware, session
β βββ ws-backend/
β βββ src/ # WebSocket server, room broadcasting
βββ packages/
β βββ db/ # Prisma schema + migrations
β βββ shapes/ # Shape type definitions + utilities
β βββ ui/ # Design system β SURFACE/PANEL, Slider, IconButton
β βββ common/ # Shared types, JWT utils, env config
β βββ typescript-config/ # Shared tsconfig presets
β βββ eslint-config/ # Shared ESLint configs
βββ deploy/ # Nginx + PM2 + deployment scripts
βββ docs/
βββ features.md # Detailed feature timeline
βββ incidents.md # Incident postmortems + lessons learned
βββ CoDraw.mdx # Full case study (portfolio)
βββ CoDraw-Whiteboard.md # Whiteboard interview script
See deploy.md for a complete production deployment guide to a single AWS EC2 instance with Nginx, PM2, and Certbot.
Quick summary:
- Push to
mainbranch - CI runs typecheck + build + lint
- On success, deploy workflow SSHes into EC2 and redeploys automatically
See features.md for the full day-by-day development timeline (Jan 2025 β Aug 2026).
| Phase | Date | What changed |
|---|---|---|
| Bootstrap | Jan 2025 | Monorepo scaffolding, HTTP + WS servers, frontend skeleton |
| Core Canvas | Jul 2025 | Canvas engine, drawing tools, shapes, undo/redo, export |
| Auth & Rooms | Jul 2025 | Signup/signin, bcrypt, Prisma schema, room CRUD |
| Real-time Sync | Jul 2025 | WebSocket backend, diff-based broadcasting, reconnection |
| Production Hardening | Aug 2026 | Session table, httpOnly cookies, optimistic concurrency, CI/CD, incidents |
| Frontend Migration | Aug 2026 | Next.js β Vite + TanStack Router |
MIT