Game API best practices
How to use the Game API so the server owns gameplay truth. This page is the contract between a game client (Unreal, CrowdyJS, CrowdyCPP, or a custom engine) and the server code that decides outcomes, which runs on ck-exec.
Hubs are authoritative
Put authoritative gameplay logic and the state it decides in ck-exec
hubs. A hub keeps its state in memory, runs one handler at a time, and is
snapshotted on an interval you choose (5 to 60 seconds), when it stops, and
whenever it asks (ctx.persist_now(), for a purchase or a checkpoint that
should not wait). Clients render and interpolate; they do not keep a second
source of truth for competitive or persistent results.
A type's seed_b64 in the manifest is the state
every new instance spawns with, and the root hub's seed is the app's starting
state.
Endpoints: direct, validated state changes
A hub's endpoints are the changes a client asks for by name. Clients call them
through the SDKs' exec connection; see
connect from a game.
Use an endpoint when the caller already knows what to change:
- Deal damage or heal
- Capture a camp
- Change ownership
- Update a player's team assignment
Prefer one intent-level endpoint (deal_damage, capture_camp,
assign_team) over a client composing several writes. A hub runs one handler
at a time, so an endpoint that updates health and then sets is_dead from
the new value changes both before any other call reaches the hub.
Check who may call it in the handler. The platform sets the caller, so a
handler can trust call.player() for authorization, and call.developer()
guards an endpoint meant for your own tools. Only types marked client in the
manifest take calls from players at all. Platform rules such as tier
features and grid permissions are read in the handler too; see
world and platform data.
Publish what changed. A hub publishes on its topics (ctx.publish) and
clients subscribe to them. Topics are not a log: a push published while a
client was disconnected is not replayed, so read the state you display again
when the connection comes back.
Clients request; they do not decide
Unreal (or any client) is responsible for requesting a change and presenting the confirmed result. It should not independently decide the authoritative outcome — not from local prediction alone, not from an elected host, and not from a peer-supplied value.
Prediction and animation are fine. Reconcile them to the hub's reply. If a call is refused, leave the world as the server left it.
Host discovery (gameHost / amIGameHost) is informational. Host-only
rules belong in a hub, which checks its caller itself; the session
starter pack keeps a host and a turn of its
own.
Spokes and calls between hubs: workflow beyond one change
Some requests need server-side work that is not one direct state change:
- Find or create a team
- Search across many objects
- Process many objects
- Coordinate a match reset
- Run dynamic fan-out
Put that work in keyed hubs and spokes. A spoke holds nothing you cannot
lose, its replicas run side by side, and it changes state only by calling the
hub that owns it. Hubs call each other with ctx.call and hear each other
through topics. The root hub is limited to 50 calls a second, so work that
must scale does not live there.
Timers simulate (ctx.timer_every); endpoints referee a caller's intent. See
timers, subscriptions and presence.
One sentence
The client requests a direct change when it knows the target. A hub validates and applies that change. Spokes and calls between hubs handle the wider workflow when the target or process must be discovered or coordinated.
Calling the Game API
- Send an app-scoped token, not the identity session token. Authentication.
- After minting, call
gameClientBootstrapand use the returnedgameApiUrl/gameApiWsUrl. Datacenter routing. - Native UDP:
serverWithLeastClients, wait ~1.5 s, then send. Replication server assignment. - Browsers: GraphQL UDP proxy.
- Spatial permission keys live on access tiers and grids. Permissions.
- Economy-sensitive mutations, such as the marketplace's, should send an
idempotencyKey.