Skip to main content
This tutorial assumes you finished tic-tac-toe with gamekit and have a working @my/tic-tac-toe-game package. Here we host the same game with @openturn/server, connect to it from a React app with <OpenturnProvider cloud> + useRoom, and deploy it to openturn cloud with openturn deploy. Reference code: examples/hosted-multiplayer/tic-tac-toe-multiplayer.

1. Switch the app metadata to multiplayer

Take the app/app/openturn.ts from the previous tutorial and update it:
runtime: "multiplayer" tells openturn build to emit a Cloudflare Worker bundle alongside the client. gameKey is the stable identifier used when deploying; schemaVersion lets you force-reset rooms when you break save compatibility. app/app/game.ts stays the same. The same game and match values drive both local and hosted play.

2. Wrap the page in <OpenturnProvider> and read useRoom

Replace the local-only experience with a lobby-aware one:
useRoom() returns a phase-aware state object:
  • phase is one of idle | missing_backend | connecting | lobby | transitioning | game | closed | error.
  • lobby is a LobbyView while players are still joining.
  • game is a HostedMatchState once the lobby hands off to the match.
  • inviteURL is a sharable link that puts a new player in the same room.
You do not pass a playerID to dispatch: the server already knows who you are from your room token.

3. Run the local hosted stack

The CLI can run your game with a local Bun-backed server, no cloud needed:
(From the tic-tac-toe-multiplayer package root. app is the project directory containing app/game.ts, app/page.tsx, app/openturn.ts.) The CLI prints a play URL. Open it in two browser windows. Each window is assigned an anonymous user; the first window creates a room, shares the invite URL, the second window joins the lobby, and both are routed into the hosted match. Moves are dispatched to the server over WebSocket; the server validates them with the same authoritative ticTacToe reducer, then broadcasts batches back to each client’s player view. Under the hood: @openturn/cli runs a Bun HTTP server backed by SQLite. It uses @openturn/server’s createRoomRuntime plus LobbyRuntime to manage rooms, and @better-auth to issue anonymous session tokens. The dev shell injects the same #openturn-bridge=… URL fragment that openturn-cloud injects in production, so the React app’s <OpenturnProvider> connects through @openturn/bridge without any code change between dev and prod.

4. Deploy to openturn cloud

When you are happy locally, build and ship:
openturn build writes a bundle to .openturn/deploy/ containing:
  • index.html + client JS and CSS.
  • A server script with a per-room namespace (GameRoom).
  • An openturn.manifest.json with gameKey, schemaVersion, a SHA256 digest, and runtime bindings.
openturn deploy uploads the bundle to the openturn cloud control plane, which provisions the per-room backends and returns a play URL. That play URL embeds a backend fragment that @openturn/bridge decodes to find the WebSocket endpoint. See how-to: deploy to openturn cloud for flags and troubleshooting.

What you learned

  • A single game definition drives local and hosted play unchanged. Only the React hook and the metadata differ.
  • The hosted stack is lobby → room. useRoom() exposes both phases.
  • openturn dev gives you production-shaped hosted play on localhost with no configuration.
  • openturn build + openturn deploy ship to Cloudflare Durable Objects.

What to do next