Crashout is a live, phone-controlled racing game we built for events: a big screen at the venue, a host running the show from a tablet, and guests steering with the phones already in their pockets. It's live today at crashout.dizenz.com.
The idea came from a simple constraint: group entertainment at a real venue has none of the luxuries a normal multiplayer game assumes. You can't ask twenty guests to install an app before the party starts. You can't count on fast, stable wifi. And whoever is running the show needs to start, reset, and hand off race after race without ever touching the TV.
This case study covers how we designed around that constraint — the product decisions, and the engineering underneath them.
The Challenge
Most multiplayer games assume a captive, technical audience: an app already installed, a stable connection, players who know the controls. None of that holds at a corporate party, a brand activation, or a night out with friends. The onboarding has to work for a guest who has never seen the game before, on whatever phone they happen to be holding, over whatever wifi the venue provides.
On top of that, one host has to be able to run the event end to end — start a race, reset it, start the next one — from a tablet, without ever walking up to the TV. And every screen in the room, from the big display to each player's phone, has to agree on exactly what's happening at every moment, or the whole thing falls apart in front of a live crowd.
The Approach
We settled on the simplest possible onboarding: a QR code on the big screen, a browser tab on the player's own phone, no app, no account. Scan, type a name, pick a car, start driving. That single decision shaped almost everything else about the product.
Scan
Guest scans the QR code shown on the big screen.
Name
Types a name — nothing else to fill in.
Car
Picks a car from the live catalog.
Drive
Steers with the phone already in their pocket.
no app · no account · just the browser
The other core decision was to make the server the single source of truth for the entire game. No client — not the big screen, not a player's phone, not the host's controller — is ever allowed to decide what happened in the race. They only render what the server tells them. That's what makes it possible for a room full of strangers' phones, over unpredictable wifi, to all agree on the same race.
One Authoritative Server, Three Synchronized Surfaces
The backend is a Go server built on the standard library's HTTP router and a lightweight WebSocket library, with no web framework in between. Every event gets its own room, and every room owns its own simulation goroutine and inbox channel — rooms never share state or lock against each other, which keeps a game with dozens of simultaneous events simple to reason about.
The simulation runs at a 60Hz tick internally, but only broadcasts a snapshot to clients 20 times a second, with the client interpolating between snapshots for smooth motion. That ratio isn't arbitrary — we originally ran the tick and snapshot rates at a 30/20 split, which produced an irregular 33/33/67 millisecond broadcast pattern and showed up as visible stutter on the big screen. Moving to a 60/20 split, an exact multiple, gives a perfectly uniform 50 millisecond cadence and fixed it. It's a good example of the kind of detail that only shows up once you're watching a race play out on an actual TV.
Three surfaces connect to that same server: the big screen renders the race and lobby QR code on canvas, the joystick page turns a player's phone into steering and a horn button, and the controller gives the host start, reset, and settings. A fourth mode, a single-device demo race, reuses the exact same protocol — it just opens two WebSocket connections from one phone instead of inventing a new client role.
Big screen
Renders the race and lobby QR code on canvas.
Player's phone
Joystick page: steering and a horn button.
Host's tablet
Controller: start, reset, and settings.
Go server
One room per event, each owning its own simulation goroutine and inbox channel — no shared state, no locking.
The server is the single source of truth — every client only renders what it's told.
All of that runs across two providers. The frontend is on Vercel — every one of the three surfaces is just a route in the same Next.js app, reaching the backend over absolute WebSocket and REST origins baked in at build time. The backend is deliberately unglamorous: a single EC2 instance in us-east-2, provisioned by Terraform. No containers, no orchestrator, no load balancer.
Caddy sits in front of it, terminating TLS for every WebSocket connection with certificates it provisions and renews itself from Let's Encrypt — the backend's internal ports are never publicly reachable. Anything that has to outlive a process lives in PostgreSQL on a separate box, reached over SSL, with migrations running at boot. Deploys are the only thing GitHub Actions does: lint, typecheck, tests, and build all run locally before a push ever happens.
Vercel
Next.js app — All three surfaces are routes in one app — big screen, joystick, controller.
AWS · us-east-2
Caddy
Terminates TLS, renews its own Let's Encrypt certs, routes to the healthy slot.
One EC2 instance
PostgreSQL
A separate box, SSL only. Both slots share one database — the only state a deploy hands over.
Only Caddy is exposed — the slot ports are never reachable from the internet.
Because Crashout runs at live events, a deploy can never be allowed to interrupt a race in progress. We run production behind blue/green deploy slots on the same box, and a shutdown signal tells the old slot to drain — finish whatever races are running, refuse new ones — for up to three hours rather than dropping connections immediately. Clients pin their reconnects to the slot they started on, and a resume token lets a player whose phone drops mid-race rejoin the exact car they were driving.
New slot · Active
Accepts every new race.
Old slot · Draining, up to 3h
Finishes races already running, refuses new ones.
What We Shipped
Beyond the core race loop, the product grew into a full event-operations tool:
Content catalog
15 vehicles across 5 model lines, 8 obstacle types, and 4 selectable race backgrounds
Configurable rooms
From 2 up to 40 players, with elimination racing and a 60-second time limit
Three languages, live
English, Spanish, and Arabic, switchable mid-event by the host — including full right-to-left layout support
Two control schemes
On-screen buttons or an analog thumbstick, chosen per player
Public leaderboards
Read-only leaderboards for every event, shareable after the race is over
Admin panel
Password-gated: event creation, security codes, a live vehicle and background catalog, and an authentication audit log
Host support
Escalates straight to a pre-filled WhatsApp message with the event code and race state attached
Results
Crashout has been in continuous, active development for about five and a half months, and we've built it to the standard of production software, not a party-game prototype:
478
commits since the first scaffold (Feb 2026)
~17,000
lines of Go on the backend
~23,000
lines of TypeScript on the frontend
400+
Go tests
~1,900
JS/TS tests
46
Playwright E2E specs
A full pre-push gate
Runs locally before code ever reaches CI.




