Build and deploy

Move authority into the Engine, run the game locally and prove the result.

Use this route when the game already works in a browser and the browser still owns something a fan could change or inspect: answers, scoring, play limits, locks or settlement.

The migration keeps the interface. It moves the decisions into the Engine and changes the page to render what the Engine returns.

The result

BeforeAfter
Answers or pick values ship to the browserSolution material stays in the Engine
The page calculates its own scoreThe Engine returns the committed score
Device storage enforces attemptsThe Engine holds player state
The device clock decides whether play is openServer-derived capability decides whether play is accepted
Retrying a request can count twiceOne idempotency key identifies one intent

1. Inspect the project

Get the tool, then let it inspect the game:

curl -fsSL https://gamestage.ai/cli -o gamestage.mjs
node gamestage.mjs inspect . --write

Downloading the bundle gives you one file rather than a command on your PATH, so every later gamestage <command> in this chapter is node gamestage.mjs <command> for you. npx gamestage <command> is the other route and needs no download. Getting started has both.

The inspection of source files is read-only. --write adds gamestage.yaml. It refuses to replace an existing manifest unless --force is present, because a later manifest may contain a complete container that the inspection cannot reconstruct.

If the CLI is already signed in, inspect also makes a best-effort registration with Backstage. A failed registration does not prevent the local report or file write.

Read the report in this order:

  • Keep as yours: presentation, interaction, copy, sound and animation stay in the game.
  • Move behind authority: each finding names the source file, line and consequence.
  • Add platform support: the report notes missing identity, consent or platform integration.
  • Ask a person: licensing, intended audience and operating decisions cannot be inferred from code.

2. Confirm the game format

The format is the Engine rule set, and it is the mechanic rather than the theme or the words on screen. A game about basketball can be a Search, a Push Your Luck, a Find the Groups, a Predict, a Bingo or a Penalties.

The formats that run today:

  • Search (hunt): pick the right few from a board.
  • Push Your Luck (push): make picks whose hidden values build towards a ceiling.
  • Find the Groups (group): submit a set and receive whole-guess feedback such as a near miss.
  • Predict (predict): commit a call before the result exists, then mark it at settlement.
  • Bingo (bingo): each fan is dealt their own board from a shared pool and races for a line.
  • Penalties (shoot): aim, choose a power, shoot. The Engine plays the keeper and decides every penalty.

Read Game formats before editing the manifest. Do not force a game into the nearest pattern when its rules differ.

3. Write the server-owned container

inspect can infer a format. It cannot infer the intended content or rules. The agent writes backend.round in gamestage.yaml, using the matching container contract.

For a hunt, move the answer set into targets. For a push, move each pick's value into slates[].entries[].value. For a group round, move the groups and their labels into groups. A predict has no hidden answer while it is open, so the protected fact is the committed call and the lock.

Do not delete solution material before it has moved. A clean client with no server-owned answer leaves the Engine nothing to mark.

The App Manifest is the current Preview source read by dev, verify and deploy. Do not also declare the same live container in Interaction Cloud element fields and treat both as authoritative: the manifest is the one source today.

Validate the file after each material edit:

gamestage manifest validate gamestage.yaml

See the App Manifest reference.

4. Replace browser-owned decisions

Write the generated browser clients beside the game:

gamestage client

This writes two generated files:

  • gamestage.js: the Gamestage client, for round updates, identity, consent, storage and analytics routing.
  • gamestage-player.js: the smaller player API client for a game that only needs Engine reads and plays.

Generated files are replaced when the command runs again. Keep game logic out of them.

The page should read the public challenge from the Engine, send the fan's pick, and render the returned round_state, feedback and outcome. It must not recalculate the result as a second opinion.

See the player API guide for the Gamestage client, raw routes, identity headers and retry rules.

Apply what a producer set

Above the Starter plan, a game's words belong to whoever runs it. A producer sets them in Monterosa Studio. There are ten, listed below, and they cover the game's name and lines, the word on every button a fan can press, a colour, and what this edition is called. The values arrive on the object start() returns, and the page chooses which element each one lands on.

import { applyPresentation, start } from "./gamestage.js";

const game = await start();

applyPresentation(game.presentation, {
  displayName: document.getElementById("game-name"),
  strapline: document.getElementById("strapline"),
  playButtonLabel: document.getElementById("submit"),
});

All ten settings

Every one of these arrives on game.presentation, and the spelling in the first column is what a producer's field is called under the covers. Seven of the ten are offered to every game whether the page uses them or not, so a target you do not name is a box a producer fills in and nothing shows.

Studio field keyTarget you passWhere a producer sets itWhat it is
display_namedisplayNameThe gameThe heading, and the browser title with it
straplinestraplineThe gameA line under the heading
how_to_playhowToPlayThe gameThe rules text
primary_colourrootThe gameA colour, applied as --gamestage-primary
play_button_labelplayButtonLabelThe gameThe word on the main button
play_again_labelplayAgainLabelThe gameThe word on the button that starts the next round
result_button_labelresultButtonLabelThe gameThe word on the link back to a finished round
share_button_labelshareButtonLabelThe gameThe word on the share button
edition_nameeditionNameThis editionWhat a fan sees this edition called
edition_straplineeditionStraplineThis editionA line for this edition only: a sponsor, a fixture, a date

The colour is the one that does not take an element of its own. It is written as a custom property on whatever you pass as root, which defaults to the document element, so the game decides what the colour is for: background: var(--gamestage-primary, var(--red)) uses the producer's colour when there is one and the game's own when there is not.

Studio writes primary_colour, spelled the British way. The client also accepts primary_color from an existing payload. If both arrive, the Studio field wins.

The last three are offered only to a game that has the control. A page that names no shareButtonLabel target has no share button, so a producer is never handed a word for one. Nominate the target and the box appears on their next deploy.

A line the page draws empty can carry hidden, which a filled field removes.

A producer saving in Studio announces it over the same connection that carries a round change, so a fan with the game open need not reload. Keep the targets in a variable and apply them again when it fires:

game.onPresentationChanged((presentation) => {
  applyPresentation(presentation, presentationTargets);
});

A field nobody has filled in is absent rather than empty, and applyPresentation leaves that element as the game built it. Copying values out by hand with ?? "" instead blanks the heading the first time a producer saves the form without typing anything.

Load when Studio is slow or down

The library remembers the last settings Studio gave each fan's device, so a page needs no words or colours of its own. Leave elements empty in the page and let applyPresentation fill them.

VisitStudioWhat the fan sees
ReturningAnswersThe remembered settings at once, replaced by Studio's when they arrive.
ReturningSlow or downThe remembered settings. The game plays.
FirstAnswersStudio's settings. Play appears once they arrive.
FirstDownThe library's "Can't load the game right now" screen with Try again.

Remembering needs the fan's "functional" consent. Without it the settings stay in memory and every visit waits for Studio, as a first visit does.

On a first visit with Studio down, start() throws GamestageUnavailableError after drawing its screen. Check error.handled and show nothing of your own:

try {
  game = await start();
} catch (error) {
  if (error.handled) return;
  throw error;
}

Nothing in verify checks this, and under dev there is no producer, so the game correctly keeps every word it shipped with. To see the values applied locally, set the Studio keys on window.GAMESTAGE_PRESENTATION before the game starts:

window.GAMESTAGE_PRESENTATION = { display_name: "Derby Day", play_button_label: "Lock it in" };

A real producer's settings reach a fan only on a game provisioned into Interaction Cloud.

5. Run the real rules locally

gamestage login
gamestage dev

dev needs a signed-in session, as verify and deploy do. Without one it refuses with "You are not signed in." login is described in full at step 7; run it once and it lasts.

dev reads backend.round and runs the same Engine rules used by the hosted service. It prints window.GAMESTAGE_API and window.GAMESTAGE_GAME for the page. Stop and restart after changing the container. There is no watch mode.

With no readable manifest or no backend.round, dev serves a contract fixture and says so. That is useful for wire work, but it is not the creator's game. --fixture selects it deliberately.

Use --as <player> to run the local JWT harness when the game needs an identified player. The harness mints a temporary keypair and loopback issuer. The manifest validator refuses that issuer in a deployed game.

6. Prove the migration

gamestage verify

Verification checks nine things:

  • Round is the game's own: the round played here is this game's own content, not a fixture or a leftover from somebody else's.
  • Attempt cap chosen: the attempt cap was set deliberately rather than left to the Engine's default.
  • Solution absent: nothing a deploy would publish contains the private material for its game format.
  • Settings read: every setting a producer can change in Monterosa Studio is read by the page. A producer changing an offered field and seeing nothing happen is a broken game rather than a matter of taste, so this fails rather than warns.
  • Client present: a page using a Gamestage API imports a generated client.
  • Client is the door: the game reaches Monterosa through the client rather than importing a platform kit directly.
  • Server time: a lock or a countdown is decided by the server's clock rather than the device's.
  • Outcome rendered: a browser makes a real play and the screen shows the score the Engine committed.
  • Connection noticed: the page notices the connection going and coming back.

The solution check reads the whole publish set, not just the page: the same list of files deploy --dir uploads, walked with the same call, so the two cannot disagree about what ships. It reports how many files are in the set and how many it opened.

A file is read on its contents rather than its extension, so a manifest copied to answers or round.dat is still found. A manifest is found by parsing, under any name, and a source map is searched inside its own copy of your sources. A credential is a fault. A .git, node_modules, .next, .nuxt, .svelte-kit, dist or build directory is reported rather than failed, because a game may legitimately live at the root of a repository, but deploy --dir would upload it whole.

What it cannot promise: binaries and very large files are listed but not opened, encoded content is not decoded, and the leak patterns for a hand-written data file work line by line. A copy of your round is caught wherever it is; an arbitrary re-encoding of the answers may not be. Treat it as a strong net over the directory, not a proof about it.

--dir <path> names the directory when it is not the game's own.

If no browser is available, the last two checks are skipped. verify exits 0 only when every check verifies, 1 when a check is open, and 2 when no check is open but the result is unverified. --no-browser also yields 2, because asking not to inspect the screen does not prove it.

7. Deploy it

Deploying runs on the CLI's own defaults. There is no environment variable to set: the CLI points at https://api.gamestage.ai and that is the service that answers.

gamestage login
gamestage link github
gamestage manifest register gamestage.yaml
gamestage deploy <game-id> --dir .

The first two commands open a device flow a person approves in a browser: login opens the browser itself, and link github prints a code and a URL for the person to open. Registration is safe to repeat and is needed when a manifest was created while signed out.

Signing in is not the same as being allowed to deploy. login creates an account and leaves it pending, and the commands that reach the Engine refuse a pending account and say so. Somebody at Monterosa approves it. Once approved, the four commands above run through with nothing else to configure.

GAMESTAGE_CONTROL_URL exists only to point the CLI at a different environment, which is a Monterosa concern rather than a step in this route.

Choose one publish shape:

  • One HTML file: --file index.html uploads the page, both generated clients and a starter favicon.
  • A static directory: --dir . preserves relative paths, requires a top-level index.html, withholds gamestage.yaml and environment files, and removes stale files left by an older deploy. It does not invent missing generated clients.

Upload happens before the deployment gate. A refusal can therefore leave new files in storage without making the game live. A refusal exits successfully and carries a stable code in JSON because it is an expected decision, not a transient fault.

What a deploy publishes besides the game

A deploy also builds the game its own App Spec, named for the game and declaring the one format that game runs. Before this, every project pointed at a single app called Gamestage, so a producer opening the Apps list in Monterosa Studio saw our product's name and no way to tell which row was theirs, and was handed every format's element types for a game running one.

Each spec is versioned 0.0.<manifest revision>, so a game's spec history is its deploy history, and every published version is stored rather than worked out later: a registered app holds one version's URL for ever and Studio fetches it each time it draws a producer's screens. A new app is registered when the shape changes, so a rename or a format change earns a new row in the Apps list while a changed round reuses the one that is there.

What finished means

There are two separate finishes today:

  • Verified locally: verify ran all nine checks, every check is verified, and a person has played the result in a browser.
  • Hosted and operating: the deployment was accepted, the container was provisioned, and a person opened the returned Playground URL.

Do not report the second when only the first has happened.