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
This commit is contained in:
236
website/DEMO_MODE.md
Normal file
236
website/DEMO_MODE.md
Normal file
@@ -0,0 +1,236 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user