Demo Guests and devtools on local and preview stacks #394

Closed
opened 2026-10-14 09:55:13 +00:00 by root · 8 comments
root commented 2026-10-14 09:55:13 +00:00 (Migrated from gitlab.core-project.space)

Problem Statement

The owner wants to look at the Guest-history features ("My games" and the stored Game page with PGN export and copy link, spec #355) on a preview or locally, without first playing whole Games to their end in two browsers. Every Guest's secret lives only in the browser that minted it, and the database holds only its hash (ADR 0034), so there is no way today to open a stack as a Guest who already has decided Games.

Solution

Each local and preview stack carries a fixed set of decided Games owned by two Demo Guests, whose secrets are fixed, public demo values. On those stacks, the TanStack devtools show a "Guest" panel that switches the browser to a Demo Guest in one click, or to a secret pasted by hand from another browser. Production carries neither the data nor the devtools: the same image is deployed everywhere (ADR 0036), so the gate is a runtime env flag set per stack, never a build flag.

User Stories

  1. As the owner, I want a local database seeded with decided Games as soon as I run pnpm dev, so that "My games" is never empty when I work on it.
  2. As the owner, I want to run the seed by hand with db:seed, so that I can restore the demo Games after wiping or editing them.
  3. As the owner, I want every preview deploy to seed its database, so that a merge request's preview shows Guest history without any setup.
  4. As the owner, I want the seed to be idempotent, so that every redeploy and every pnpm dev leaves exactly the same demo rows.
  5. As the owner, I want the seed to refuse to run unless the stack enables devtools, so that it can never write demo Games into production.
  6. As the owner, I want Demo Guest A to own Games covering every ending: a checkmate won and one lost, a resignation by each Color, each of the engine's four draw reasons (stalemate, fifty-move, threefold, insufficient material), an agreed draw, at least one Game played as Black, and one whose opponent Seat has no owner, ended at dates spread over time, so that every row and result of "My games" can be seen.
  7. As the owner, I want Demo Guest B to own one Game that A did not play, so that I can open a stored Game page as a reader rather than as a Player.
  8. As the owner, I want every demo Game's line to replay through the engine to its stated ending, so that the Game page never shows a board that contradicts its result.
  9. As the owner, I want the API to answer a devtools probe only on stacks whose flag is on, and to answer 404 everywhere else, so that production exposes nothing new.
  10. As the owner, I want the probe to list the Demo Guests' labels and secrets, taken from the seed's own definition, so that the switcher and the seed can never drift apart.
  11. As the owner, I want the devtools to appear on local and preview stacks, and never in production, from the same build, so that I can use them on a preview.
  12. As a Player in production, I want no devtools code downloaded, so that the page loads no heavier than today.
  13. As the owner, I want a "Guest" panel in the devtools that shows the current Guest masked, so that I know who the browser is without revealing a secret on screen.
  14. As the owner, I want to switch the browser to a Demo Guest in one click, so that I see "My games" and the Game page as that Guest at once, without a reload.
  15. As the owner, I want to paste a Guest secret from another browser, so that I can look at that browser's history too.
  16. As the owner, I want a preview deploy to fail when its seed fails, and every deploy's verification to check that the devtools probe answers on previews and 404s in production, so that a misconfigured stack is caught at deploy time.

Seams

  • API integration, through the RPC on the in-process Postgres (ADR 0038): run the seed, then read through the game procedures and the new devtools procedure, as the existing rpc-* integration tests do. A unit test on the seed's definition, and integration tests. Covers stories 1, 2, 4, 5, 6, 7, 8, 9, 10.
  • Front view, the root route with the app harness and MSW (front CODING_STANDARDS.md rule 8): the devtools mount behind the probe, and the Guest panel inside it. View tests. Covers stories 11, 12, 13, 14, 15.
  • The deploy verification, deploy/verify.sh on a real stack: the seed step's exit state, and the probe's status by stack. Covers stories 3 and 16.

No e2e seam: no Player perceives this feature in production, and the feature sits outside apps/e2e's purpose as the oracle for the Room.

Implementation Decisions

  • The runtime flag, DEVTOOLS_ENABLED. It is on only when it is exactly true; any other value or no value means off, failing closed like an empty origin allowlist (ADR 0028). loadConfig reads it like the other API settings. It is set in the preview-only compose overlay, which up.sh already applies to chess-pr-* stacks only, on both the app and seed services, and in apps/api/.env.local through pnpm env:sync. Production's compose never sets it. There is no hostname check: the stack regex that picks the overlay is already what tells a preview from production.
  • The seed module. A new API entry point beside migrate, outside Nest, with the same env-file handling. It exposes the Demo Guests (label and secret) and the demo Games as data, plus a run function. That function refuses unless DEVTOOLS_ENABLED is on, naming the flag in its error, and upserts by fixed ids: Guests by their secret's hash, Games by a fixed id each. Scripts: db:seed (built, for the image) and a dev variant. The API's dev script runs the seed after migrate:dev. It prints the next steps: which Guests exist and how to switch to them.
  • Demo Guests. There are two, "Demo Guest A" and "Demo Guest B". Their secrets are fixed, readable 22-character base64url strings, the same shape a minted Guest has, so they stay valid if GuestSchema ever checks the format. They are public by design: only demo data is reachable with them.
  • Demo Games. About 12 for A and one for B, matching story 6. Each Game is a UCI line (ADR 0021) plus its ending, winner and draw reason, consistent with the games table's CHECK constraints. The lines that already exist in the Room fixtures (stalemate, threefold, insufficient material, fifty-move, Fool's mate) are the prior art, and moving them into a shared place is a fair prefactor. startedAt and endedAt are fixed, relative to a fixed date, never to now, so reruns stay idempotent.
  • Preview deploy. A one-shot seed service in the preview overlay runs the image's built seed. up.sh runs it after migrate and before app on preview stacks only, with its exit code failing the deploy, the same way it treats migrate (ADR 0032). verify.sh allows the seed container on previews, and checks that it exited cleanly. It also checks that GET /rpc/devtools answers 200 on a preview and 404 on production.
  • Contract: a new devtools group in @chess/contracts (ADR 0027), with one procedure, GET /devtools, whose output is { demoGuests: [{ label, secret }] }. It sits under /rpc, so the origin guard applies (a GET without Origin passes, per ADR 0028).
  • API: a devtools Nest feature module, imported by the app module only when the config's flag is on. When it is off, the route does not exist and /rpc/devtools answers 404. The controller returns the seed module's Demo Guests. Nest stays out of the seed module (ADR 0027's layering).
  • Front: the devtools ship in the build. Set removeDevtoolsOnBuild: false. The root route no longer mounts the TanStack devtools unconditionally: it calls the probe once at boot, through the contract client and TanStack Query (ADR 0012). Only on a 200 does it dynamically import() the devtools and mount them with the Router, Query and Guest panels. On 404 it mounts nothing and downloads no devtools chunk. Local dev goes through the same probe, with no import.meta.env.DEV branch.
  • Front: the Guest panel, a TanStack devtools plugin. It shows the held Guest masked, "Switch to…" for each Demo Guest from the probe, and "Paste a secret". Both actions call keepGuest() and invalidate the Query cache, so the views refetch with the new x-guest header. There is no "Forget" and no "back to my Guest": a private window resets, and a local or preview Guest is throwaway.
  • Glossary. Add Demo Guest: a Guest whose secret is a fixed, public value, seeded only on stacks that enable devtools.
  • New ADR: "devtools and demo data ship in the image, gated at runtime per stack", recording that the gate is the DEVTOOLS_ENABLED flag set by the preview overlay, never a build flag, because previews and production run one image. It is written with domain-modeling and added to the ADR index.

Testing Decisions

  • Prior art:
    • the rpc-games and rpc-game-reads integration tests (in-process Postgres, rpc.fixtures) for seeding and reading back;
    • room-decided.test and the Room fixtures for the ending lines;
    • the config tests for the flag's parsing;
    • the front's games harness with MSW for the view tests (My games and the Game page tests).
  • Unit (seed definition): every demo Game's line replays through the engine to exactly its stored ending, winner and draw reason, and the set covers every ending in story 6.
  • Integration:
    • after the seed runs, Demo Guest A's game.list returns its Games in endedAt order, with the expected results;
    • B's Game is readable through game.get, and is absent from A's list;
    • running the seed twice leaves the same rows;
    • the seed refuses with the flag off, and with the flag set to anything but true;
    • /rpc/devtools answers the Demo Guests when the flag is on, and 404 when it is off.
  • View:
    • a 404 probe mounts no devtools;
    • a 200 probe mounts them with the Guest panel;
    • the panel masks the current Guest;
    • "Switch to…" and "Paste a secret" change the x-guest header of the next request, and My games refetches.
  • Unhappy paths:
    • a probe that errors (network, 500) behaves as off;
    • an empty pasted secret is refused;
    • the seed against a database with no migrations fails loudly, rather than half-seeding.

Out of Scope

  • Recovering a real Guest's secret: impossible by design, since only its hash is stored (ADR 0034).
  • Any Guest in a URL (spec #355, story 2): no ?secret= route.
  • Demo Accounts, claims and Sessions.
  • Seeding live Rooms or in-progress Games.
  • An e2e test.
  • Seeding production, or a production toggle for devtools.

Further Notes

  • Tracked as three tickets delivered in one merge request through implement-spec: the seed, then devtools on previews, then the Guest panel.
  • Previews are public, so anyone can switch to a Demo Guest there. That is accepted: only demo data is reachable.
  • Watch apps/e2e: its built target reads .env.local, which will now turn devtools on. Check that the devtools trigger overlaps none of the elements the suite clicks.
  • The seed ships in the production image, because it is the same image, but it refuses to run without the flag, and no production step calls it.
## Problem Statement The owner wants to look at the Guest-history features ("My games" and the stored Game page with PGN export and copy link, spec #355) on a preview or locally, without first playing whole Games to their end in two browsers. Every Guest's secret lives only in the browser that minted it, and the database holds only its hash (ADR 0034), so there is no way today to open a stack as a Guest who already has decided Games. ## Solution Each local and preview stack carries a fixed set of decided Games owned by two **Demo Guests**, whose secrets are fixed, public demo values. On those stacks, the TanStack devtools show a "Guest" panel that switches the browser to a Demo Guest in one click, or to a secret pasted by hand from another browser. Production carries neither the data nor the devtools: the same image is deployed everywhere (ADR 0036), so the gate is a runtime env flag set per stack, never a build flag. ## User Stories 1. As the owner, I want a local database seeded with decided Games as soon as I run `pnpm dev`, so that "My games" is never empty when I work on it. 2. As the owner, I want to run the seed by hand with `db:seed`, so that I can restore the demo Games after wiping or editing them. 3. As the owner, I want every preview deploy to seed its database, so that a merge request's preview shows Guest history without any setup. 4. As the owner, I want the seed to be idempotent, so that every redeploy and every `pnpm dev` leaves exactly the same demo rows. 5. As the owner, I want the seed to refuse to run unless the stack enables devtools, so that it can never write demo Games into production. 6. As the owner, I want Demo Guest A to own Games covering every ending: a checkmate won and one lost, a resignation by each Color, each of the engine's four draw reasons (stalemate, fifty-move, threefold, insufficient material), an agreed draw, at least one Game played as Black, and one whose opponent Seat has no owner, ended at dates spread over time, so that every row and result of "My games" can be seen. 7. As the owner, I want Demo Guest B to own one Game that A did not play, so that I can open a stored Game page as a reader rather than as a Player. 8. As the owner, I want every demo Game's line to replay through the engine to its stated ending, so that the Game page never shows a board that contradicts its result. 9. As the owner, I want the API to answer a devtools probe only on stacks whose flag is on, and to answer 404 everywhere else, so that production exposes nothing new. 10. As the owner, I want the probe to list the Demo Guests' labels and secrets, taken from the seed's own definition, so that the switcher and the seed can never drift apart. 11. As the owner, I want the devtools to appear on local and preview stacks, and never in production, from the same build, so that I can use them on a preview. 12. As a Player in production, I want no devtools code downloaded, so that the page loads no heavier than today. 13. As the owner, I want a "Guest" panel in the devtools that shows the current Guest masked, so that I know who the browser is without revealing a secret on screen. 14. As the owner, I want to switch the browser to a Demo Guest in one click, so that I see "My games" and the Game page as that Guest at once, without a reload. 15. As the owner, I want to paste a Guest secret from another browser, so that I can look at that browser's history too. 16. As the owner, I want a preview deploy to fail when its seed fails, and every deploy's verification to check that the devtools probe answers on previews and 404s in production, so that a misconfigured stack is caught at deploy time. ## Seams - **API integration, through the RPC on the in-process Postgres (ADR 0038):** run the seed, then read through the `game` procedures and the new `devtools` procedure, as the existing `rpc-*` integration tests do. A unit test on the seed's definition, and integration tests. Covers stories 1, 2, 4, 5, 6, 7, 8, 9, 10. - **Front view, the root route with the app harness and MSW (front `CODING_STANDARDS.md` rule 8):** the devtools mount behind the probe, and the Guest panel inside it. View tests. Covers stories 11, 12, 13, 14, 15. - **The deploy verification, `deploy/verify.sh` on a real stack:** the seed step's exit state, and the probe's status by stack. Covers stories 3 and 16. No e2e seam: no Player perceives this feature in production, and the feature sits outside `apps/e2e`'s purpose as the oracle for the Room. ## Implementation Decisions - **The runtime flag, `DEVTOOLS_ENABLED`.** It is on only when it is exactly `true`; any other value or no value means off, failing closed like an empty origin allowlist (ADR 0028). `loadConfig` reads it like the other API settings. It is set in the preview-only compose overlay, which `up.sh` already applies to `chess-pr-*` stacks only, on both the `app` and `seed` services, and in `apps/api/.env.local` through `pnpm env:sync`. Production's compose never sets it. There is no hostname check: the stack regex that picks the overlay is already what tells a preview from production. - **The seed module.** A new API entry point beside `migrate`, outside Nest, with the same env-file handling. It exposes the Demo Guests (label and secret) and the demo Games as data, plus a run function. That function refuses unless `DEVTOOLS_ENABLED` is on, naming the flag in its error, and upserts by fixed ids: Guests by their secret's hash, Games by a fixed id each. Scripts: `db:seed` (built, for the image) and a dev variant. The API's `dev` script runs the seed after `migrate:dev`. It prints the next steps: which Guests exist and how to switch to them. - **Demo Guests.** There are two, "Demo Guest A" and "Demo Guest B". Their secrets are fixed, readable 22-character base64url strings, the same shape a minted Guest has, so they stay valid if `GuestSchema` ever checks the format. They are public by design: only demo data is reachable with them. - **Demo Games.** About 12 for A and one for B, matching story 6. Each Game is a UCI line (ADR 0021) plus its ending, winner and draw reason, consistent with the `games` table's CHECK constraints. The lines that already exist in the Room fixtures (stalemate, threefold, insufficient material, fifty-move, Fool's mate) are the prior art, and moving them into a shared place is a fair prefactor. `startedAt` and `endedAt` are fixed, relative to a fixed date, never to now, so reruns stay idempotent. - **Preview deploy.** A one-shot `seed` service in the preview overlay runs the image's built seed. `up.sh` runs it after `migrate` and before `app` on preview stacks only, with its exit code failing the deploy, the same way it treats `migrate` (ADR 0032). `verify.sh` allows the `seed` container on previews, and checks that it exited cleanly. It also checks that `GET /rpc/devtools` answers 200 on a preview and 404 on production. - **Contract: a new `devtools` group** in `@chess/contracts` (ADR 0027), with one procedure, `GET /devtools`, whose output is `{ demoGuests: [{ label, secret }] }`. It sits under `/rpc`, so the origin guard applies (a GET without Origin passes, per ADR 0028). - **API: a `devtools` Nest feature module**, imported by the app module only when the config's flag is on. When it is off, the route does not exist and `/rpc/devtools` answers 404. The controller returns the seed module's Demo Guests. Nest stays out of the seed module (ADR 0027's layering). - **Front: the devtools ship in the build.** Set `removeDevtoolsOnBuild: false`. The root route no longer mounts the TanStack devtools unconditionally: it calls the probe once at boot, through the contract client and TanStack Query (ADR 0012). Only on a 200 does it dynamically `import()` the devtools and mount them with the Router, Query and Guest panels. On 404 it mounts nothing and downloads no devtools chunk. Local dev goes through the same probe, with no `import.meta.env.DEV` branch. - **Front: the Guest panel**, a TanStack devtools plugin. It shows the held Guest masked, "Switch to…" for each Demo Guest from the probe, and "Paste a secret". Both actions call `keepGuest()` and invalidate the Query cache, so the views refetch with the new `x-guest` header. There is no "Forget" and no "back to my Guest": a private window resets, and a local or preview Guest is throwaway. - **Glossary.** Add **Demo Guest**: a Guest whose secret is a fixed, public value, seeded only on stacks that enable devtools. - **New ADR: "devtools and demo data ship in the image, gated at runtime per stack"**, recording that the gate is the `DEVTOOLS_ENABLED` flag set by the preview overlay, never a build flag, because previews and production run one image. It is written with `domain-modeling` and added to the ADR index. ## Testing Decisions - **Prior art:** - the `rpc-games` and `rpc-game-reads` integration tests (in-process Postgres, `rpc.fixtures`) for seeding and reading back; - `room-decided.test` and the Room fixtures for the ending lines; - the config tests for the flag's parsing; - the front's games harness with MSW for the view tests (My games and the Game page tests). - **Unit (seed definition):** every demo Game's line replays through the engine to exactly its stored ending, winner and draw reason, and the set covers every ending in story 6. - **Integration:** - after the seed runs, Demo Guest A's `game.list` returns its Games in `endedAt` order, with the expected results; - B's Game is readable through `game.get`, and is absent from A's list; - running the seed twice leaves the same rows; - the seed refuses with the flag off, and with the flag set to anything but `true`; - `/rpc/devtools` answers the Demo Guests when the flag is on, and 404 when it is off. - **View:** - a 404 probe mounts no devtools; - a 200 probe mounts them with the Guest panel; - the panel masks the current Guest; - "Switch to…" and "Paste a secret" change the `x-guest` header of the next request, and My games refetches. - **Unhappy paths:** - a probe that errors (network, 500) behaves as off; - an empty pasted secret is refused; - the seed against a database with no migrations fails loudly, rather than half-seeding. ## Out of Scope - Recovering a real Guest's secret: impossible by design, since only its hash is stored (ADR 0034). - Any Guest in a URL (spec #355, story 2): no `?secret=` route. - Demo Accounts, claims and Sessions. - Seeding live Rooms or in-progress Games. - An e2e test. - Seeding production, or a production toggle for devtools. ## Further Notes - Tracked as three tickets delivered in one merge request through `implement-spec`: the seed, then devtools on previews, then the Guest panel. - Previews are public, so anyone can switch to a Demo Guest there. That is accepted: only demo data is reachable. - Watch `apps/e2e`: its `built` target reads `.env.local`, which will now turn devtools on. Check that the devtools trigger overlaps none of the elements the suite clicks. - The seed ships in the production image, because it is the same image, but it refuses to run without the flag, and no production step calls it.
root commented 2026-10-14 09:55:43 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #395

mentioned in issue #395
root commented 2026-10-14 09:55:49 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #396

mentioned in issue #396
root commented 2026-10-14 09:55:55 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #397

mentioned in issue #397
root commented 2026-10-14 09:56:02 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #395

marked this issue as related to #395
root commented 2026-10-14 09:56:04 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #396

marked this issue as related to #396
root commented 2026-10-14 09:56:06 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #397

marked this issue as related to #397
root commented 2026-10-14 10:13:39 +00:00 (Migrated from gitlab.core-project.space)

mentioned in merge request !522

mentioned in merge request !522
root (Migrated from gitlab.core-project.space) closed this issue 2026-10-14 11:15:25 +00:00
root commented 2026-10-14 12:43:33 +00:00 (Migrated from gitlab.core-project.space)

mentioned in merge request !523

mentioned in merge request !523
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
amaurycoudr/chess-project#394
No description provided.