Drawgether

Case Study

Realtime · Canvas · Social

Drawgether

A social media platform built around collaborative drawing. Users join rooms, receive an AI-generated topic, draw together on a shared canvas in real time, and publish their finished piece to a community feed.

RoleSolo Developer
StackNext.js · Appwrite · Cloudflare · OpenAI
AuthCustom-built + TOTP
DeployVercel
Problem

Drawing is more fun
together

Solo drawing tools are everywhere. But collaborative drawing, with real people, on a shared canvas, with a creative constraint, is a fundamentally different experience that very few platforms get right.

Most multiplayer canvas tools are either too complex (Figma-style whiteboards) or too shallow (one-off doodle apps with no social layer). There's a gap between the two.

Drawgether fills that gap: a game-like drawing session that ends with a real artifact, published to a profile and a community feed. Part drawing game, part social network.

Collaborative drawing requires low-latency sync without complex infrastructure overhead

Shared canvas state needs to stay consistent across reconnects and undo actions

A social layer requires proper auth, profiles, and a scalable feed architecture

AI topic generation needs to feel integrated, not bolted on

Goals

Built to be production-grade

01

Ship a full multiplayer canvas with real-time sync and collaborative undo

02

Build a complete custom auth system without relying on third-party providers

03

Integrate AI topic generation as a first-class part of the game loop

04

Implement a social layer: profiles, a community feed, and auto-published drawings

Tech Decisions

Every choice deliberate

Drawgether originally ran on Supabase Realtime's channel model, one channel per room. Migrating to a `RoomDO` Durable Object per room code kept the same natural per-room scoping, but moved connection state, reconnection, and message relay onto infrastructure the app owns outright, with no client-side filtering by room ID needed.

The Durable Object runs on Cloudflare's hibernation API (`acceptWebSocket`), so idle connections don't keep it billed as active. Stroke segments are buffered client-side and flushed as one batched message roughly every 40ms, keeping message rates low even on the free Cloudflare plan.

One Durable Object per room acts as a websocket relay, scoped naturally per room, with no separate realtime infrastructure to run.

Challenges

Problems worth solving

Undo in a solo drawing app is trivial, maintain a local history stack and pop it. In a multiplayer canvas, undo is a shared operation: when one user undoes a stroke, all clients need to reflect the same state.

The fix was broadcasting a start_drawing event the moment a user begins a stroke, which triggers all connected clients to simultaneously push a history snapshot. This keeps the history stacks in sync across clients, so undo operations are deterministic regardless of who triggers them.

canvas-sync.ts
01// sender
02pushHistory();
03roomSocket.send({ type: "start_drawing" });
04
05// receiver
06roomSocket.on("start_drawing", () => {
07 pushHistory();
08});

Undo needed to be deterministic for all participants, not just the user who triggered it.

Retrospective

What I'd do differently

01 / Architecture

Design the state machine first

The room state machine (WAITING → STARTING → ACTIVE → FINISHED) evolved organically, which caused rework. Defining all states, transitions, and edge cases upfront would have avoided several bugs during the timer and undo sync work.

02 / Auth

Start with custom auth

I spent time evaluating NextAuth before deciding to build from scratch. For a project with custom session requirements, the decision to go custom was always correct. I just reached it later than I should have.

03 / Testing

Test realtime flows with multiple clients early

Several bugs, including the undo desync and the timer offset, only appeared when two real clients were connected simultaneously. A simple two-client test harness from the start would have caught these earlier.

Results

Shipped

Multiplayer canvas with real-time stroke sync and collaborative undo via a Cloudflare Durable Object per room

Room state machine with server-authoritative timer and AI-generated topic reveal

Complete custom auth: registration, email verification, TOTP, forgot password, role-based permissions

Social layer: profile pages, community feed with cursor-based infinite scroll, auto-published drawings

Zero-downtime migration off Supabase to Appwrite (data + storage) and Cloudflare Durable Objects (realtime), with a dual-backend flag kept as an instant rollback path