{"doc":"migration","title":"Build and deploy","markdown":"# Build and deploy\n\nUse this route when the game already works in a browser and the browser still\nowns something a fan could change or inspect: answers, scoring, play limits,\nlocks or settlement.\n\nThe migration keeps the interface. It moves the decisions into the Engine and\nchanges the page to render what the Engine returns.\n\n## The result\n\n| Before | After |\n| --- | --- |\n| Answers or pick values ship to the browser | Solution material stays in the Engine |\n| The page calculates its own score | The Engine returns the committed score |\n| Device storage enforces attempts | The Engine holds player state |\n| The device clock decides whether play is open | Server-derived capability decides whether play is accepted |\n| Retrying a request can count twice | One idempotency key identifies one intent |\n\n## 1. Inspect the project\n\nGet the tool, then let it inspect the game:\n\n```sh\ncurl -fsSL https://gamestage.ai/cli -o gamestage.mjs\nnode gamestage.mjs inspect . --write\n```\n\nDownloading the bundle gives you one file rather than a command on your PATH,\nso every later `gamestage <command>` in this chapter is\n`node gamestage.mjs <command>` for you. `npx gamestage <command>` is the other\nroute and needs no download. [Getting started](/docs/getting-started) has both.\n\nThe inspection of source files is read-only. `--write` adds\n`gamestage.yaml`. It refuses to replace an existing manifest unless `--force`\nis present, because a later manifest may contain a complete container that the\ninspection cannot reconstruct.\n\nIf the CLI is already signed in, `inspect` also makes a best-effort registration\nwith Backstage. A failed registration does not prevent the local report\nor file write.\n\nRead the report in this order:\n\n* **Keep as yours**: presentation, interaction, copy, sound and animation stay in the game.\n\n* **Move behind authority**: each finding names the source file, line and consequence.\n\n* **Add platform support**: the report notes missing identity, consent or platform integration.\n\n* **Ask a person**: licensing, intended audience and operating decisions cannot be inferred from code.\n\n## 2. Confirm the game format\n\nThe format is the Engine rule set, and it is the mechanic rather than the\ntheme or the words on screen. A game about basketball can be a Search, a Push\nYour Luck, a Find the Groups, a Predict, a Bingo or a Penalties.\n\nThe formats that run today:\n\n* **Search** (`hunt`): pick the right few from a board.\n\n* **Push Your Luck** (`push`): make picks whose hidden values build towards a ceiling.\n\n* **Find the Groups** (`group`): submit a set and receive whole-guess feedback such as a near miss.\n\n* **Predict** (`predict`): commit a call before the result exists, then mark it at settlement.\n\n* **Bingo** (`bingo`): each fan is dealt their own board from a shared pool and races for a line.\n\n* **Penalties** (`shoot`): aim, choose a power, shoot. The Engine plays the keeper and decides every penalty.\n\nRead [Game formats](/docs/game-formats) before editing the manifest. Do not\nforce a game into the nearest pattern when its rules differ.\n\n## 3. Write the server-owned container\n\n`inspect` can infer a format. It cannot infer the intended content or rules.\nThe agent writes `backend.round` in `gamestage.yaml`, using the matching\ncontainer contract.\n\nFor a hunt, move the answer set into `targets`. For a push, move each pick's\nvalue into `slates[].entries[].value`. For a group round, move the groups and\ntheir labels into `groups`. A predict has no hidden answer while\nit is open, so the protected fact is the committed call and the lock.\n\nDo not delete solution material before it has moved. A clean client with no\nserver-owned answer leaves the Engine nothing to mark.\n\nThe App Manifest is the current Preview source read by `dev`, `verify` and\n`deploy`. Do not also declare the same live container in\n[Interaction Cloud](https://products.monterosa.co/mic/core-concepts)\n[element](https://products.monterosa.co/mic/reference/core-platform/elements)\nfields and treat both as authoritative: the manifest is the one source today.\n\nValidate the file after each material edit:\n\n```sh\ngamestage manifest validate gamestage.yaml\n```\n\nSee the [App Manifest reference](/docs/app-manifest).\n\n## 4. Replace browser-owned decisions\n\nWrite the generated browser clients beside the game:\n\n```sh\ngamestage client\n```\n\nThis writes two generated files:\n\n* **`gamestage.js`**: the Gamestage client, for round updates, identity, consent, storage and analytics routing.\n\n* **`gamestage-player.js`**: the smaller player API client for a game that only needs Engine reads and plays.\n\nGenerated files are replaced when the command runs again. Keep game logic out\nof them.\n\nThe page should read the public challenge from the Engine, send the fan's pick,\nand render the returned `round_state`, `feedback` and `outcome`. It must not\nrecalculate the result as a second opinion.\n\nSee the [player API guide](/docs/player-api) for the Gamestage client, raw\nroutes, identity headers and retry rules.\n\n### Apply what a producer set\n\nAbove the Starter plan, a game's words belong to whoever runs it. A producer\nsets them in Monterosa Studio. There are ten, listed below, and they cover the\ngame's name and lines, the word on every button a fan can press, a colour, and\nwhat this edition is called. The values arrive on the object `start()` returns,\nand the page chooses which element each one lands on.\n\n```js\nimport { applyPresentation, start } from \"./gamestage.js\";\n\nconst game = await start();\n\napplyPresentation(game.presentation, {\n  displayName: document.getElementById(\"game-name\"),\n  strapline: document.getElementById(\"strapline\"),\n  playButtonLabel: document.getElementById(\"submit\"),\n});\n```\n\n#### All ten settings\n\nEvery one of these arrives on `game.presentation`, and the spelling in the first\ncolumn is what a producer's field is called under the covers. Seven of the ten\nare offered to every game whether the page uses them or not, so a target you do\nnot name is a box a producer fills in and nothing shows.\n\n| Studio field key | Target you pass | Where a producer sets it | What it is |\n| --- | --- | --- | --- |\n| `display_name` | `displayName` | The game | The heading, and the browser title with it |\n| `strapline` | `strapline` | The game | A line under the heading |\n| `how_to_play` | `howToPlay` | The game | The rules text |\n| `primary_colour` | `root` | The game | A colour, applied as `--gamestage-primary` |\n| `play_button_label` | `playButtonLabel` | The game | The word on the main button |\n| `play_again_label` | `playAgainLabel` | The game | The word on the button that starts the next round |\n| `result_button_label` | `resultButtonLabel` | The game | The word on the link back to a finished round |\n| `share_button_label` | `shareButtonLabel` | The game | The word on the share button |\n| `edition_name` | `editionName` | This edition | What a fan sees this edition called |\n| `edition_strapline` | `editionStrapline` | This edition | A line for this edition only: a sponsor, a fixture, a date |\n\nThe colour is the one that does not take an element of its own. It is written\nas a custom property on whatever you pass as `root`, which defaults to the\ndocument element, so the game decides what the colour is for:\n`background: var(--gamestage-primary, var(--red))` uses the producer's colour\nwhen there is one and the game's own when there is not.\n\n**Studio writes `primary_colour`, spelled the British way.** The client also\naccepts `primary_color` from an existing payload. If both arrive, the Studio\nfield wins.\n\n**The last three are offered only to a game that has the control.** A page that\nnames no `shareButtonLabel` target has no share button, so a producer is never\nhanded a word for one. Nominate the target and the box appears on their next\ndeploy.\n\nA line the page draws empty can carry `hidden`, which a filled field removes.\n\nA producer saving in Studio announces it over the same connection that carries a\nround change, so a fan with the game open need not reload. Keep the targets in a\nvariable and apply them again when it fires:\n\n```js\ngame.onPresentationChanged((presentation) => {\n  applyPresentation(presentation, presentationTargets);\n});\n```\n\nA field nobody has filled in is absent rather than empty, and\n`applyPresentation` leaves that element as the game built it. Copying values out\nby hand with `?? \"\"` instead blanks the heading the first time a producer saves\nthe form without typing anything.\n\n### Load when Studio is slow or down\n\nThe library remembers the last settings Studio gave each fan's device, so a\npage needs no words or colours of its own. Leave elements empty in the page and\nlet `applyPresentation` fill them.\n\n| Visit | Studio | What the fan sees |\n| --- | --- | --- |\n| Returning | Answers | The remembered settings at once, replaced by Studio's when they arrive. |\n| Returning | Slow or down | The remembered settings. The game plays. |\n| First | Answers | Studio's settings. Play appears once they arrive. |\n| First | Down | The library's \"Can't load the game right now\" screen with Try again. |\n\nRemembering needs the fan's \"functional\" consent. Without it the settings stay\nin memory and every visit waits for Studio, as a first visit does.\n\nOn a first visit with Studio down, `start()` throws `GamestageUnavailableError`\nafter drawing its screen. Check `error.handled` and show nothing of your own:\n\n```js\ntry {\n  game = await start();\n} catch (error) {\n  if (error.handled) return;\n  throw error;\n}\n```\n\nNothing in `verify` checks this, and under `dev` there is no producer, so the\ngame correctly keeps every word it shipped with. To see the values applied\nlocally, set the Studio keys on `window.GAMESTAGE_PRESENTATION` before the game\nstarts:\n\n```js\nwindow.GAMESTAGE_PRESENTATION = { display_name: \"Derby Day\", play_button_label: \"Lock it in\" };\n```\n\nA real producer's settings reach a fan only on a game provisioned into\nInteraction Cloud.\n\n## 5. Run the real rules locally\n\n```sh\ngamestage login\ngamestage dev\n```\n\n`dev` needs a signed-in session, as `verify` and `deploy` do. Without one it\nrefuses with \"You are not signed in.\" `login` is described in full at step 7;\nrun it once and it lasts.\n\n`dev` reads `backend.round` and runs the same Engine rules used by the hosted\nservice. It prints `window.GAMESTAGE_API` and `window.GAMESTAGE_GAME` for the\npage. Stop and restart after changing the container. There is no watch mode.\n\nWith no readable manifest or no `backend.round`, `dev` serves a contract\nfixture and says so. That is useful for wire work, but it is not the creator's\ngame. `--fixture` selects it deliberately.\n\nUse `--as <player>` to run the local JWT harness when the game needs an\nidentified player. The harness mints a temporary keypair and loopback issuer.\nThe manifest validator refuses that issuer in a deployed game.\n\n## 6. Prove the migration\n\n```sh\ngamestage verify\n```\n\nVerification checks nine things:\n\n* **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.\n\n* **Attempt cap chosen**: the attempt cap was set deliberately rather than left to the Engine's default.\n\n* **Solution absent**: nothing a deploy would publish contains the private material for its game format.\n\n* **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.\n\n* **Client present**: a page using a Gamestage API imports a generated client.\n\n* **Client is the door**: the game reaches Monterosa through the client rather than importing a platform kit directly.\n\n* **Server time**: a lock or a countdown is decided by the server's clock rather than the device's.\n\n* **Outcome rendered**: a browser makes a real play and the screen shows the score the Engine committed.\n\n* **Connection noticed**: the page notices the connection going and coming back.\n\nThe solution check reads the whole publish set, not just the page: the same\nlist of files `deploy --dir` uploads, walked with the same call, so the two\ncannot disagree about what ships. It reports how many files are in the set and\nhow many it opened.\n\nA file is read on its contents rather than its extension, so a manifest copied to\n`answers` or `round.dat` is still found. **A manifest is found by parsing, under\nany name**, and a source map is searched inside its own copy of your sources. A\ncredential is a fault. A `.git`, `node_modules`, `.next`, `.nuxt`,\n`.svelte-kit`, `dist` or `build` directory is reported rather than failed,\nbecause a game may legitimately live at the root of a repository, but\n`deploy --dir` would upload it whole.\n\nWhat it cannot promise: binaries and very large files are listed but not opened,\nencoded content is not decoded, and the leak patterns for a hand-written data\nfile work line by line. A copy of your round is caught wherever it is; an\narbitrary re-encoding of the answers may not be. Treat it as a strong net over\nthe directory, not a proof about it.\n\n`--dir <path>` names the directory when it is not the game's own.\n\nIf no browser is available, the last two checks are `skipped`. `verify` exits `0`\nonly when every check verifies, `1` when a check is open, and `2` when no check\nis open but the result is unverified. `--no-browser` also yields `2`, because\nasking not to inspect the screen does not prove it.\n\n## 7. Deploy it\n\nDeploying runs on the CLI's own defaults. There is no environment variable to\nset: the CLI points at `https://api.gamestage.ai` and that is the service that\nanswers.\n\n```sh\ngamestage login\ngamestage link github\ngamestage manifest register gamestage.yaml\ngamestage deploy <game-id> --dir .\n```\n\nThe first two commands open a device flow a person approves in a browser:\n`login` opens the browser itself, and `link github` prints a code and a URL\nfor the person to open. Registration is safe to repeat and is needed when a\nmanifest was created while signed out.\n\n**Signing in is not the same as being allowed to deploy.** `login` creates an\naccount and leaves it pending, and the commands that reach the Engine refuse a\npending account and say so. Somebody at Monterosa approves it. Once approved,\nthe four commands above run through with nothing else to configure.\n\n`GAMESTAGE_CONTROL_URL` exists only to point the CLI at a different\nenvironment, which is a Monterosa concern rather than a step in this route.\n\nChoose one publish shape:\n\n* **One HTML file**: `--file index.html` uploads the page, both generated clients and a starter favicon.\n\n* **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.\n\nUpload happens before the deployment gate. A refusal can therefore leave new\nfiles in storage without making the game live. A refusal exits successfully\nand carries a stable code in JSON because it is an expected decision, not a\ntransient fault.\n\n### What a deploy publishes besides the game\n\nA deploy also builds the game its own App Spec, named for the game and\ndeclaring the one format that game runs. Before this, every project pointed at\na single app called Gamestage, so a producer opening the Apps list in Monterosa\nStudio saw our product's name and no way to tell which row was theirs, and was\nhanded every format's element types for a game running one.\n\nEach spec is versioned `0.0.<manifest revision>`, so a game's spec history is\nits deploy history, and every published version is stored rather than worked\nout later: a registered app holds one version's URL for ever and Studio\nfetches it each time it draws a producer's screens. A new app is registered\nwhen the shape changes, so a rename or a format change earns a new row in the\nApps list while a changed round reuses the one that is there.\n\n## What finished means\n\nThere are two separate finishes today:\n\n* **Verified locally**: `verify` ran all nine checks, every check is `verified`, and a person has played the result in a browser.\n\n* **Hosted and operating**: the deployment was accepted, the container was provisioned, and a person opened the returned Playground URL.\n\nDo not report the second when only the first has happened.\n"}