Why Server Authority?
In a real-time card game where two players can tap the same Set within milliseconds of each other, the client cannot be trusted to resolve claims fairly or correctly. Three problems arise if clients self-validate:- Cheating is trivial. A modified client can pre-validate a Set and send the result rather than the raw card IDs, guaranteeing a win on every contested claim.
- Race conditions are unresolvable. Two clients seeing two different “first claim” results with no shared clock to arbitrate creates an unsolvable split-brain.
- State diverges. Without a single broadcaster, clients accumulate subtle differences in board state, scores, or deck order that become corrupted game sessions.
The Core Rule
The Unity client collects the player’s three card selections and passes them to theOnlineMatchController, which fires a match_claim message over the Nakama WebSocket. The Nakama match handler runs SetValidator on the server, updates match state, and broadcasts the new MatchState to all connected clients. The OnlineMatchController receives that state update, translates it into an ApplyServerStateCommand, and the GameSession applies it via GameSession.ApplyServerState(). The UI reacts automatically through the R3 reactive stream.
There is no shortcut path where the client checks first and sends only valid claims. That would still be a form of cheating enablement — an attacker could intercept the send and replay it with different IDs.
Architecture
The diagram below shows every component involved in a single multiplayer claim, from the player’s finger to the state broadcast that reaches all clients. TheGameSession is a mirror in multiplayer — it holds a copy of the server’s state and applies updates atomically. It never computes state independently. Critically, GameSession does not hold a reference to IMultiplayerService directly; the OnlineMatchController acts as the intermediary that bridges the network layer and the game session.
Why Nakama?
The project evaluated several networking options before choosing Nakama. The comparison below captures the reasoning:
The key advantage: Nakama replaces at least four previously separate infrastructure concerns — matchmaking, leaderboards, cloud save, and auth — with one coherent, self-hostable backend. The Heroic Labs managed cloud option reduces DevOps overhead during launch and can be migrated to self-hosted when scale justifies it.
Nakama Components Used
Client-Side Interface Contract
The entire networking layer is hidden behind a single interface defined in the Application assembly. No gameplay or UI code ever imports a Nakama type directly:NakamaMultiplayerService in the Infrastructure assembly implements IMultiplayerService using the Nakama .NET Client SDK. The Application layer depends only on the interface — it can be swapped for a mock during testing without any changes to game logic.
The OnlineMatchController Pattern
GameSession does not hold a direct reference to IMultiplayerService. Instead, the OnlineMatchController (Infrastructure assembly) sits between the two:
GameSessionremains ignorant of networking. It processesIGameCommands regardless of whether they originate from local input, the AI, or the network.OnlineMatchControlleris the single translation point:ServerMessage→IGameCommand. If the message schema changes, onlyOnlineMatchControllerneeds updating.- The pattern makes
GameSessiontrivially unit-testable for the multiplayer path — inject a mockIMultiplayerService, emit messages, assert commands.
The IOnlineGameSession Contract
TheIOnlineGameSession interface, which GameSession implements when in multiplayer mode, is deliberately designed with no ValidateSet method:
ValidateSet is not an oversight — it is the architecture’s structural enforcement of server authority. A developer cannot accidentally add client-side validation because the interface does not expose the affordance.
Risks and Mitigations
Implementation Checklist
-
NakamaMultiplayerServiceimplementsIMultiplayerServicein the Infrastructure assembly — no Nakama SDK types leak into Application or Domain assemblies -
OnlineMatchControlleris the sole consumer ofIMultiplayerService.Messages; it translates eachServerMessageinto anApplyServerStateCommandforGameSession -
GameSessiondoes NOT hold a direct reference toIMultiplayerService— onlyOnlineMatchControllerdoes -
IOnlineGameSessionhas noValidateSetmethod — onlySendClaimandApplyServerState -
ApplyServerState()is the only write path for all multiplayer game state on the client
Common Mistakes
Related Pages
Match Lifecycle
The full flow from matchmaking queue to post-match results, including the in-match play loop.
Sync & Reconnect
How the 20 Hz server tick, full-state broadcasts, and the 30-second reconnect window work.
Anti-Cheat
Rate limiting, server-side data ownership, and why the client cannot influence outcomes.
Architecture Layers
How the Domain, Application, Infrastructure, and Presentation assemblies enforce dependency boundaries.