Life drawing sessions run on time. The tutor sets a pose — five minutes, ten minutes, twenty — and everyone in the room draws until the time is called. If you're running the session from a laptop at the front while twenty people are scattered across a church hall with their easels, keeping everyone synchronised is an underrated logistics problem.
The old solution was a kitchen timer. Or shouting.
The problem with kitchen timers
I run lifedrawing.art out of Hackney. We regularly have 15–25 attendees across a room that isn't always acoustically friendly. The tutor calls time, people at the back don't hear, someone loses count, someone else loses their nerve and stops too early. The rhythm of a session depends on everyone knowing exactly how much time is left.
The obvious fix is a visible display — a countdown on a screen at the front. Except then you need a dedicated screen at the front, someone to manage it, and it only solves the tutor's view problem not the attendees' individual awareness.
The WebSocket approach
A shared countdown is a solved problem in distributed systems. You emit a tick, everyone listening receives it. WebSockets are built exactly for this.
The architecture is deliberately simple:
- Server: a lightweight Node.js process with a WebSocket server. It holds the single source of truth — the current pose duration, elapsed time, and run/pause state.
- Client: a Progressive Web App. Any device that navigates to the URL gets connected. The UI is a fullscreen countdown with large accessible numerals.
- Sync protocol: when the tutor starts a pose, the server broadcasts a
STARTevent with the target duration and a server timestamp. Clients compute elapsed time locally from that timestamp — no polling, no drift.
The tutor opens the same URL on their phone. They tap Start. Every phone in the room counts down together.
Why a PWA?
No app install. No account. No App Store approval cycle.
I give attendees the URL at the start of the session. They bookmark it if they like. It works offline — the countdown runs in the client once started, so no server is needed until the next pose begins. On iOS it installs as a home screen app if they want it there.
The Web App Manifest is six lines. The Service Worker caches the shell. The whole thing is smaller than a photograph.
The tricky part: reconnection
The real engineering work was handling reconnection gracefully. Phones go to sleep. Screens lock. Someone's battery dies and comes back. When a client reconnects mid-pose it needs to catch up to the current state immediately without disrupting the room.
The server always holds the canonical state. On CONNECT it sends the current timer state — duration, start timestamp, paused-at timestamp if paused. The client reconstructs the correct elapsed time from those values. No sync message required. No special handshake. The client does the maths.
const elapsed = Date.now() - startTimestamp - totalPausedMs;
const remaining = duration - elapsed;
That's the entire reconnection strategy.
What I'd do differently
The current implementation uses a single shared room — everyone who visits the URL joins the same session. That worked for one venue. For multiple simultaneous sessions (we occasionally run parallel workshops) I'd add room codes: Nanoid for the room key, a Map on the server, a hash in the URL.
I'd also add a proper accessibility mode — a pulsing background colour as the timer approaches zero, for anyone at the back who can't read the numerals at distance.
The result
It replaced the kitchen timer on the first session I deployed it. No explanation required. The tutor taps start, everyone's phone counts down. When it reaches zero a gentle chime plays — the same AudioContext tone on every device, triggered by the broadcast event, not the client's own timer, so they all sound within milliseconds of each other.
Twenty artists, one shared beat. That's the whole point.
The stopwatch is deployed at lifedrawing.art. Source code available on request.