A production-grade real-time collaborative whiteboard web application similar to Google Docs + Canva + Miro, where multiple users can draw and edit a shared canvas in real time.
| Layer | Technology |
|---|---|
| Frontend | React + TypeScript, Vite, Konva.js, Zustand |
| UI | Tailwind CSS, Lucide Icons, Shadcn/ui |
| Backend | Node.js + Express (MVC), Socket.io |
| Database | MongoDB (via Mongoose) |
| Pub/Sub | Redis (via ioredis + @socket.io/redis-adapter) |
| Infrastructure | Docker Compose (Redis, MongoDB) |
canva-miro/
βββ docker-compose.yml # Redis + MongoDB containers
β
βββ backend/ # Node.js Express Server (MVC)
β βββ src/
β β βββ index.ts # Entry point β bootstraps DB, Redis, Socket, Server
β β βββ app.ts # Express app β middleware, routes
β β βββ config/
β β β βββ database.ts # MongoDB connection
β β β βββ redis.ts # Redis pub/sub client setup
β β βββ models/
β β β βββ Board.ts # Mongoose schema for Board (roomId + elements)
β β βββ services/
β β β βββ board.service.ts # Business logic (getBoardState, saveBoardState)
β β βββ controllers/
β β β βββ board.controller.ts # REST API handlers
β β βββ routes/
β β β βββ board.routes.ts # Express route definitions
β β βββ socket/
β β βββ socket.handler.ts # Socket.io event handlers
β βββ tsconfig.json
β βββ package.json
β
βββ frontend/ # React + Vite Application
βββ src/
β βββ main.tsx # React DOM mount
β βββ App.tsx # Root component with router
β βββ pages/
β β βββ Index.tsx # Main page β room generation + Whiteboard mount
β βββ components/
β β βββ canvas/
β β β βββ Whiteboard.tsx # Core canvas β drawing, tools, grid, zoom/pan
β β βββ toolbar/
β β βββ Toolbar.tsx # Drawing tool palette + color pickers + undo/redo
β βββ hooks/
β β βββ useSocket.ts # WebSocket connection hook (emit/receive events)
β βββ store/
β β βββ useBoardStore.ts # Zustand state β elements, tools, history, view
β βββ types/
β βββ canvas.ts # TypeScript types (ToolType, CanvasElement, Point, UserCursor)
βββ index.html
βββ vite.config.ts
βββ tailwind.config.ts
βββ package.json
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FRONTEND (React + Vite) β
β β
β βββββββββββ ββββββββββββββββ ββββββββββββββ ββββββββββββββ β
β β Index βββββΆβ Whiteboard βββββΆβ Toolbar β β useSocket β β
β β (Page) β β (Canvas) βββββΆβ (UI) β β (Hook) β β
β βββββββββββ ββββββββ¬ββββββββ ββββββββββββββ βββββββ¬βββββββ β
β β β β
β βΌ β β
β ββββββββββββββββ β β
β β useBoardStoreββββββββββββββββββββββββββββββββ β
β β (Zustand) β β
β ββββββββββββββββ β
ββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β Socket.io (WebSocket)
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BACKEND (Express + Socket.io) β
β β
β ββββββββββββ ββββββββββββββββββ βββββββββββββββββββββββ β
β β index.ts βββββΆβ app.ts βββββΆβ board.routes.ts β β
β β (Boot) β β (Express App) β β board.controller.ts β β
β ββββββ¬ββββββ ββββββββββββββββββ ββββββββββββ¬βββββββββββ β
β β β β
β βΌ βΌ β
β βββββββββββββββββββ βββββββββββββββββββ β
β β socket.handler βββββββββββββββββββββΆβ board.service β β
β β (Realtime) β β (Business Logic)β β
β ββββββββββ¬βββββββββ ββββββββββ¬βββββββββ β
β β β β
β βΌ βΌ β
β ββββββββββββββββ ββββββββββββββββ β
β β Redis Adapterβ β MongoDB β β
β β (Pub/Sub) β β (Persistence)β β
β ββββββββββββββββ ββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
User opens app β Index.tsx generates/retrieves roomId from localStorage
β Whiteboard mounts β useSocket connects to backend
β socket.emit('join-room', roomId)
β Server fetches stored board from MongoDB via BoardService
β Server sends 'board-state' to the joining user
β useBoardStore.setElements() renders all elements
User draws on canvas β handleMouseDown creates new CanvasElement
β addElement() updates Zustand store (local render)
β handleMouseUp β emitDraw(element) sends to server
β Server broadcasts 'draw-update' to other room users
β Other users' useSocket receives 'draw-update'
β addElement() renders the element on their canvas
User selects Eraser β clicks on an element
β removeElement(id) removes from local store
β emitRemoveElement(id) sends to server
β Server broadcasts 'element-removed' to room
β Other users' removeElement(id) removes it
User selects Text tool β clicks on canvas
β A textarea overlay appears at click position
β User types text and presses Enter
β handleTextSave() creates a CanvasElement of type 'text'
β addElement() + emitDraw() syncs to all users
Every draw/erase/clear action β commitHistory() saves current elements[] snapshot
Ctrl+Z β undo() reverts to previous snapshot β emitSyncBoard(elements)
Ctrl+Y β redo() moves forward in history β emitSyncBoard(elements)
Server broadcasts 'sync-board' β other users setElements()
| Event | Direction | Payload | Purpose |
|---|---|---|---|
join-room |
Client β Server | roomId |
Join a whiteboard room |
board-state |
Server β Client | elements[] |
Initial board state on join |
draw |
Client β Server | { roomId, element } |
Send a new/updated element |
draw-update |
Server β Client | element |
Broadcast element to other users |
remove-element |
Client β Server | { roomId, id } |
Remove a specific element |
element-removed |
Server β Client | id |
Broadcast element removal |
clear-board |
Client β Server | { roomId } |
Clear entire board |
board-cleared |
Server β Client | β | Broadcast board clear |
sync-board |
Bidirectional | { roomId, elements[] } |
Full state sync (undo/redo) |
cursor-move |
Client β Server | { roomId, cursor: {x,y} } |
Send cursor position |
cursor-update |
Server β Client | { userId, cursor } |
Broadcast cursor to others |
save-board |
Client β Server | { roomId, elements[] } |
Persist board to MongoDB |
| Property | Type | Description |
|---|---|---|
elements |
CanvasElement[] |
All canvas elements |
cursors |
Record<string, UserCursor> |
Connected users' cursor positions |
activeTool |
ToolType |
Currently selected tool |
selectedElementId |
string | null |
Currently selected element |
strokeColor |
string |
Current stroke color |
fillColor |
string |
Current fill color |
strokeWidth |
number |
Current stroke width |
zoom |
number |
Canvas zoom level (0.1β5) |
stagePos |
Point |
Canvas pan offset |
showGrid |
boolean |
Grid visibility |
history |
CanvasElement[][] |
Undo/redo history snapshots |
historyStep |
number |
Current position in history |
select Β· pan Β· pencil Β· rectangle Β· circle Β· line Β· arrow Β· text Β· eraser
- Infinite Canvas β Pan freely in any direction with the Hand tool or middle mouse button
- Zoom β Mouse wheel zoom anchored to cursor position (0.1x β 5x)
- Grid System β Dynamic grid that scales with zoom and extends with pan
- Freehand Pen β Smooth freehand drawing with customizable stroke
- Shapes β Rectangle, Circle, Line, Arrow with stroke + fill colors
- Text Tool β Click to place, type, and render text elements on canvas
- Eraser β Click on any element to remove it
- Color Pickers β Separate stroke color and fill color selection
- Stroke Width β Adjustable via range slider
- Clear Canvas β Remove all elements with one click
- Undo / Redo β Full history with Ctrl+Z / Ctrl+Y keyboard shortcuts
- Real-time Sync β All actions broadcast to room participants instantly
- Cursor Presence β See other users' cursors with labels in real-time
- Board Persistence β Save/load board state to/from MongoDB
- Node.js (v18+)
- Docker & Docker Compose
docker compose up -dThis starts Redis (port 6379) and MongoDB (port 27017).
cd backend
npm install
npm run devBackend runs on http://localhost:4000.
cd frontend
npm install
npm run devFrontend runs on http://localhost:5173.
Navigate to http://localhost:5173. A unique room ID is auto-generated. Share the same room ID (via localStorage) across tabs to test multi-user collaboration.
Refer to plan.md for the full roadmap. Upcoming items include:
- Selection & Multi-select with bounding box
- Layer management (z-index ordering, lock, hide)
- Image upload support
- Sticky notes
- Export to PNG/PDF
- User authentication & room sharing via URL
- Reconnection handling with state reconciliation