Under the hood

This page is for the curious. The repository and its README are the source of truth.

Local first, by design

The host phone opens a WebSocket server and advertises the room over mDNS as _whotook._tcp. The QR code contains an invite URL with the room ID, a host ID, the room secret and the LAN endpoints in the URL fragment. Joiners race every endpoint for 800 milliseconds and keep the first socket that answers.

Nothing needs an account or a central server on this path. If the Wi-Fi has no internet, the match still works.

Online fallback

When no LAN endpoint answers, the player connects over WebRTC. The phones exchange offers, answers and ICE candidates through a small Cloudflare Worker. Each room is a SQLite Durable Object that stores only the room token and the live sockets.

STUN is free and unlimited. TURN is optional and only signs short-lived relay credentials when the Cloudflare Realtime key is configured. The Worker caps relayed rooms per network and across the day so the free tier cannot be drained by accident.

Default relay budget: 5 rooms per IP per UTC day, 250 rooms per day across the whole Worker. That keeps even a worst-case 12-player, 25-round room near 760 GB per month, inside the 1 TB free Realtime tier.

The photo pipeline

  • Photos are read through photo_manager, resized to 1080 pixels and re-encoded as JPEG at quality 75 with EXIF removed.
  • The app splits a photo into 16 KB chunks, hashes the whole frame with SHA-256, and verifies the hash after reassembly.
  • The host keeps approved frames in memory. A round broadcasts one frame to every player, then reveals the owner and the scores.
  • Screenshot detection uses the Android 14 screen capture callback, a scoped MediaStore observer on older Android, and UIApplication.userDidTakeScreenshotNotification on iOS.

Party modes

The host picks Classic, Caption This, Time Traveler or Mixed Party in the setup flow, before the room opens. Captions, anonymous votes, timeline guesses and the actual photo date travel over the encrypted game channel. The signaling Worker only sees room sockets, never the mode or the answers.

Time Traveler reads the gallery capture date on the owner's phone, rejects photos without a usable date, and strips EXIF before the photo is sent. Only the year and month appear in the reveal. Mixed Party shuffles the three modes in a repeating cycle.

Match state

The host owns the engine: a balanced round robin deck, a speed and streak score, one secret photo per player, and an optional five-round extension with a 10-second approval window. Every client mirrors the state it receives, so a reconnect can land on the current screen.

Run the signaling Worker yourself

cloudflare-signaling
git clone https://github.com/warreth/WhoTook.git
cd WhoTook/cloudflare-signaling
npm install
npx wrangler secret put IP_HASH_SALT
npx wrangler deploy

TURN is optional. Without TURN_TOKEN_ID and TURN_API_TOKEN the Worker serves STUN only. The app can point at your Worker and your own TURN server under Settings, then Self-hosted relay.

Links

Back to WhoTook