One question, answered end to end: what exactly happens — component by component, file by file — between the moment a builder saves a spec in the dashboard and the moment a player launches the game in an operator's lobby? This page traces that path through the two real repositories as they stand today (standout-platform, this repo; and vhslab/standout-games, the engine), names every artefact and the tool that produces it, marks honestly what is automated versus done by hand, and ends with the changes that would close the gap. Almost everything here is GROUNDED · matches main; the proposals at the end are CONCEPT · proposed. The audience includes external Game Builders — terms are glossed on first use.
"Going live" is not one system — it is a baton passed across two repositories and a cloud runtime. The dashboard (this repo, standout-platform) is where a builder authors a GameSpec and saves it. The CLI bridge (standout scaffold-game, which lives in standout-platform's apps/cli) turns that spec into source code inside the engine repo (vhslab/standout-games) — a Bun + Turborepo monorepo holding 15 games today. CI (three GitHub Actions workflows) builds container images from the engine repo and ships them to GCP, where three Cloud Run services run the engine, the game clients, and the launcher API. An operator then mints a signed launch URL and the player connects. The diagram below shows the artefact flow across those lanes; the table beneath names every component with its exact path.
Read it as a relay: blue lane authors and stores the spec; the flame edges are where a human carries the baton (export the spec, run the scaffold, run sync-games.ts, set the CI GAMES arg, merge the PR); the green lane is fully automated once the PR lands. The two flame boxes inside the engine lane — sync-games.ts and the CI GAMES argument — are the friction this whole page exists to make legible.
| Component | Exact path | Role |
|---|---|---|
| Builder UI | apps/web (this repo) | React app where a builder authors a GameSpec — template + config overrides + theme. |
| Spec API + audit | apps/mcp/src/api.ts | Serves /api/specs; every write attributed to the authenticated actor and appended to audit_log (create_spec / update_spec). |
| Spec store | D1 table game_specs | Persists each spec as a JSON blob (specRow() in packages/core/src/repo.ts). |
| Template registry | packages/shared/src/templates.ts | Three hard-coded GameTemplates (crash, trunk-raider, silk-road) declaring config fields + baseGame. Three of the engine's 15 games. |
| Scaffold CLI | apps/cli/src/scaffold.ts | standout scaffold-game --spec <spec.json> — the bridge that turns a spec into engine source. 12 steps; see Stage B. |
| Base game (source) | games/<baseGame>/ | The folder the scaffold copies: server/ (plugin), client/ (React 19 + Vite app), shared/ (types/physics), optional provably-fair/. |
| Server plugin + manifest | games/<slug>/server/src/plugin.ts | Exports the plugin; the embedded manifest (id, name, version, type, sessionMode, defaultRtp, useBonus, onServerFailure, rateLimits) is the contract the engine loads. |
| Config + types | games/<slug>/server/src/types.ts | Holds DEFAULT_CONFIG — the object literal the scaffold edits to apply spec overrides. |
| Client app | games/<slug>/client/ | Full Vite app using @standout/game-sdk/react (RoyaleProvider, GameSessionProvider, GameLoader); Socket.IO 4.8 + Zustand. client/src/assets.ts hard-codes 110+ CDN asset paths. |
| Plugin registry | apps/game-engine/src/plugins/registry.ts | Hard-coded PLUGIN_MAP (manifest.id → { pkg, exportName }) + GAME_ID_ALIASES; GAME_ID/GAMES env vars select subsets; dynamic import(config.pkg). |
| Game engine | apps/game-engine (Cloud Run :8080) | Fastify + Socket.IO host. SessionManager + SocketManager; verifies the launch JWT on Socket.IO auth; serves GET /games (manifest catalogue source). |
| Game clients service | games/ built via games/Dockerfile → nginx | Per-game Vite client bundles served statically behind nginx; routes generated by scripts/sync-games.ts into games/nginx.conf. |
| Launcher API | apps/games-service (Cloud Run) | Operator-facing supplier API. POST /supplier/launch mints the session JWT; reads the engine catalogue (60s cache of GET /games). |
| Sync script | scripts/sync-games.ts | Regenerates the auto-generated COPY blocks in both Dockerfiles and the redirect/location/fallback blocks in games/nginx.conf. Manual step after adding a game. |
| CI workflows | .github/workflows/_deploy-{engine,games,games-service}.yml | Build images with --build-arg GAMES=… → Artifact Registry → Cloud Run. The engine GAMES arg is a manually-supplied workflow input. |
| RNG package | packages/game-rng | SHA-256 hash-counter CSPRNG (GLI-19 §3.3.1); CERTIFICATION.md locks src/provably-fair.ts; BUILD_HASH verified at deploy. |
| Wallet adapter | packages/wallet-adapter-server | ExternalAdapter calls operator REST /wallet/{balance,bet,win,rollback}, HMAC-signed both directions; bet state machine in Postgres. |
| Backing services | Cloud SQL · Memorystore · GCS | Postgres (bet/session state), Redis (Memorystore), and the GCS asset bucket parameterised by VITE_ASSET_BUCKET_URL. |
games/trunk-raider-edge/, apps/edge-do/, apps/edge-engine/, and packages/edge-game-sdk/ (Rust/WASM Cloudflare Workers, with wrangler.jsonc and Durable-Object hosts). These are real, but they are experiments: production today is Cloud Run. The Workers-for-Platforms architecture described in architecture.html · Edge delivery is the decided target, not today's runtime. See Today vs target.
A "game" in production is not a single file — it is a set of artefacts that must all exist and agree for one manifest.id to be launchable. The table below is the definitive checklist. For each artefact it records who creates it today: ✅ scaffold means scaffold-game writes it; ⚠ manual means a human does it by hand after scaffolding; 🤖 automatic means the runtime derives it with no per-game action. The manual rows are the gap story.
| Artefact | Path | What it declares / does | Created by |
|---|---|---|---|
| Copied game folder | games/<slug>/ |
The whole tree copied from the base game, excluding node_modules/dist/.turbo. |
✅ scaffold |
| Server plugin + manifest | …/server/src/plugin.ts |
Plugin export + embedded manifest. Scaffold re-IDs id and name; everything else inherits from the base. |
✅ scaffold |
| Types / DEFAULT_CONFIG | …/server/src/types.ts |
The config object the engine instantiates the game with. Scaffold splices spec overrides into the literal (per-key regex). | ✅ scaffold |
| Package identities | every package.json + import refs |
@standout/<base>-server → @standout/<slug>-server across server/client/shared and workspace deps + scripts. |
✅ scaffold |
| Client app | …/client/ |
The playable front end. Copied verbatim — the slug is renamed in package identity, but the theme is not applied (see next row). | ✅ scaffold ⚠ theme by hand |
| Theme applied to client | …/client/src/assets.ts, Tailwind, loader |
Palette/asset swap so the variant looks different. Scaffold writes the theme only to variant.meta.json; nothing reads it. |
⚠ manual |
| Shared package | …/shared/ |
Types/physics shared between server and client; renamed in identity, copied otherwise. | ✅ scaffold |
| variant.meta.json | games/<slug>/variant.meta.json |
Provenance record: id, name, templateId, baseGame, theme, createdAt. Written by scaffold; not consumed by any build step. | ✅ scaffold |
| Registry entry | apps/game-engine/src/plugins/registry.ts |
PLUGIN_MAP["<slug>"] = { pkg, exportName } inserted at the top of PLUGIN_MAP (exportName copied from the base entry). Not type-checked by the scaffold. |
✅ scaffold |
| nginx route | games/nginx.conf |
Redirect + location ^~ /<slug>/ + asset fallbacks so the client is reachable. Generated only when sync-games.ts is run. |
⚠ manual (sync-games) |
| Dockerfile COPY lines | games/Dockerfile, apps/game-engine/Dockerfile |
The auto-generated COPY block that pulls the new game's package.json files into the image. Regenerated by sync-games.ts. |
⚠ manual (sync-games) |
| CI GAMES build-arg | deploy-engine-*.yml input → --build-arg GAMES |
The engine image only loads the games named in GAMES. A new slug must be added to the workflow input for the engine to load it. |
⚠ manual |
| games-service catalogue row | derived from engine GET /games |
Once the engine boots with the game loaded, its manifest appears in the catalogue (60s cache) and /supplier/launch accepts the gameId. No per-game file. |
🤖 automatic |
| Operator config | operator integration (off-platform) | The operator must add the gameId to their lobby and point wallet callbacks at the adapter. Outside both repos. | ⚠ manual (operator) |
Count the colours: of the fourteen artefacts, the scaffold produces eight outright, the catalogue row is automatic, and five are manual — theme application, the two sync-games outputs, the CI GAMES arg, and operator config. A variant is therefore roughly "scaffold gets you to source-complete; a human gets you to deployable." Section 5 proposes collapsing the manual rows.
Let us trace one concrete build all the way to a player. The worked example is Neon Heist — the suite's reskin of Crash (see examples · Reskin — Neon Heist): same certified shared-world mechanic and RTP 97.0%, a cyan/violet palette (c1 #38e8ff, c2 #7a5cff) replacing Crash's flame. Five stages, A through E.
The builder opens the Builder UI, picks the Crash template, sets the theme colours and the variant name "Neon Heist", and saves. The save is a POST /api/specs handled in apps/mcp/src/api.ts; the spec is written to the D1 game_specs table as a JSON blob and the action is appended to audit_log as create_spec (attributed to the authenticated actor). The shape persisted is the GameSpec:
{
"name": "Neon Heist",
"templateId": "crash",
"config": { "minCrashPoint": 1.0, "maxCrashPoint": 100.0 },
"theme": { "c1": "#38e8ff", "c2": "#7a5cff", "name": "Neon Heist" }
}
To feed the next stage, the builder exports this record to a spec.json file. That hand-off — dashboard to CLI — is the first flame edge in diagram 1: there is no automated trigger from the dashboard into the scaffold today.
The engineer runs standout scaffold-game --spec spec.json --games-repo ../standout-games. The CLI executes twelve steps (verified against apps/cli/src/scaffold.ts), grouped into four bands — Copy, Rewrite, Wire, Ship:
spec.json; require name and templateId. fail-fast if missinggetTemplate(templateId) → resolves baseGame ("crash") and base manifest id. packages/shared/src/templates.ts--games-repo → $STANDOUT_GAMES_REPO → sibling ../standout-games. existence-checkedslugify("Neon Heist") → neon-heist; refuse if games/neon-heist exists. games/<slug> reservedcpSync the whole games/crash/ tree, excluding node_modules/dist/.turbo. → games/neon-heist/ [Copy]package.json name + workspace dep + script: @standout/crash-* → @standout/neon-heist-*. server/client/shared + root [Rewrite]server/src/plugin.ts, set manifest id: "neon-heist" and name: "Neon Heist" (anchored regex). other manifest fields inherited{min,max} ranges; warn on keys not found. …/server/src/types.tsPLUGIN_MAP["neon-heist"] = { pkg: "@standout/neon-heist-server", exportName } at the top of PLUGIN_MAP (exportName copied from the base entry). apps/game-engine/src/plugins/registry.tsgit checkout -b builder/neon-heist; stage the new folder + registry; commit. never on main [Ship]--pr and gh available, open the PR; otherwise print the command.
Output of Stage B: a committed branch builder/neon-heist in standout-games with a source-complete game folder and a registry entry — and a flame-coloured reminder that the scaffold's job ends there. What it deliberately does not do becomes Stage C.
bun scripts/sync-games.ts in the engine repo. This regenerates the COPY blocks in games/Dockerfile and apps/game-engine/Dockerfile and the redirect/location/fallback blocks in games/nginx.conf. Without it the client image has no route to /neon-heist/ and the Dockerfiles never copy the package. The scaffold does not run it — the Dockerfiles and nginx are stale the moment the branch is created.GAMES build arg. The engine loads all PLUGIN_MAP keys when GAMES is empty, but when a deploy explicitly limits the list (as staging and production deploys typically do), any slug absent from GAMES in deploy-engine-staging.yml (passed through to _deploy-engine.yml as --build-arg GAMES=…) will be silently omitted — the deployed engine never registers the plugin and /supplier/launch will 404 the gameId.theme (c1 #38e8ff, c2 #7a5cff) sits unused in variant.meta.json — nothing reads it. To make Neon Heist actually look like Neon Heist, an engineer edits the copied client's client/src/assets.ts (which hard-codes ${ASSET_BUCKET_URL}/game-assets/trunk-raider-style paths — the per-game segment is literal, only the bucket is parameterised), the Tailwind palette, and the loader colours, then produces and uploads the new art. This is the bulk of the human effort in a reskin.
With the PR merged, the staging deploy workflows run. _deploy-engine.yml builds apps/game-engine/Dockerfile with --build-arg GAMES=… (now including neon-heist), tags the image, pushes it to GCP Artifact Registry, and runs gcloud run services replace to roll out the royale-game-engine Cloud Run service. _deploy-games.yml builds games/Dockerfile (every client's Vite bundle + nginx) and deploys the clients service. _deploy-games-service.yml ships the launcher API. Backing services are fixed: Postgres on Cloud SQL, Redis on Memorystore, assets on the GCS bucket. Once the engine boots with the game loaded, its manifest is served at GET /games; the games-service catalogue picks it up within its 60-second cache window — no per-game launcher change.
Now a player can play. The operator calls POST /supplier/launch on the games-service with an HMAC-signed request (verified by verifyHmac); the body carries operatorId, playerId, gameId: "neon-heist", currency, optional betLimits, callbackUrl, and a correlation sessionId. The service confirms the gameId exists in the catalogue, then mintSessionToken(...) issues a session JWT (subject operatorId:playerId) and returns a launch URL: {playBaseUrl}/neon-heist?token=<jwt>. The player's browser loads that URL — nginx serves the neon-heist client bundle — and the client opens a Socket.IO connection to the engine, which verifies the launch JWT (GAME_SESSION_JWT_SECRET) on auth. From there, every bet/win flows through the ExternalAdapter to the operator's wallet REST endpoints (/wallet/{balance,bet,win,rollback}), HMAC-signed both directions, with the bet state machine persisted in Postgres.
GAMES build-arg includes the slugGET /games)/supplier/launch accepts the gameIdEach row is one gap: where it bites (with a path), what it costs today, the fix, and which suite proposal already covers it. Amber "NOT YET COVERED" marks a gap no current proposal owns.
| Gap | Where it bites | Cost today | Proposed fix | Covered by |
|---|---|---|---|---|
| Theme captured, not applied | variant.meta.json vs client/src/assets.ts |
Every reskin is hand-edited across assets + Tailwind + loader; the spec's colours are dead data. | Shared client shell that reads theme tokens at runtime. | build-changes #1 |
| Client duplicated per variant | games/trunk-raider vs games/trunk-raider-v2 |
A full client app is copied for every variant — no shared shell or theme tokens; bug fixes must be re-applied per copy. | Data-not-code reskins: one shell, per-variant token files. | build-changes #1 |
| Scaffold doesn't run sync-games | scripts/sync-games.ts, Dockerfiles, nginx.conf |
Dockerfiles + nginx stale the instant the branch exists; forgetting it means no route and no COPY. | scaffold-game v2 runs sync-games.ts as a step. |
build-changes #3 |
| CI GAMES list is manual | deploy-engine-*.yml → --build-arg GAMES |
Engine silently omits the plugin if the slug isn't added; launch 404s with no obvious cause. Note: the engine loads all PLUGIN_MAP keys when GAMES is empty — the risk bites only when a deploy explicitly limits the list. |
scaffold-game v2 patches the workflow input. | build-changes #3 |
| Registry insert unvalidated | apps/game-engine/src/plugins/registry.ts |
A bad exportName or pkg name only surfaces at engine boot / deploy, not at scaffold time. |
Type-check the variant + validate the registry entry in the CLI. | build-changes #3 |
| Templates: 3 of 15; risk of drift | packages/shared/src/templates.ts |
Only crash/trunk-raider/silk-road are buildable; the field lists are hand-maintained copies of engine DEFAULT_CONFIGs, so they can silently diverge as the engine evolves. |
Auto-extract templates from engine DEFAULT_CONFIGs at build time. |
build-changes #2 |
| Scaffold is CLI-only | apps/cli/src/scaffold.ts |
Not exposed via MCP or the dashboard; an engineer must run it locally, and the action is not written to audit_log. |
Expose scaffold via MCP so the dashboard/agents trigger it, audited. | build-changes #3, arch · agents |
| No spec ↔ game linkage | D1 game_specs ↔ games/<slug> |
Nothing in the DB records that a spec produced a given game folder/PR; provenance lives only in variant.meta.json in the other repo. |
versions[] / baseGameRef on the grown spec record the PR + Worker. |
arch · data model |
| No RTP simulation tooling | engine repo — none exists | Config changes ship without a proof that RTP/volatility still hold; reviewers eyeball numbers. | Build an RTP simulation harness (the Math-Sim agent's tool). | build-changes #5, arch · Math-Sim |
| GCP today vs WfP target | Cloud Run vs architecture.html#edge | The decided edge target is unbuilt for production; migration ownership/sequencing is undefined. | Grow the existing edge beachhead incrementally per game. | build-changes #6 |
| No per-variant preview deploys | CI — no preview stage | A variant can't be seen running until it is merged to main and deployed to staging. | Add a preview-deploy CI stage per variant branch. | NOT YET COVERED · #4 |
packages/shared/src/templates.ts (lines 64–66) and the engine's games/trunk-raider/server/src/types.ts (lines 103–105) use silentPick 15 / informant 32 / cheatCode 65. The real risk is not a current mismatch but the mechanism: the template field lists are hand-maintained copies, so nothing prevents them from drifting the next time the engine's DEFAULT_CONFIG changes. That is exactly what build-change #2 (auto-extraction) removes.
Six changes, in priority order. Effort is a rough guess (S / M / L). All are CONCEPT · proposed.
Change: Extract one client shell that all variants share, driven by a per-variant theme-token file (palette, asset manifest, copy). A reskin becomes a data file, not a forked client.
Why: This is the single highest-leverage change. Today games/trunk-raider and games/trunk-raider-v2 are near-duplicate client apps, and a reskin is hours of hand-editing assets.ts + Tailwind. The theme captured in variant.meta.json would finally be read.
Unlocks: data-not-code reskins; shared bug fixes; the theme field stops being dead data; per-variant previews become cheap.
Change: A build-time script reads each game's server/src/types.ts DEFAULT_CONFIG and generates the GameTemplate field list, replacing the three hand-maintained templates.
Why: Kills drift at the source (see the corrected drift note in §gaps) and opens all 15 engine games to the Builder, not just three.
Unlocks: every game buildable; template fields can never silently diverge from the engine; new engine games appear in the Builder automatically.
Change: Extend the CLI to (a) run sync-games.ts, (b) patch the CI GAMES input, (c) write theme tokens (paired with #1), (d) tsc --noEmit the variant, (e) validate the new registry entry resolves, (f) write the action to audit_log, and (g) expose itself over MCP so the dashboard and agents can trigger it.
Why: Collapses four of the five manual rows in §anatomy into the tool, and makes the Scaffolder agent in architecture.html buildable on a real CLI rather than a hypothetical one.
Unlocks: "save spec → PR" with no hand steps; audited, agent-triggerable scaffolding; fewer broken deploys.
Change: A CI stage that deploys each variant branch to a throwaway preview (its own engine + client) before merge.
Why: Today a variant can't be seen running until it's merged and on staging. This is the one gap no current proposal fully owns.
Unlocks: review-before-merge on real running games; the sandbox-launch experience from Launch Lab on actual deploys.
Change: Build the simulation tool the Math-Sim agent needs — replay the (certified, locked) RNG against a config to produce an RTP/volatility/max-win report. No such tooling exists in the engine repo today.
Why: Config changes currently ship without a proof artefact. Note the open compliance question in architecture.html · open questions about whether simulating against the certified RNG carries recertification implications — that must be answered first.
Unlocks: the Math-Sim agent; RTP-sim validation gate; reviewers approve a number, not a guess.
Change: Grow the existing beachhead — games/trunk-raider-edge, packages/edge-game-sdk, apps/edge-do/edge-engine — into the Workers-for-Platforms target one game at a time, rather than a big-bang cutover.
Why: The edge target is decided (architecture.html#edge) and the experiments already prove the runtime; the missing piece is owned, sequenced migration.
Unlocks: the WfP architecture (immutable Workers, route-flip rollback, Durable-Object rooms) without abandoning Cloud Run mid-flight.
The same game, two runtimes. The left column is what ships today (GROUNDED · matches main); the right is the decided edge target (CONCEPT · proposed, fully specified in architecture.html · Edge delivery).
VITE_ASSET_BUCKET_URL; per-game path segment hard-coded in assets.ts.GAMES build-arg.gcloud run services replace.
The point of the two columns is not that today is wrong — Cloud Run ships real games to real operators right now. The point is that the migration is credible and incremental: the edge experiments (trunk-raider-edge, edge-game-sdk, edge-do) already run the target runtime, so the path from here to there is per-game, not a rewrite. See build-change #6.