NovaDraw
A real-time collaborative whiteboard on an infinite canvas — hand-drawn-style rendering, shareable rooms and Yjs CRDT sync so teams can brainstorm and draw together, just like an in-person whiteboarding session.
- Role
- Solo Developer & Architect
- Year
- 2025
- Category
- Collaborative Tools / Real-time
- Platform
- Web App
- Stack
- ReactTypeScriptViteYjsWebSocketExpressMongoDBRough.jsVercel



Why I built this
Teams need a free, open place to think visually together. NovaDraw targets an Excalidraw-level experience — an infinite canvas with hand-drawn aesthetics — where brainstorming, diagramming and shared visual work happen live across a room, without the friction of installs or per-seat pricing.
How it works
Ten drawing tools on an infinite canvas
Pencil, rectangle, ellipse, diamond, line, arrow, eraser, text, pan and selection tools, each with per-tool options (stroke style, dash/dot, opacity, roughness, fill, rounded/elbow arrows). The viewport supports momentum pan/zoom (0.1x–5x), fit-to-screen and keyboard shortcuts.
Shareable real-time rooms
A Y.Doc connects over y-websocket to a room joined via the URL hash (#room=<id>,<key>), with room ids and crypto keys generated by nanoid. All canvas elements live in a shared Y.Array so every participant edits the same document.
Live in-progress stroke previews
While a user is drawing, the in-progress element is broadcast through a shared Y.Map with normalized global coordinates, so collaborators watch strokes form in real time — before they are committed to the shared array — with 60fps debounced redraws to avoid flicker.
Undo/redo, multi-select and export
A custom dual-stack UndoRedoManager (50-entry cap with duplicate-state dedupe) works in both local and live modes, alongside multi-select, align/rotate, clipboard PNG/SVG and export to PNG/SVG/JSON.
Library, workspaces and media rooms
A shape library and workspaces persist to localStorage with thumbnails, plus a separate media-collaboration room type (comments/polls) driven by its own Yjs hook, and dark mode throughout.
Key decisions
Yjs was chosen for conflict-free multi-user editing: the shared Y.Array holds committed elements and a Y.Map streams in-progress strokes, letting the server stay a thin y-websocket relay rather than owning merge logic.
The React SPA ships to Vercel with an SPA rewrite while the Express + WebSocket server runs on Render, with production env files pointing the client at wss://novadraw.onrender.com and a CORS allowlist gating origins.
Board content is saved client-side to localStorage per board id for a fast start, with server-side document persistence (S3/Cloudinary) tracked as a deliberate next step rather than a launch blocker.
Backend architecture
y-websocket relay on Express
A WebSocketServer is attached to the same HTTP server as Express and delegates each connection to y-websocket setupWSConnection, classifying rooms by URL path (media-* vs whiteboard) and tracking room types and participants in memory.
In-memory CRDT documents
Yjs documents are held in a server-side Map with no persistence layer, so collaborative state is authoritative only while the process lives — clients reconcile via Y.Array/Y.Map structures and y-protocols awareness.
Mongoose data model
MongoDB Atlas via Mongoose 8 with User (bcrypt-hashed passwords, Google-OAuth users), Board (name, owner, content) and File (uploads) models, plus REST routes for auth, boards, files, chat, collab and media.
JWT + Firebase auth
Authentication uses JWTs (7-day expiry) via an Authorization-header middleware, with optional Firebase Google sign-in, hardened by helmet, morgan and a CORS allowlist.
Challenges
CORS and routing on a split deploy
Getting the Vercel frontend and Render backend to talk took several dedicated fixes — a vercel.json SPA rewrite, trimming a trailing slash from the app URL, and tuning the CORS allowlist — before cross-origin auth and WebSocket upgrades worked in production.
Flicker-free real-time canvas sync
Live editing needed coordinate normalization for in-progress elements, 60fps debounced redraws, JSON-comparison guards against redundant updates and Excalidraw-style seeding of the first user's local elements into the session — the hard 10% of collaborative canvases.
Document durability
Server-side Yjs documents are memory-only and client boards live in localStorage, so a server restart or cleared cache can lose a canvas until persistent storage lands.
What I learned
A shared CRDT array alone is not enough — live previews, debouncing and redundant-update guards are what make multi-user drawing feel real.
Env and deploy hygiene matters early: separating dev/production Vite configs and removing committed .env files prevents split-deploy CORS pain.
Lint and strict-type discipline paid off right before production — the clean build depended on clearing accumulated noUnusedLocals and type debt.
Code snippets
function broadcastInProgressElement(element: any) {
if (yjsEnabled && yInProgress) {
// Ensure in-progress elements use global coordinates
const globalElement = {
...element,
points: element.points
? element.points.map((point: Point) => ({ x: point.x, y: point.y }))
: element.points,
};
yInProgress.set(clientId.current, globalElement);
}
}