All developer documentation

Room shapes — the four things a game room is for

A game room is one mechanism with four useful shapes. The same browser surface (gem.rooms) and the same server entry point (defineServer) serve all four; what differs is the room template and what the hosting server chooses to do with its time.

Only three of the four need a second player. The fourth is for a game that has no multiplayer at all and still needs something a browser cannot be trusted to do.

This page is the decision. rooms-and-matches.md is the mechanism behind it — the room state machine, the event stream, room args, reconnection — and is what to read once you know which shape you want.

Pick a shape#

Answer in this order. The first yes wins.

  1. Does the server need to run a match at all, or only answer requests from clients who are not playing each other? Only answer requests → authoritative worker.

  2. Does one player decide when a round starts, and with what settings?custom.

  3. Do players arrive and leave continuously, with the world outliving any one of them?hub.

  4. Otherwise a lobby fills and plays one match → match.

The four#

Match#

The ordinary shape. Players are matchmade into a room, the room fills, a match starts, the match ends. One lobby, one match — what most competitive and co-op games want.

The join that brings membership to min_players starts the match, so nobody has to press a button labelled "go". A match is sized by who is in the room the instant it starts, and nothing tops it up afterwards; if you want late joiners, that is allow_join_in_progress, and it is a genuinely different game to write — the server has to place a player mid-match and hand them current state rather than a start-of-match snapshot.

Hub#

A shared world that outlives any one player. Members arrive and leave continuously and the world does not restart around them: a persistent zone, a town, a social lobby players visit between games.

A hub is served or unserved, and match_type decides which:

  • dedicated with allow_join_in_progress: true — a server runs the world, players see each other move, the server is the authority.

  • none — the room starts no match at all. It is a place to be rather than a world to simulate: somewhere players gather, see a roster, agree on something through room args, and leave for a real match. No allocation, no server bundle, no duration ceiling.

Reach for none when nothing needs simulating. A lobby that needs a member list and a settings object does not need a server, and a served hub costs one.

Authoritative worker#

A room whose server runs no match. It accepts no player input, replicates no world state, simulates nothing. It answers requests from members who are not playing each other, and each member's session is unrelated to every other member's.

This is how a browser game reaches an execution context a modified client cannot rewrite. Use it when something must be decided on your behalf rather than the player's, and the platform has no built-in rule for it:

  • Confirming an offline, single-player run really was finished, and to the score claimed.

  • Granting an award whose condition is game state — level reached, boss defeated, run completed under a time — that only your game code can evaluate.

  • Checking a score before it counts.

A game with no multiplayer can need this shape. The room is not a lobby; it is a way to get your own code running somewhere the player does not control.

Two things decide whether you need it at all. See what a worker owes you, and what it does not.

Custom#

A room one player owns, where a group gathers and plays round after round. The owner picks the settings, members see what was picked, the owner starts. When the match ends everyone comes back to the room and does it again, possibly with different settings.

The room outlives each match, which is what separates it from a match room. Players arrive by join code or, if the room is public, from the room browser. What the owner may choose is declared by the template's room-args schema, so a settings menu should render from that schema rather than from a list hard-coded in your client — then the config can add a mode without a new build.

The settings that produce each#

MatchHubWorkerCustom
match_typededicated/p2pdedicated or nonededicateddedicated/p2p
auto_start_hosttruetruetruefalse
min_playersthe match size111
allow_join_in_progressfor backfilltruetruefalse
show_in_browserfalsetruefalsewhen public
auto_close_when_emptytruefalsetruefalse
How a player gets inquickJoinquickJoinquickJoincreate/joinByCode
What starts the matchmin_playersmin_playersfirst jointhe owner

Templates live in your game config under game_room_templates, and are uploaded and promoted per channel like the rest of it.

Two entries in that table do most of the work. auto_start_host: false is what makes a custom room — with it true, the room starts a match the moment membership reaches min_players and the owner never gets to choose anything. allow_join_in_progress is what makes a hub — without it, a player who quick-joins while a match runs starts a new room, and your shared world quietly becomes one world per player.

What the platform decides for you#

Four limits bind every shape, and three of the four shapes exist partly to work around one of them.

Seats belong to the template, not to the room. A create request carrying its own max_players is refused rather than having the field ignored. The template's count is not confined to a lobby-sized number. What is bounded 2–64 is resizing a room that already exists, and that resize is refused below the room's current member count. Choose a served room's seats from what one process can carry.

A match has a maximum duration. max_match_duration_seconds defaults to 21600 — six hours — after which the platform ends the match the way any end happens: members get the ordinary match_ended push and the server is released. Nothing here runs forever. A hub or a worker is recycled, not permanent, and has to be written that way: durable state belongs in player save data or your own store, written as it changes rather than at shutdown, and the client re-enters on match_ended rather than treating it as a crash.

Concurrent matches are capped per player. Starting one allocates real infrastructure. The cap counts only that player's own matches — nothing another player does can refuse you — and it clears when one of yours ends. A shape that gives every player a private room spends this budget and one server per player.

Starting a match is idempotent. Repeated starts on the same room return the same match, behind a shared fence, so an auto_start_host template and a client that also calls start cannot race into two matches.

What a worker owes you, and what it does not#

The worker shape is the one most easily got wrong, in both directions.

It is not the answer to economy arithmetic. Spending a currency and granting an item is a recipe, and a recipe is already evaluated on the platform's side where a modified client cannot change the arithmetic — no server of yours, no allocation, no seat. See inventory-and-catalogs.md. A worker room is for validating game state the platform knows nothing about. A worker that grants after validating is doing both: decide here, then let the grant itself be a recipe or an order.

Its server must check its own roster. A server bundle's credential is scoped to your game and channel, not to its match, and the room roster is not a boundary the platform enforces on it. A bundle can therefore act on any player of your game, including one who never joined its room. That is needed by an ordinary match server writing results — and it means a worker that skips its own membership check is an open grant endpoint wearing a room as decoration. Read the roster and check the requesting player against it before acting; not against an id the client sent.

It should hold nothing between joins. Each join is an unrelated session. A returning player may be in a completely different state than when they left, and the process may have served hundreds of others in between.

Where to go next#

Read this page as Markdown