← Back to Work

Schema-first protocol design across a game engine and a native mobile app

Couch Royale

A local multiplayer party-game platform: a Godot host app renders the game on the TV, phones are the controllers, and one wire protocol keeps them in sync.

Role
Sole engineer, designer, and operator
Year
2026
Status
In Development
  • Godot 4.7 / GDScript (host app)
  • Capacitor / React / TypeScript (controller app)
  • WebSocket (host-authoritative game state)
  • Python / FastAPI
  • PostgreSQL (Neon prod, RDS staging)
  • AWS CDK / EC2 + ALB

158 backend + 66 website + 32 controller + 23 Godot host

Automated tests passing across four runtimes

2026-07-04 · CI on main (GitHub Actions test.yml run 28696056405: pytest, vitest x2, Godot headless)

8/8 valid, 6/6 rejected

Wire-protocol conformance (schema examples valid, malformed payloads rejected)

2026-04-29 · ajv-cli schema validation, protocol/v1 spec, PR #29

Challenge

Couch Royale splits one game across two programs: a Godot desktop app renders it on the TV, and a native Capacitor app turns each player’s phone into a controller. They use different languages and runtimes over local WiFi, and every message must match or the game desyncs. Browser controllers were tried first and ruled out (tab switching, gesture conflicts, pinch zoom hijacking button presses); going native raised the wire-format stakes.

Process

The protocol is the contract. protocol/v1/messages.json is a canonical JSON Schema (draft 2020-12) covering every host-controller WebSocket message variant, plus the QR-code pairing payload that starts a session. Godot (GDScript) and the controller (TypeScript) hand-sync their type bindings to it, and ajv-cli validates a full set of example payloads in CI, so drift fails the build, not someone’s TV at 2am. Six deliberately malformed payloads I sent the validator before the schema shipped (wrong version, bad token shape, an extra envelope field, and others) were all rejected.

Game state is host-authoritative: Godot is the only source of truth, controllers are pure input devices, and pairing uses session-scoped tokens, not long-lived credentials. A phone that sleeps mid-round keeps its slot and rejoins the same match; ambiguous reconnect claims are rejected, not guessed at. Staging is API-only (branch pushes deploy the FastAPI catalog backend), because a living-room WebSocket session has no meaningful staging equivalent. One CI gate covers all four runtimes: pytest (backend), vitest (controller app and website), headless Godot tests plus gdlint (host), ajv (protocol), and a CDK synth.

Result

The website is deployed at couchroyale.com, the wire protocol has passed automated positive and negative tests since April 2026, and the full suite is green on all four runtimes. Word Pop, the launch game, is built, playable, and in polish. It is pre-launch, with no customers. Business model: subscription-only, ad-free, NYT Games style, with a free tier gated by library size and player count instead of features. Lesson: a cross-runtime real-time system needs a versioned message format validated in CI, even with hand-written bindings.