Files
docs/website/DEMO_MODE.md
wtclaude c714906bbd docs(website): design demo mode for demo.runicgateway.com
The design for website#222, before any code: one DEMO_MODE flag on the
normal image that locks destructive writes with a 403 DEMO_LOCKED, four
published static logins, no outbound mail/push/Discord, a banner with the
next reset time, and a scheduled wipe on the demo-site runner (drop the
volumes, redeploy, seed accounts + the rig link + content).

Tier A locks are listed route by route. Nine questions for the org lead
(module lock declaration, the rig, visitor content, rig sharing with
walks, the stand-in Steam link, cadence, where the files live, version).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-23 17:43:52 -05:00

14 KiB

Demo mode — demo.runicgateway.com

Status: design, waiting on the org lead's answers to §9. Nothing is built. Issue: RunicGateway/website#222. Why now: Google Play (../android/PLAN.md item 9, M8) needs a public HTTPS site with working sign-ins for its reviewers and for the 12 testers who must be opted in for 14 days before the app can apply for production.

1. What the demo is

A public Runic Gateway deployment at demo.runicgateway.com, linked to the Oxide Rust rig, that anyone can sign into with published accounts. The org lead set three requirements on 2026-10-23:

  1. Static logins. Ready-made accounts (player, moderator, admin, and one for Google's reviewer) whose names and passwords are published.
  2. No destructive actions. Someone signed in as the public admin must not be able to break the site for the next visitor, or reach through it to damage the rig.
  3. An automatic wipe every couple of hours that brings the site back to a known base: the Rust module, the link to the rig, the demo accounts and the seeded content.

The demo-site Gitea Actions runner is where the demo is deployed and wiped. The runner is only the host for those jobs; the design below covers what runs there.

2. Principle: the same image, switched by one flag

The demo runs the normal website-app image and the normal Rust module release. Nothing is forked. A single env var on the deployment, DEMO_MODE=true, turns on three things:

  • a lock on a fixed list of write routes (§3), answered before the route's own handler runs;
  • a banner on every page and in the app saying this is a demo and when it next resets (§6);
  • no outbound side effects that reach a real person (§5).

With the flag unset, none of the demo code runs, so a normal shard is unaffected.

3. The lock

3.1 How it works

A core middleware, mounted ahead of every router when DEMO_MODE is on, compares each write request (POST/PUT/PATCH/DELETE) against a deny list of method + path patterns. A match answers:

403 { "message": "This is turned off on the demo site.", "code": "DEMO_LOCKED" }

The web client shows the message as a toast wherever an action fails with that code. The Android app maps the code to a new error kind with the same wording; today it would show the generic "Something went wrong", which is misleading for a reviewer.

Reads are never locked. Every page can be opened and every form filled in; only the save is refused. That keeps the demo useful as a tour of the admin panel.

Module routes need their own entries, because they are mounted by core but belong to the module. §9 Q1 asks how the module declares them.

3.2 What is locked

Paths are relative to /api/v1. Tier A breaks the site for the next visitor and is always locked. Tier B reaches the real game server, and Tier C is content anyone can see; both are §9 questions.

Tier A — always locked (core)

Area Routes What a visitor could otherwise do
Site mode PUT /admin/site-mode Put the site into maintenance; every non-admin is locked out
Settings & branding PUT /admin/settings, POST /admin/settings/brand-asset/:slot, DELETE /admin/settings/:key Rename or rebrand the site, swap the logo
Modules every write under /admin/modules (install, sources, restart, enable, disable, purge, delete) Disable or purge the Rust module; restart the server
SSO providers writes under /admin/auth/providers Add a sign-in provider pointing anywhere
Email PUT /admin/email/config, POST /admin/email/test, POST /admin/email/disconnect Connect a mail account; send mail from the site
Discord bot PUT /admin/discord-bot/config Point the bot at someone's server
Team voice writes under /admin/teams/voice Configure or sync Discord voice channels
Demo accounts, by an admin PUT/DELETE /admin/users/:id, DELETE …/trusted-devices…, POST …/mfa/reset when :id is a demo account Demote, disable or delete the account everyone signs in with
Own account, on a demo account PATCH /auth/me/account/username, …/password, …/email; POST /auth/me/account/totp/setup, …/enable, …/disable; POST /auth/me/account/recovery-codes/generate; DELETE /auth/me/account/identities/:provider Change the shared password, or turn on 2FA so nobody else can sign in
Other people's sessions DELETE /auth/me/sessions/:id for a session that is not the caller's Sign every other visitor out of the shared account
Password reset POST /auth/password/forgot, POST /auth/password/reset/:token Reset a demo account's password

Tier A — always locked (Rust module)

Area Routes Why
The link to the rig PUT/DELETE /admin/rust/servers/:id Repoint or delete the sidecar link; the site goes dark
Plugin config files POST /admin/rust/config/:serverId/file Writes a plugin's config file on the rig's disk

Tier B — reaches the rig (§9 Q3). Permission writes (policy, grant, revoke, groups, members, drift actions, sync), RunicNPC writes (factions, profiles, placements, push, respawn), economics and permission grants to a linked player, console-command steps and every other world action an event can run, and map fetch/render.

Tier C — content anyone can see (§9 Q4). Posts, wiki, pages, events, teams and team forums, moderation actions, invites, uploads, and the public contact form.

4. Static logins

Account Role Notes
demo-admin admin The admin panel, read in full; writes per §3
demo-moderator moderator Moderation queue, appeals
demo-player player Self-service; a linked Steam account so the Rust player screens have data (§9 Q6)
play-review admin Google's reviewer. Same powers as demo-admin, but a separate account, so a reviewer is never signed out because someone else revoked a session on the shared one

Usernames and passwords are published on the demo's sign-in page and on runicgateway.com /beta. They are not secrets. The seed recreates them at every wipe, so a password that somehow changed is back within one cycle. Public registration is closed on the demo: there is no mail to verify an address with (§5), and the static logins cover every role.

5. Outbound effects

Channel On the demo
Email Not configured. Registration closed, password reset locked, digest and engagement mail never send
Push (ntfy) Not deployed. The app's notification settings still save; nothing is delivered
Discord bot Not deployed
The rig Per §9 Q3

6. The banner

GET /api/v1/public/settings gains demo: { resetsAt } while the flag is on. The web client shows a thin strip on every page: "Demo site — anything you change is wiped at 14:00 UTC. Sign-ins: demo-admin / demo-player …". The app shows the same strip on Home. Both read resetsAt, so neither needs to know the schedule.

7. The wipe

A scheduled workflow on the demo-site runner (§9 Q7 for the cadence) runs:

  1. docker compose down -v — drops the database and the uploads volume, so nothing a visitor made survives.
  2. docker compose pull && docker compose up -d — the app boots, creates the schema, and installs the Rust module from MODULES=rust@<version>=<manifest> in .env.
  3. Wait for the app's healthcheck.
  4. docker compose exec app npm run seed:demo — one core script that, from a fixture file in the deploy directory:
    • creates the four accounts (§4);
    • creates the Rust server row with the rig's sidecar URL and token taken from .env, so the link survives every wipe without being stored in the fixture;
    • writes the seeded content: a few news posts, wiki pages, an upcoming event and a team, enough for every screen to have something on it;
    • stamps resetsAt for the banner.
  5. Smoke check: GET /api/v1/public/status answers, and the Rust server reports a live connection.

A wipe takes the site down for about a minute. Signed-in visitors, including app users, are signed out: their sessions and refresh tokens were in the database.

The same workflow runs on a push that changes the deploy directory and on workflow_dispatch, so a redeploy is the same path as a wipe.

8. Where it lives

Piece Repo
DEMO_MODE lock, banner field, seed:demo script website (core)
The Rust module's own lock entries Module-Rust (§9 Q1)
DEMO_LOCKED error kind and the banner in the app Android-app
docker-compose.yml, .env.example, seed fixture, wipe workflow §9 Q8
.env (JWT secret, encryption key, the rig's sidecar token) On the demo-site host only, never in a repo

9. Questions for the org lead

Q1 — How does the Rust module declare its locked routes? Example: the demo is up and demo-admin opens Rust → Servers → Edit and changes the sidecar URL. Something has to know that route is locked.

  • (a) Recommended: module.json gains an optional demoLocks list of method + path, read by core's lock. A module with no list is unaffected. It is a new manifest field, so MODULE_API gets a minor bump and the Integration Kit documents it.
  • (b) Core hard-codes the Rust module's paths. No module change, but core then knows one module's routes, which the module system was built to avoid.

Q2 — Is the Tier A list (§3.2) complete, and is anything on it you want left open? Example: should demo-admin be able to change the site's theme colours, which are under Settings? It is harmless and resets at the wipe, but it is also the first thing a visitor would deface.

Q3 — What may a visitor do to the rig? Example: demo-admin grants demo-player a kit permission, or starts an event whose step runs server.time 0 on the Oxide rig. The website wipe does not undo either; the rig keeps them.

  • (a) Recommended: lock everything that writes to the rig (all of Tier B). Visitors see the live map, feed, players and every admin screen, but saves that would reach the rig are refused.
  • (b) Allow a short list of harmless, self-healing actions — for example respawning a RunicNPC or rendering the map — and lock the rest.
  • (c) Allow everything and also reset the rig in the wipe. Much larger: a rig wipe means permissions, NPC placements and plugin state, and a Rust server restart.

Q4 — May visitors publish content? Example: someone signs in as demo-admin and posts a news item with an offensive image or a phishing link. It is public on demo.runicgateway.com until the next wipe.

  • (a) Recommended: allow posts, wiki, pages, events and teams, so the demo shows the real editors. Lock uploads (no hosting of arbitrary files on our domain) and the contact form. Serve the demo with noindex, so nothing a visitor writes reaches search engines.
  • (b) Lock all publishing as well; the editors open but every save is refused.
  • (c) Allow uploads too, with the wipe as the only cleanup.

Q5 — Which rig does the demo use, and what happens to our walks on it? Example: next week a walk needs the Oxide rig while the demo is linked to it. The Rust module's site-owned permission sync means two sites linked to one rig fight over its permissions.

  • (a) Recommended: the Oxide rig becomes the demo's rig; walks use the Carbon rig.
  • (b) A third rig for the demo.
  • (c) Share it, with permission sync off on the demo.

Q6 — Does demo-player have a linked Steam account? Example: the player's Rust screens (stats, linked accounts, economics) are empty without one.

  • (a) Recommended: the seed links a stand-in Steam ID that has played on the rig, so the screens have data. It is not anyone's real account.
  • (b) Leave it unlinked, and show the empty states.

Q7 — How often does it wipe?

  • (a) Recommended: every 2 hours, on the hour (00:00, 02:00, … UTC). Each wipe signs everyone out, which a reviewer in the middle of a review may notice.
  • (b) Every 6 hours, which leaves visitor changes up for longer.

Q8 — Where do the deployment files live?

  • (a) Recommended: website/deploy/demo/ (compose file, .env.example, seed fixture) and website/.gitea/workflows/demo-wipe.yml. The seed script ships in the image, so its fixture sits next to it.
  • (b) A new repo, RunicGateway/demo-site, which keeps the website repo free of one deployment's files.

Q9 — Which version does the demo run?

  • (a) Recommended: whatever website main and the latest Rust module release are at each wipe, so the demo always shows the current release.
  • (b) Pinned versions in .env, moved by hand.

Not asked, assumed: the host's reverse proxy and DNS for demo.runicgateway.com, and that the demo-site host can reach the Oxide rig's sidecar over the network. These are for the org lead to confirm, not to decide.

10. Build order, once §9 is answered

  1. Core (website): the DEMO_MODE lock and its tests, the banner field, seed:demo, and the OpenAPI note that a route may answer DEMO_LOCKED. One PR.
  2. Module (Module-Rust): its lock entries (Q1), plus a kit note if the manifest changes. One PR.
  3. Clients: the web banner and toast (in the core PR), and the DEMO_LOCKED error kind and banner in Android-app. One PR.
  4. Deployment: compose, fixture and wipe workflow (Q8); first deploy on demo-site; a walk of every static login and every Tier A lock, in the browser and the app. One PR.
  5. Play: the sign-in details in the Play Console point at play-review; runicgateway.com /beta publishes the logins.