redact is the boundary
redact(ctx, state, viewerId, revealed): unknownThe model-specific view one player is allowed to see.
Redact an opponent’s secret information here, and reveal it only when revealed is true — which happens when the session is terminal.
A fully-open game returns the same view to everyone. A visible board needs no redaction, and saying so is one line.
The server holds the state no client can be trusted with
That state lives for the lifetime of a session, the pattern helpers manage the collect-then-reveal around it, and the reference has the signature.
That is the whole premise. The authoritative state lives in the Durable Object; what reaches a player is whatever redact produced for them.
GET /multiplayer/sessions/:id returns your redacted view. Opponents’ hidden state stays hidden, and there is no route that returns the raw state.
Atomic simultaneous resolution
This is the genuinely novel piece, and it is more than a visibility filter.
A trusted server collects every hidden submission and resolves them together — the same trust assumption a wager rests on.
Not: show nobody until both arrive, then compute. The resolution itself is one transition — there is no instant at which one submission is committed and the other is not, and therefore no instant at which a leak could give one player the other’s move with their own still changeable.
The simultaneous helper owns that lifecycle so a game does not re-implement it.
redact must be pure
No database, no clock, no randomness.
Drawing from the random stream in redact would advance the cursor on a read — so two players fetching their views would produce different game states, and a replay would diverge from what actually happened.
Randomness belongs in init and apply, the transitions the object commits.
What revealed is for
A terminal session has nothing left to protect.
So the same function serves live play and the post-game reveal, with one boolean deciding which. That keeps the reveal logic beside the redaction logic, where the two cannot drift apart — rather than in a second function somebody forgets to update when a field is added.
The session’s random seed is revealed on the same event, for the same reason.
An honest comparison
Colyseus ships a better hidden-state primitive than this does — its StateView is more expressive than one redaction function.
Colyseus does not run on Cloudflare: it needs an always-on Node or Bun process holding room state in memory, scaled by a matchmaker and a load balancer. If you already run one, or want what it does best, use it.
What this offers is narrow and specific: if you are already on Cloudflare, the sessions wire into your auth, your D1, your leaderboard and your environments, and you operate nothing.
Where hidden state ends and a room begins
This is not rooms, not chat, not presence, not broadcast.
Cloudflare acquired PartyKit and still ships and maintains PartyServer — room routing, lifecycle hooks, a hibernation-uniform API, broadcast. If you want a room, use PartyServer or a raw Durable Object.
This capability does not rebuild that layer, and does not compete with it.