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.

01

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.

02

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.

01

Scan

Guest scans the QR code shown on the big screen.

02

Name

Types a name — nothing else to fill in.

03

Car

Picks a car from the live catalog.

04

Drive

Steers with the phone already in their pocket.

no app · no account · just the browser

From scan to steering wheel — no app, no account.

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.

03

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.

30Hz tick / 20Hz snapshot
Irregular 33/33/67ms broadcasts — visible stutter on the big screen.
60Hz tick / 20Hz snapshot
A perfectly uniform 50ms cadence.
Moving from a 30/20Hz split to an exact 60/20Hz multiple fixed the stutter.

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.

net/httpWebSocketno framework

The server is the single source of truth — every client only renders what it's told.

One authoritative Go server, three synchronized client surfaces.

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 appAll three surfaces are routes in one app — big screen, joystick, controller.

Next.js 16React 19crashout.dizenz.com
wss:// + REST

AWS · us-east-2

Caddy

Terminates TLS, renews its own Let's Encrypt certs, routes to the healthy slot.

One EC2 instance

Blue slotGreen slot

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.

Two providers: the frontend on Vercel, the backend and its database on AWS.

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.

Reconnects pin to the slot a client started onA resume token rejoins the exact car mid-race
Blue/green deploy slots let a race in progress finish before its slot goes away.
04

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

05

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.

lint
typecheck
tests
build