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
| Before | After |
|---|---|
| Answers or pick values ship to the browser | Solution material stays in the Engine |
| The page calculates its own score | The Engine returns the committed score |
| Device storage enforces attempts | The Engine holds player state |
| The device clock decides whether play is open | Server-derived capability decides whether play is accepted |
| Retrying a request can count twice | One 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 . --writeDownloading 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.yamlSee the App Manifest reference.
4. Replace browser-owned decisions
Write the generated browser clients beside the game:
gamestage clientThis 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 key | Target you pass | Where a producer sets it | What it is |
|---|---|---|---|
display_name | displayName | The game | The heading, and the browser title with it |
strapline | strapline | The game | A line under the heading |
how_to_play | howToPlay | The game | The rules text |
primary_colour | root | The game | A colour, applied as --gamestage-primary |
play_button_label | playButtonLabel | The game | The word on the main button |
play_again_label | playAgainLabel | The game | The word on the button that starts the next round |
result_button_label | resultButtonLabel | The game | The word on the link back to a finished round |
share_button_label | shareButtonLabel | The game | The word on the share button |
edition_name | editionName | This edition | What a fan sees this edition called |
edition_strapline | editionStrapline | This edition | A 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.
| Visit | Studio | What the fan sees |
|---|---|---|
| Returning | Answers | The remembered settings at once, replaced by Studio's when they arrive. |
| Returning | Slow or down | The remembered settings. The game plays. |
| First | Answers | Studio's settings. Play appears once they arrive. |
| First | Down | The 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 devdev 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 verifyVerification 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.htmluploads the page, both generated clients and a starter favicon. - A static directory:
--dir .preserves relative paths, requires a top-levelindex.html, withholdsgamestage.yamland 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:
verifyran all nine checks, every check isverified, 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.
