Back to work
Collaborative Tools / Real-time·2025

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
NovaDraw collaborative whiteboard canvas 1
NovaDraw collaborative whiteboard canvas 2
NovaDraw collaborative whiteboard canvas 3
01

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.

02

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.

03

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.

04

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.

05

Challenges

High

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.

Medium

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.

High

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.

06

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

src/components/CollaborativeCanvas.tsxtypescript
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);
  }
}