Skip to content

Repository files navigation

Draw Together

A cooperative, two-player browser whiteboard for long-distance friends: one shared canvas, normal drawing + pixel art, live cursors, chat, and peer-to-peer voice.

What it is

Two friends open the same room link and land on one shared canvas — not two canvases stitched together. Everything either of you draws, erases, or places is visible to the other in real time, whether you're free-drawing, using shapes, or working in pixel-art mode.

Features

  • Real-time shared drawing: pen, marker, eraser, line, rectangle, circle
  • A fully shared eraser — either player can erase anything, regardless of who drew it
  • Pixel-art mode (8×8 / 16×16 / 32×32 / 64×64 density) that lives in the same shared world as normal drawing — mix and match freely, switch modes without losing anything
  • Exactly two players per room, with a friendly "This room is full" for anyone else
  • Live cursors with names and colors
  • Text chat with sanitization and rate limiting
  • Peer-to-peer voice chat (WebRTC) that degrades gracefully if it can't connect
  • Persistent rooms — refreshing, disconnecting, or the server restarting doesn't lose your drawing
  • Pan (drag / two-finger touch / space+drag) and zoom (scroll, pinch, ctrl+scroll)

Shared canvas architecture

There is exactly one authoritative drawing surface per room, not one per player. Concretely:

  • The server keeps an ordered log of operations (stroke, shape, pixel) per room, each with a server-assigned sequence number. It never receives or stores screenshots/images from clients.
  • Every client renders the same op log onto an offscreen bitmap ("the world"), in the same order. Erasing is implemented as a real canvas composite operation (destination-out), so an eraser stroke actually removes pixels underneath it — it doesn't matter who drew them.
  • New strokes are drawn locally the instant you move your pointer (no waiting on the network), then sent to the server as one batched operation. The server assigns the real sequence number and echoes it back to you (to reconcile bookkeeping) while broadcasting it to the other player.
  • Undo/redo is per-player: each player has their own undo stack of their own operations. The shared eraser is not affected by undo/redo semantics — it's just another operation.
  • Coordinates are always in world space, not screen pixels, so two players with different zoom levels and window sizes still see everything in exactly the same place (server/rooms.ts, shared/types.ts, client/src/engine/).

Normal drawing

Pen, marker (thicker), eraser, line, rectangle, and circle tools, with a color palette and adjustable brush size. Drawing feels instant locally; sync to the other player typically takes well under 100ms on a normal connection.

Pixel mode

Pixel art isn't a separate bounded canvas — it's the same infinite shared world, just quantized to a grid. Switching pixel density (8×8 → 64×64) changes how big each "pixel" is in world units; it doesn't create a new canvas or clear anything. Includes a pixel pen, pixel eraser, eyedropper, and a bounded flood-fill tool (fills up to 500 connected cells at a time, to keep it fast and predictable).

Multiplayer

Rooms hold exactly two player slots, assigned on a first-come basis. Each player gets a private reconnect token (stored in sessionStorage) so refreshing or briefly losing connection lets you back into your own slot — even though the room already shows "2/2" — while a third, unrelated visitor is turned away with "This room is full."

Chat

Real-time text chat alongside the canvas. Messages are sanitized server-side (HTML/script tags stripped) and rate-limited (8 messages per 10 seconds per player) to prevent spam.

Voice

Peer-to-peer audio via WebRTC, with signaling (offer/answer/ICE candidates) relayed over the same WebSocket connection used for drawing — no separate service needed. Uses a public STUN server; a TURN server can be added later by extending the ICE_SERVERS list in client/src/hooks/useVoice.ts, nothing else needs to change. If the microphone is blocked or the connection fails, drawing and chat are completely unaffected — voice is fully decoupled from the rest of the app.

Tech stack

  • Frontend: React + TypeScript, Vite, plain Canvas 2D (no heavy canvas library)
  • Backend: Node.js + Express, ws for WebSockets
  • Database: PostgreSQL (see "Database setup" below)
  • Voice: WebRTC (browser-native), signaling over the existing WebSocket
  • Validation: Zod schemas on every inbound WebSocket message

Project structure

client/               React app
  src/engine/          camera math + the canvas rendering engine (canvasEngine.ts)
  src/hooks/           useSocket (WebSocket + protocol), useVoice (WebRTC)
  src/components/      Toolbar, Chat, TopBar, VoiceControls
  src/pages/           Landing, Room
server/               Express + WebSocket server
  db.ts                 Postgres pool + schema
  rooms.ts               authoritative in-memory room state + persistence
  validate.ts             Zod schemas, sanitization
  ws.ts                   WebSocket message routing
  index.ts               entrypoint (dev/prod bootstrapping)
shared/types.ts        wire protocol — the single source of truth both sides import
tests/                 automated tests (see "Testing" below)

Local installation

You'll need Node.js 20+ and a PostgreSQL database (see "Database setup").

npm install

Running locally

npm run db:migrate   # creates the database tables (safe to re-run)
npm run dev           # starts the dev server with hot reload at http://localhost:5000

For a production-like run:

npm run build          # bundles the React app
npm run start           # runs the server against the built app

Testing with two players

  1. Start the server (npm run dev).
  2. Open http://localhost:5000 in one browser window, click Create Room, enter a nickname.
  3. Copy the invite link (or just note the room code).
  4. Open the link in a second, separate browser profile or incognito window (a normal second tab in the same profile can also work, since each player has their own session token, but a separate profile/incognito window most closely simulates two different people).
  5. Draw in one window, watch it appear in the other. Try erasing each other's drawings, switching one window to pixel mode while the other stays in normal mode, and refreshing either window mid-session.

Database setup

This app uses PostgreSQL for persistence — rooms, drawing history, and chat all survive server restarts and page refreshes. It intentionally does not rely on the local filesystem for storage, since that isn't reliably persistent across deployments on Replit.

Connection is entirely through the DATABASE_URL environment variable (standard postgres://user:pass@host:port/dbname format). The schema is created automatically on startup (and via npm run db:migrate) — there's no manual SQL to run.

Environment variables

Variable Required Purpose
DATABASE_URL Yes Postgres connection string. On Replit, the Database tool sets this for you.
PORT No Port to listen on. Replit sets this automatically; defaults to 5000 locally.

Copy .env.example to .env for local development. Never commit a real .env file — on Replit, use the Secrets tool instead (see below).

Replit setup

Because this project assumes no coding experience, here's every step spelled out.

  1. Create a Repl. On replit.com, click Create Repl, choose Import from GitHub (if you've pushed this project there) or Node.js as a blank template and upload/drag in this project's files.
  2. Add a database. In the left sidebar, find the Database tool (sometimes under a "Tools" menu) and enable it. Replit provisions a PostgreSQL database and automatically sets a DATABASE_URL secret for you — you don't need to type a connection string yourself.
  3. Install dependencies. Open the Shell tab (not the "Console") and run:
    npm install
    
  4. Create the database tables. In the same Shell:
    npm run db:migrate
    
  5. Run the app. Click the big green Run button, or type npm run dev in the Shell.
  6. Open the Preview. Replit opens a preview pane/tab automatically — that's your app's URL.
  7. Test as Player 1. In the preview, click Create Room and enter a nickname.
  8. Open a second window. Copy the preview URL, open it in a new incognito/private window (this matters — it keeps the two players' sessions separate, just like two different people on two different computers).
  9. Test as Player 2. Paste in the room link (or the /room/CODE URL) and enter a different nickname.
  10. Test shared drawing. Draw in one window, confirm it shows up in the other.
  11. Test pixel mode. Switch one window to Pixel mode and place a few cells.
  12. Test chat. Send a message from each side.
  13. Test voice. Click "Enable voice" in both windows and allow microphone access when prompted.
  14. Build the production version (once you're happy):
    npm run build
    
  15. Deploy. Use Replit's Deploy button (top right) to publish the app. Choose "Autoscale" or "Reserved VM" deployment — either works, since all state lives in Postgres rather than on local disk. Replit will run npm run build and npm run start for you if asked, or you can set those explicitly in the deployment's build/run commands.
  16. Open the public URL Replit gives you after deploying.
  17. Send the room link to a friend and start drawing.

Troubleshooting

  • "Something went wrong creating the room" — usually means DATABASE_URL isn't set or the database tables haven't been created yet. Run npm run db:migrate.
  • Voice shows "Microphone blocked" — the browser denied mic access. Check the browser's site settings (usually a padlock/icon in the address bar) and allow the microphone, then click "Enable voice" again. Drawing and chat work fine either way.
  • Voice shows "Voice unavailable" — the two browsers couldn't establish a direct peer-to-peer connection (this can happen on some restrictive networks, like certain corporate or campus Wi-Fi, which block the kind of connection WebRTC needs without a TURN server). Drawing and chat are unaffected.
  • A third browser window can't join — this is intentional; rooms are capped at two players.
  • Drawing looks slightly blocky when zoomed in a lot — normal-mode strokes are rasterized at 1:1 scale and then scaled up without smoothing (so pixel art stays crisp); zooming in far enough on regular strokes will show that scaling. This is a deliberate trade-off, not a bug.

Known limitations

  • Undo/redo replays the whole canvas. Because erasing is a real compositing operation, undoing/redoing rebuilds the shared bitmap from the full operation history rather than surgically patching one stroke. For a casual two-person session this is fast and imperceptible; on a very long-running room with thousands of operations, undo could become noticeably slower. There's no automatic history compaction/snapshotting in this version.
  • The world is large but not infinite — it spans roughly ±2000 world units in both directions (rendered as one 4000×4000 bitmap). That's a genuinely large whiteboard for two people, but drawing far enough outside that area won't be captured.
  • Flood fill is capped at 500 cells per click, to keep it fast and prevent runaway fills on what is otherwise an unbounded pixel grid.
  • Op-level spam protection is minimal. Chat has rate limiting; individual drawing operations are validated (size, color, coordinate bounds) but not rate-limited per second. For a trusted two-friends use case this is a reasonable trade-off, but it's not hardened against a deliberately abusive client.
  • No TURN server configured. Voice uses a public STUN server only, which works for the large majority of home/mobile networks but can fail on networks with strict NATs or firewalls (common on some corporate/public Wi-Fi). The code is structured so a TURN server can be dropped in later without other changes.
  • Browser/device testing was not performed by the AI that built this — see the "What was actually tested" note in the project's final report/handoff message. Please do a real two-browser pass yourself before relying on this for something important (e.g. a friend's birthday plan!).

Future improvements

  • Canvas history snapshotting/compaction for long-lived rooms
  • TURN server support for voice on restrictive networks
  • More brush styles (textures, opacity control for a true highlighter-style marker)
  • Optional room passwords or expiry
  • Export the canvas as an image

About

A cooperative, two-player browser whiteboard for long-distance friends: one shared canvas, normal drawing + pixel art, live cursors, chat, and peer-to-peer voice.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages