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
- Site
- couchroyale.com
- 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.