the pvp duel system: server-authoritative state

Building the biggest single system in the game: dueling a friend asynchronously, backed by a server-authoritative engine and a compare-and-set turn model.

When we set out to build the PvP duel system—where you can challenge a friend asynchronously—we knew it would be the biggest single system in the game. It required a fundamentally different architecture than the local AI duels. The design phase was so extensive that the architecture documentation pull request (#308) was opened in June and only merged in July, serving as the blueprint for the entire implementation arc.

We ended up writing seven Architectural Decision Records (ADRs 0014 through 0020) to capture the model before writing any UI code. We moved the game logic authority to the server side: the engine runs in the API route, and the game state lives in PostgreSQL. Because we needed to notify players when it was their turn, we used Server-Sent Events (SSE). But instead of broadcasting full state objects, we kept the SSE stream unauthenticated and payload-free—it just serves as a thin wake-up signal telling the client to fetch the latest state via a token-guarded GET request.

Managing state for two different players looking at the same board presented a classic perspective problem. We decided to store exactly one canonical state in the database: the host’s perspective. When the guest requests the state, a pure engine module transforms the canonical state into a viewer perspective, swapping the sides cleanly.

To prevent race conditions during a fast-paced game, every write goes through a single compare-and-set guard. The game service validates the current turn index and phase before appending a turn, replaying duplicate submissions idempotently and rejecting out-of-order moves. We polished the experience with a live opponent typing indicator, a fold-safe mobile layout, and a script to record gameplay for both the host and the guest, closing out the arc.