A real-time collaborative whiteboard built with Next.js, Excalidraw, and Y.js. Local-first with offline support, using CRDTs for conflict resolution and eventual consistency. No central authority.
This project is a collaborative whiteboard that allows users to:
- Draw freehand sketches
- Add Shapes (rectangles, ellipses, arrows, etc.)
- Eraser
- Collaborate Real-time: See other users' changes instantly
- Share-Via-URL: No login required. Just share the URL
- Offline Support: Continue working offline, changes sync when reconnected
- Frontend: Next.js (App Router), React, Tailwind CSS
- Whiteboard Engine: Excalidraw
- Real-time Sync: Y.js - CRDT (Conflict-free Replicated Data Type) library
- WebSocket Server: Y.js WebSocket server
- Offline Persistence: y-indexeddb
(Made on this project's site itself!)

Excalidraw elements are stored in a shared Y.Map:
- Key: Element ID (e.g.,
arrow_1702...) - Value: Excalidraw element object
When two users edit simultaneously, Y.js ensures eventual consistency:
| Scenario | Resolution |
|---|---|
| User A and B move same shape | "Last Write Wins" - latest timestamp wins |
| User A goes offline, adds shapes | Shapes merge when reconnected |
| User A deletes, User B modifies | Deletion typically wins |
The backend is data-agnostic. It only relays Y.js binary updates:
Client A → Yjs Update (binary) → Server → broadcast → Client B
It doesn't parse or validate the data - works with any Y.js content.
- Node.js (v18+)
- npm
-
Install Dependencies
npm install
-
Run Backend (Terminal 1)
cd apps/backend npm run dev(Runs on ws://localhost:1234)
-
Run Frontend (Terminal 2)
cd apps/frontend npm run dev(Runs on http://localhost:3000)
.
├── apps
│ ├── backend # Raw Y.js WebSocket Server
│ │ └── raw-server.ts
│ └── frontend # Next.js App
│ └── components
│ └── ExcalidrawCanvas.tsx
├── package.json # Monorepo configuration
cd apps/backend
railway init
railway up- Push to GitHub
- Import project at vercel.com
- Set root directory:
apps/frontend - Add environment variable:
NEXT_PUBLIC_WS_URL=wss://your-backend.railway.app
| Variable | Location | Description |
|---|---|---|
PORT |
Backend | Railway injects automatically |
NEXT_PUBLIC_WS_URL |
Frontend | WebSocket URL (wss:// for production) |