All work

Case study

Photo Games

Party games made from a group's own photos, specced by me and built task by task by an autonomous agent pipeline.

Role
Product owner and spec author; Claude agents wrote the code
Stack
TypeScript, React, Node, Socket.IO, SQLite, Gemini, Fly.io
Timeline
September 2026, about a week
The Photo Games TV lobby: the room code KVBR in big coloured magnet letters, a scan-to-join QR code, a photo countdown, and name tags for four players.

The problem

Party games out of the photos you already took

The bet in the spec: photos a group took themselves are the one kind of personal content that has ever scaled in party games, and nobody had built the games around them. A laptop or TV shows a room code, players join on their phones and add a few camera-roll photos, and that pool feeds three games: Caption, Geo, and Recreate.

It's the successor to Wing Night, and a test of a different way of building. I spent a planning session turning the idea into a spec with dated decisions, then let an autonomous pipeline build it one task at a time.

Architecture

One machine, with seams to swap later

A single Node process serves the web app and holds every live room in memory, writing each change through to SQLite so a restart mid-round picks up where it left off.

Photo Games architecturePlayers' phones send game actions and photo uploads to a single room server on Fly.io and receive their own scoped view. The server sends the TV display a view with no secrets, writes every change through to SQLite and a photo volume, sends Recreate image edits to Gemini two at a time, and sends host login links through Resend.TV displayany browserPhonesplayers and hostRoom serverNode + Socket.IOSQLite + photosFly volumeGeminiRecreate imagesResendhost login links
  • Every socket event passes a payload guard, then role and token auth, then the game's pure reducer. Each game exposes host, player, and display views, and leak tests serialize them to prove the display never carries a secret.
  • Timers are absolute deadlines, and rooms rehydrate from their SQLite snapshot, so a restarted server picks up mid-round.
  • Image edits go through a queue: two jobs at a time, a 90-second timeout, one run per job, results cached. Outside production without a key, it never calls Gemini.

Hard decisions

Three calls that shaped it

  1. Pick teams on cost, then reverse after a playtest

    What I chose
    Per-player renders would have cost about $0.80 per target and blown the night budget, so Recreate started with one prompt and one render per team, keeping a four-target game near $1.10. After the first playtest I changed my mind: the game I wanted was my prompt, my picture. Individuals became the default, and teams became a mode that runs through the same reducer.
    What it cost
    A higher cost per night, and a 12-player round now queues 12 renders behind a cap of two. Because the cost math and the queue were already in place, the reversal was one added task.
  2. One machine and SQLite, behind interfaces

    What I chose
    I weighed an object store, managed Postgres, and Cloudflare Durable Objects, which cost nothing when idle but meant rewriting the socket layer. I chose one Fly machine with SQLite and a local volume at about $3.50 a month, with the room, snapshot, and photo stores behind interfaces so Postgres or R2 can come later.
    What it cost
    A single point of failure, backed up only by Fly's daily volume snapshots.
  3. Test the protocol with bots, keep browsers few

    What I chose
    The main gate is a scripted bot night that plays whole games across several rooms over real sockets in seconds, next to pure game tests and leak tests. Browsers only prove rendering, so Playwright is held to four specs.
    What it cost
    Thin real-browser coverage, accepted on purpose and written into the spec.

How it was verified

A pipeline that only trusted exit codes

Each task had its own verify command. The orchestrator sent one fresh builder agent per task and marked it green only when that command exited 0, never on the agent's say-so.

unit and game tests, all passing
690
tasks green by exit code
23 of 23
to build the first 22 tasks
16 h
  • The gate runs lint, typecheck, the tests, a bot night with 12 bots per room, and a bots smoke test. It passed again at the audited commit.
  • One failure is on the record: a task first failed on a load-sensitive socket test and passed on its second attempt.
  • The bot night restarts the server mid-round and checks that every deadline survives.
  • The first Fly image would have crashed at boot on a missing content file. Now a test derives every content path from the server source, and the gate parses the Dockerfile and fly.toml.
  • Four Playwright specs, including a full Caption round across a host, three players, and the display.

Screens

What it looks like

Captured from the running app for this page.

The TV revealing a caption pair: a taped Polaroid captioned 'Me explaining my weekend plans', labelled from the house pack, with the author hidden until after the vote.
Caption on the TV: the author stays hidden until the vote.
The TV after a Caption round: a 'Best pair' rosette on Dev's winning caption, with the other pairs ranked by votes and their authors revealed.
Round results, with authors revealed.
A player's phone after joining: 'Add 3 photos from tonight', three empty photo slots, and a note that photos are deleted when the room ends.
Joining: add three photos, with the deletion promise up front.
A phone in Caption's write phase: a dealt photo in a Polaroid frame with the player's caption previewed on it, a character counter, a Report button, and a timer.
Writing a caption on a dealt photo.
A phone in the vote phase: 'Vote for the best pair', listing two photo-and-caption pairs, with the player's own pair left out.
Voting, with your own pair left out.

Where it stands

Built in a week, offline for now

Caption, Geo, and Recreate all work, with host accounts, a host photo library, a house pack so a room with no uploads can still play, a report flow, and a later design pass that gave it a paper-and-magnets look. It went live on Fly on September 24. It's 70 commits, 61 of them co-authored by Claude.

It's offline today: the Fly trial ended, and the site stays down until billing is added. An open code review has since found a crash bug and two memory problems to fix before it goes back up. The house pack is placeholder art, the terms are drafts, the name is a working title, and the repo is private.