A real-time collaborative whiteboard with a hand-drawn feel — an Excalidraw-style clone.
Sketch shapes, arrows, freehand and text on an infinite canvas, then share a private, password-protected room and draw together live with cursors and presence.
Next.js 16 · React 19 · WebSockets · rough.js · Prisma · Tailwind CSS 4
- Infinite canvas with pan, zoom (wheel / ⌘-scroll), and a dot grid.
- Hand-drawn rendering via rough.js — rectangles, diamonds, ellipses, arrows, lines, freehand draw, and text.
- Full editing — select, multi-select (rubber-band), move, resize (with font scaling for text), rotate, duplicate, delete, and layer ordering.
- Excalidraw-style style panel — stroke/background colors, fill style, stroke width & style, sloppiness, edges, opacity, font family & size, arrowheads.
- Real-time collaboration — shared canvas, live cursors with names, and an avatar stack, powered by a custom WebSocket server.
- Auto-persistence — canvas content is debounced and saved to PostgreSQL via Prisma, so a refresh keeps everything (no manual save).
- Private rooms — each room has a password; access is enforced server-side with an httpOnly, signed cookie (a share link alone can't bypass it).
- Undo / redo, keyboard shortcuts, and PNG export.
- No sign-up — pick a display name per session and go.
| Area | Choice |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack) |
| UI | React 19, Tailwind CSS 4 |
| Realtime / sync | Custom WebSocket Server (Node.js/ws) |
| Canvas rendering | rough.js + HTML Canvas 2D |
| Database | PostgreSQL via Prisma (room metadata & canvas elements) |
| Language | TypeScript |
Local-first canvas, synced through a custom WebSocket Server.
- The drawing engine (
lib/scene.ts,lib/rough-renderer.ts) is pure and framework-agnostic: element creation, hit-testing, resize/rotate math, and a cached rough.js renderer drawn on arequestAnimationFrameloop. Shapes are drawn in local coordinates and positioned withctx.translate, so moving an element never regenerates its rough path. CanvasStage.tsxkeeps per-user UI state (tool, viewport, selection) in local React state, while shared scene state lives in a custom RealtimeStore (shapes,elementOrder). Reads use React'suseSyncExternalStore; writes are broadcasted over a WebSocket using a Last-Writer-Wins (LWW) CRDT algorithm.- During a drag, edits are applied to an in-memory override map (instant local render) and flushed to the network on a throttle, so collaborators see smooth updates without per-frame network spam.
- Presence broadcasts each user's cursor (in canvas coordinates) and name for live cursors and avatars.
The custom standalone Node.js WebSocket server (server/index.ts) handles connections, routes messages, and resolves conflicts using monotonic sequencing. It also persists dirty canvas state to Postgres in the background so you never lose your drawings. The frontend connects to it using a secure HMAC ticket minted by a Next.js API route.
- Creating or joining a room verifies the password (
POST /api/boardsor/api/boards/[id]/join). - On success the server sets a signed, httpOnly, room-scoped cookie (
lib/roomToken.ts, HMAC, 24h TTL). POST /api/liveblocks-authauthorizes the realtime connection only if that cookie is valid — so the password leaves the browser after one submit, is never stored in client-readable storage, and a share link without the cookie returns403.- Passwords are stored scrypt-hashed in Postgres (
lib/password.ts).
app/
page.tsx # Home — create / join a room (name + password)
board/[boardId]/page.tsx # Room: access gate + canvas + share panel
api/
boards/route.ts # Create room (hash password, set cookie)
boards/[boardId]/join/ # Verify password, set access cookie
ws-ticket/route.ts # Mint a secure HMAC ticket for the WebSocket server
components/
canvas/ # Toolbar, StylePanel, ZoomControls, TextEditor, ExportMenu, CanvasStage
collab/ # RealtimeProvider, Cursors/AvatarStack, SharePanel
lib/
scene.ts, rough-renderer.ts # Drawing engine + renderer
types.ts, text.ts, export.ts # Element types, text measurement, PNG export
password.ts, roomToken.ts # scrypt hashing + signed access cookie
prisma.ts, user.ts # DB client + session helpers
realtime/ # Custom WebSocket client, LWW store, and hooks
prisma/schema.prisma # Board (room) model
server/ # Standalone WebSocket server (Node.js/ws)
git clone https://github.com/MeKaustubh07/CoSketch.git
cd CoSketch
npm installCreate .env (see .env.example):
# PostgreSQL — stores room metadata (id, name, hashed password) and canvas elements
DATABASE_URL="postgresql://user:password@host:5432/cosketch?sslmode=require"
# Shared HMAC secret for WS ticket auth (Next.js mints, WS server verifies)
WS_TICKET_SECRET="generate-a-long-random-string"
# Public URL of the WebSocket server (used by the client)
NEXT_PUBLIC_WS_URL="ws://localhost:8080"
# Secret used to sign room-access cookies
NEXTAUTH_SECRET="generate-a-long-random-string"npx prisma generate
npx prisma db pushnpm run dev
npm run dev:wsOpen http://localhost:3000 (Next.js frontend), pick a name, set a room password, and start sketching. The frontend will connect to the dev:ws server running on port 8080.
| Command | Description |
|---|---|
npm run dev |
Start the dev server (Turbopack) |
npm run build |
Production build |
npm run start |
Run the production build |
npm run lint |
Lint with ESLint |
V select · R rectangle · D diamond · O ellipse · A arrow · L line · P draw · T text · E eraser · H pan · ⌘/Ctrl+Z undo · ⌘/Ctrl+Shift+Z redo · ⌘/Ctrl+D duplicate · ⌘/Ctrl+A select all · Delete remove · Esc deselect
- The display name is per-session (sessionStorage), not an account.
- Not yet implemented: groups, copy/paste, image tool, and arrow-to-shape binding.
MIT — personal / educational project.