Blog

The server holds what no client can be trusted with

2026-07-23

An isometric illustration of a roulette wheel lying flat in its bowl, with a spoked turret at its center

Two players are in a match. One of them opens the network tab.

Every design decision in turn-based multiplayer follows from that moment. If the client holds the game state, the client can edit it. If the client decides who won, the client wins. If the client rolls the dice, the dice are whatever the client says.

pithy add multiplayer puts the state somewhere a client cannot reach — a Durable Object in your own account. It is the first Durable Object in the kit, and the CLI wires the binding and its class migration tag for you across every environment, which is the fiddly part nobody enjoys doing twice.

What authoritative actually means here

The session lives in the DO. Players submit moves, not outcomes. The DO validates each move against the current state, applies it, and when the game resolves it writes a durable result to your own D1.

So a client can lie about what it wants to do. It cannot lie about what happened.

The result in D1 is the part that outlives the session — a row you can join against your users, your ratings, your ledger. The Durable Object is where the game is played; your database is where it is remembered.

Games are pluggable

The kit does not know what your game is, so the game is a plugin. Three ship as examples, and each one exists to demonstrate a different shape:

gameshape it demonstrates
battlesimultaneous turns — both players commit, then both resolve
connect-nalternating turns on a board (tic-tac-toe, Connect Four)
crapsa wagering table with money on it

Each is built on a reusable pattern helper rather than from scratch, so writing your own means layering on a pattern that already handles turn order, validation and resolution. Register it, and it is available like the built-ins.

It supports N players, not just two. That constraint is one you would have to design around later if it were not there from the start.

The wagering stack

If your game has stakes, the interesting problems move from game logic to money handling, and there are three of them.

Provably-fair dice. A player who loses a wager wants to know the roll was not chosen after they bet. Provable fairness is the standard answer — the server commits to a seed before the roll and reveals it after, so anyone can verify the outcome was fixed in advance.

Persistent tables. Real wagering games are not one round. Players buy in, play several rounds, and cash out — so a table holds a seat between rounds rather than tearing down after each one.

Ledger-settled bets. The money itself is not multiplayer’s job. Bets settle through the ledger capability, which is where holds, atomicity and overdraft-safety already live. Multiplayer decides who won. Ledger decides what that means for a balance.

What it is deliberately not

This is the clearest “use something else” in the kit, and it is worth stating plainly.

It is not rooms, chat, or real-time netcode. No presence in a lobby, no message fan-out, no twenty-times-a-second position sync. If you are building an action game, or anything where the interesting problem is latency rather than authority, Cloudflare’s own PartyServer is the right tool and this is not.

The line is roughly: multiplayer here is for games where a turn is a decision, not a frame.

It is not matchmaking. It gives you an authoritative session once players are in one. How they got there — a room code, an invite, a queue — is a separate capability, for the good reason that finding an opponent and running a match are different problems with different failure modes.

What it needs from you

Sessions bind to an authenticated player, so compose auth. Without it, routes deny rather than open — the same failure direction the rest of the kit takes.

And you write the game. The kit gives you a place to hold state that clients cannot touch, a pattern for turns, and a durable result. What constitutes a legal move in your game is domain logic, and it should be, because that is the part that makes it yours.

Why a Durable Object and not a table

Because a game in progress is not a query workload. It is a small piece of hot, contended state with exactly one writer, which is the shape a Durable Object exists for.

Trying to run it in a database means every move is a read-modify-write race against every other move, and you spend your time writing optimistic locking instead of writing your game. Trying to run it in a client means the network tab, above.