Skip to main content
Worker-safe. The authoritative side of a hosted openturn match. Used by openturn dev (via @openturn/cli) and by the generated Cloudflare Worker bundle.

Install

Deployment definition

GameDeployment<TGame>

The minimum information needed to host a game.

defineGameDeployment(deployment)

Type-safe factory for a deployment. Errors at the type level if TGame’s state or views are not protocol-compatible (not JSON).
For cloud authoring, you do not need to call defineGameDeployment yourself. openturn build generates the deployment — and the Cloudflare Worker that hosts it — directly from your app/game.ts and app/openturn.ts files. Reach for defineGameDeployment only when you need a hand-written Worker entry (custom bindings, advanced routing), when you are embedding @openturn/server into your own Bun/Node host, or when you are writing a test that exercises createRoomRuntime directly.

loadGameDeployment(moduleValue)

Extract a deployment from a module value (handles default, deployment, and bare exports). Used by the CLI to load user-authored deployment files.

Room runtime

createRoomRuntime(options)

Build a RoomRuntime<TGame> for one room. The runtime accepts ProtocolClientMessages and produces RoomServerMessages.

RoomRuntime<TGame>

RoomRuntimeEnvelope<TGame>

{ playerID, message }: a message addressed to a specific seat.

RoomRuntimeState<TGame>

RoomRuntimeOptions<TGame>

The input to createRoomRuntime.

InitialSavedSnapshot<TGame>

{ branch?, initialNow, match, revision, seed, snapshot }. Feeds a hot-started room (decoded from a save envelope) directly into createRoomRuntime.

Profiles

Server-side settlement of per-player persistent state. See concepts: persistent profiles.

onSettle: RoomSettleHandler<TGame>

Invoked exactly once when the match reaches a terminal result. Receives the pure delta computed by the game’s declared profile.commit, already restricted to seated players.
The handler is idempotent at the caller’s boundary: a retry (DO crash, settle retry) re-computes the same delta from the checkpoint, so your storage layer should dedupe on (roomID, userID). Games that don’t declare profile call the handler with delta: {} — safe to skip.

onActionProfileCommit: RoomActionProfileCommitHandler<TGame>

Invoked once per in-match transition that emitted a profile delta (mid-match unlocks, incremental rewards). The host is responsible for dedupe on (roomID, actionID) — failures do not block gameplay.

onSaveRequest: RoomSaveHandler<TGame>

Invoked when a player requests a save envelope. Return { saveID, downloadURL? } to tell the client where the artifact lives.

RoomActionProfileCommitInput<TGame>

Input to onActionProfileCommit.

How openturn-cloud wires this

The generated game-worker’s GameRoom Durable Object reads a cloudAPIBase URL that’s threaded in via the x-openturn-cloud-base header on the cloud’s dispatch proxy. When it sees onSettle, it POSTs the delta to ${cloudAPIBase}/api/profiles/commit, signing the body with HMAC-SHA256 using ROOM_TOKEN_SECRET (the same secret already used for room tokens). No new env vars are required. Hydration is the mirror call: at first bootstrap, the DO POSTs to /api/profiles/hydrate with the seated user IDs and merges the returned profile data into MatchInput.profiles before creating the session.

Persistence

RoomPersistence

Pass a persistence adapter when you want rooms to survive runtime restart. @openturn/cli ships a SQLite adapter; the generated Cloudflare Worker uses Durable Object storage.

RoomPersistenceRecord<TGame>

The serialized snapshot of a room.

parseRoomPersistenceRecord(value)

Deserialize and validate a persistence record.

Save envelopes

Opaque, signed blobs that carry the log needed to resume a match elsewhere. Used by onSaveRequest on the server side and by the openturn-cloud replay UI on the client side.

encodeSave(payload, secret, options?) / decodeSave(bytes, secret)

Round-trip between SavedGamePayload and a self-describing encrypted binary envelope. decodeSave throws SaveDecodeError on malformed input; inspect its .code (SaveDecodeErrorCode = "magic" | "version" | "auth" | "key" | "payload") to disambiguate.

EncodeSaveOptions

SaveDecodeError

Error class thrown by decodeSave. Has a code: SaveDecodeErrorCode field for programmatic handling.

deriveSaveKey(saveID, secret)

Derive a stable, unguessable download key for a saveID — use when signing presigned URLs.

SAVE_FORMAT_VERSION

Constant: the version byte embedded in every save envelope. Bumped when the envelope schema breaks.

SavedGamePayload, SavedGameCheckpoint, SavedGameMeta

Types describing the decoded envelope shape.

Room tokens

signRoomToken(claims, secret)

Sign a room token. Returns SignedRoomToken = { claims, token }.

verifyRoomToken(token, secret)

Verify and decode. Returns RoomTokenClaims | null.

RoomTokenClaims

scope: "lobby" is issued during lobby, scope: "game" after seat assignment. The RoomTokenScope type alias is exported for reuse.

SignedRoomToken

signValue(value, secret)

HMAC-SHA256 signer used internally for room tokens; re-exported for server-to-server signing (e.g. DO → openturn-cloud profile commits share this primitive with ROOM_TOKEN_SECRET). Returns a base64url digest.

Lobby

LobbyRuntime

Pure state machine for the pre-game lobby. The Durable Object host and the local dev server both wrap an instance with their own WebSocket/presence plumbing; this class has no knowledge of sockets or persistence.
  • env: LobbyEnv{ hostUserID, capacity, minPlayers, playerIDs, knownBots? }. knownBots is the bot catalog (built via buildKnownBots) and turns on lobby:assign_bot validation.
  • persisted?: LobbyPersistedState — rehydrate from storage; omit for a fresh lobby.
Key methods: apply(userID, userName, message), takeSeat, leaveSeat, setReady, assignBot(hostUserID, seatIndex, botID), clearSeat(hostUserID, seatIndex), start(hostUserID), close(hostUserID), dropUser(userID), pruneToConnected(connectedUserIDs), buildStateMessage(roomID, connectedUserIDs), toPersisted(), playerIDFor(userID), seatIndexFor(userID). See how-to: build a lobby and reference: @openturn/lobby.

Lobby types

SeatRecord is now a discriminated union over kind: "human" | "bot". LobbyStartAssignment carries kind plus botID for bot seats. See reference: @openturn/lobby for the full discriminated shape.

Bot driver in the Durable Object

@openturn/server ships an in-DO BotDriver<TGame> that dispatches bot moves directly through RoomRuntime.handleClientMessage after every state change. This avoids opening a WebSocket-to-self from the DO and is the variant the cloud uses; sidecar processes that connect over the network use createHostedBotSupervisor from @openturn/lobby/supervisor instead.
tick() chains bot-vs-bot moves up to maxChainDepth (default 64) before yielding. isBot(playerID) exposes the seat assignment for the room’s broadcast logic. Cold-start safe: rebuild from lobbyRuntime.toPersisted().seats after DO wake-up. BotDriver accepts a BotRegistryShape rather than the full BotRegistry from @openturn/lobby/registry to dodge the server → lobby → server package cycle — the structural shape on game.bots matches.

Cloudflare Worker factory

createGameWorker(deployment, options?) (from @openturn/server/worker)

Produce a Worker-ready { default: ExportedHandler, GameRoom: DurableObject } from a game deployment.
openturn build generates a Worker entry that calls this for you.

GameWorkerEnv

The required Worker bindings: GAME_ROOM (DurableObjectNamespace) and ROOM_TOKEN_SECRET (string).

GameWorkerOptions

{ idleReapMs?, lobbyTokenTtlSeconds?, gameTokenTtlSeconds? }.

GameWorkerInfoResponse

The shape served at /__info: { deploymentVersion, gameKey, players, schemaVersion, minPlayers, capacity }.

GameWorkerExports

The return type of createGameWorker:

See also