Web platform (site + admin)
How the player site and the staff console run — ports and commands, the local Supabase database, where each kind of data lives, caching and instant publishing, the security model, the AI bug-fix pipeline (Mac first, cloud when the Mac is off), remote access from a phone, the test commands and the known limits.
Two Next.js apps sit next to the wiki in this monorepo (ADR-025): apps/site (the
player website) and apps/admin (the staff console, "Caravan Command"). They share one local Supabase database in
Docker (packages/db), a UI package (packages/ui) and the canonical game data. The wiki is unchanged and stays the
source of truth for the game.
| App | Dev | Tests / production build | Notes |
|---|---|---|---|
| Wiki | 4310 | 4311 | static export, unchanged |
| Site | 4340 | 4341 | Node server (instant publishing) |
| Admin | 4350 | 4351 | dynamic, installable app (PWA) |
| Supabase API · DB · Studio | 56321 · 56322 · 56323 | — | pnpm db:status |
| Mailpit (local e-mail) | 56324 (SMTP 56325) | — | every e-mail lands here locally |
◆Start everything (owner runbook)
pnpm web:preflight— checks ports and Docker memory (the stack needs about 1 GB on top of the other two stacks).pnpm db:start, thenpnpm db:env— writes the.env.localfiles for the site, admin and worker from the running stack. It never overwrites a value you filled in yourself, and the site's file never gets the service key.pnpm db:seed:staff(nine test staff, one per role, local only) andpnpm db:seed:demo(generated demo players, security events and economy; see Demo data).- Your own owner account:
pnpm admin:bootstrap-owner(asks for your e-mail, name and a password, never shown; at your first sign-in the console makes you set up an authenticator app). pnpm site:devandpnpm admin:dev; optionallypnpm admin:worker(AI jobs, site refresh retries, phone alerts).
pnpm db:reset rebuilds the database from migrations and seeds; it refuses while a real (non-test) staff account
exists, unless you pass --force after a backup.
◆Where each kind of data lives
| Data | Home | Changed by |
|---|---|---|
| Game rules and content (skills, items, world, rulesets…) | JSON in git (packages/game-data/data), shown by the wiki | agents and the owner, through tasks |
| What the site may show of the game | allow-listed export (packages/game-data/public-data), no proposed records, no tuning numbers | pnpm data:public |
| News, FAQ, legal, status, site settings | Supabase cms | admin → Website |
| Sign-ups, newsletter, support requests, bug reports | Supabase community, quality | site forms (through checked functions only) |
| Staff, roles, audit log, approvals, notifications | Supabase admin | admin |
| Live switches, flags, ruleset overrides, releases | Supabase ops | admin → Live controls, Config, Releases |
| AI fix jobs | Supabase ai + branches and pull requests on GitHub | admin → Bugs / AI jobs, the worker |
| Site analytics (first-party, no cookies) | Supabase analytics | the site |
| Player accounts, characters, items, receipts, rankings | the Rust gateway's own database (M6) | the game; until then the admin shows demo data |
◆Instant publishing and caching
Public pages are cached ("use cache" with tags) and refreshed the moment something changes:
- Any change that is or becomes public (a post, FAQ, settings, status, a site switch) writes its cache tags to the
cms.revalidation_outboxtable in the same transaction. - Right after the change, the admin drains the outbox to the site's signed webhook (
/api/revalidate, HMAC withREVALIDATE_SECRET, at most 50 tags per request, batch after batch until empty). - The site expires those tags immediately. If the site was down, the rows stay queued with backoff; the worker retries every minute and Health shows the backlog.
| Cache profile | Fresh for | Kept at most | Used by |
|---|---|---|---|
cms | 5 min | 1 h | news, patch notes, FAQ, legal |
settings | 2 min | 15 min | main button, banner, sign-ups, routes, platforms |
status | 30 s | 2 min | status page (streams in at request time) |
Site maintenance (Website → Site settings) is re-read by the site's edge proxy at most every 30 s; while it is on, every page except status, legal and support answers 503 with Retry-After.
◆Security model
| Who | Credential | Can |
|---|---|---|
| Visitors | anonymous key | call public read functions only (api schema); no table access anywhere |
| Site server | site_server token | write only through rate-limited functions: sign-ups, support and bug reports, analytics |
| Staff | session cookie set by the admin server (the browser never talks to Supabase) | read and act within their role, enforced again by the database (row-level security + permission checks in every function) |
| Tools, seeds, tests | service key (server side only, never in the site) | everything, audited as "system" |
| Worker | job_worker token | claim AI jobs, deliver site refreshes, push alerts |
- 2-step sign-in for every staff member; critical actions (bans, exports, rule tiers, settings, grants) also need an authenticator check within the last 10 minutes, a reason and, for the most destructive ones, typed confirmation.
- Audit log: every change and every sensitive read (e.g. revealing a player's e-mail) is written with who, when, why and the before/after, in an append-only hash chain (Audit log → Verify chain).
- Two-person rule: approvals (GM grants, compensation, AI fix jobs) are decided by someone else; the owner may decide alone as the solo operator.
- Privacy: player e-mails are masked unless revealed with a logged reason (
players.read_pii); the site sets no cookies, respects Do Not Track / GPC and keeps only daily hashed visitor ids. - Leaks:
pnpm site:leakscanchecks the built site for internal names and secrets;pnpm web:secrets-scanchecks every tracked file.
◆Demo data
The Players, Security and World modules read the game_demo schema until the gateway exists. Rows come from
pnpm db:seed:demo (deterministic, built from the game's own ids: builds, zones, bases, quests and the rules in
security.json). Every demo screen says so. The actions are real functions with permissions, reasons and audit, so
the same screens switch to the gateway later through one adapter (apps/admin/lib/gameops.ts).
◆AI bug-fix pipeline
- On a bug, Fix with AI creates a job waiting for approval; the owner gets a phone notification.
- After approval the job goes to the Mac worker when its heartbeat is less than 90 s old, otherwise to the
cloud (GitHub Actions,
.github/workflows/ai-fix.yml, a pinned Claude Code action). - The agent works in its own branch with Zoen's rules, treats the bug report as untrusted data, runs the tests, pushes the branch and opens a pull request. It never merges. Steps, files and cost are recorded on the job.
- A kill switch and turn/budget caps live in AI settings.
Owner setup (nothing runs without it): CLAUDE_CODE_OAUTH_TOKEN (from claude setup-token) or ANTHROPIC_API_KEY,
a fine-grained GITHUB_TOKEN (contents, pull requests, issues, actions on the repository) in apps/admin/.env.local,
and the same token as a repository secret for the cloud path. Then pnpm ai:smoke.
◆Remote access (phone)
Run the admin on the Mac and share it privately with Tailscale (tailscale serve), then set ADMIN_PUBLIC_URL
and ADMIN_ALLOWED_ORIGINS to that address. On the phone, open it and Add to Home Screen: the console installs as an
app and can receive approval notifications. When the Mac is off, approvals still work from the GitHub mobile app by
labelling the mirrored issue ai-fix.
◆Tests
| What | Command |
|---|---|
| Database rules (pgTAP) | pnpm db:test |
| Public data is fresh / allow-listed | pnpm data:public:check · pnpm data:test |
| Site (desktop, tablet, phone, accessibility) | pnpm site:e2e |
| Admin (every module, all sizes, role × module matrix) | pnpm admin:e2e |
| Site + admin together (publish, switches, maintenance, bug intake) | pnpm web:e2e:integration |
| Performance (Lighthouse, mobile) | pnpm site:lighthouse |
| AI worker (prompt fence, fake end-to-end) | pnpm ai:test |
| Leaks and secrets | pnpm site:leakscan · pnpm web:secrets-scan |
| Wiki untouched | pnpm web:guard · pnpm wiki:test |
◆Known limits and follow-ups
- Largest paint on phones: Lighthouse scores 91–99 (performance) and 100 elsewhere, but the simulated slow-4G largest paint is 2.2–3.5 s against the 2.5 s budget on the local HTTP/1.1 server. Re-measure on HTTP/2 hosting.
- Hosting is not chosen yet (OPEN-4): the site needs a Node host; Supabase moves to the cloud at the same time.
- Rate limits use the first
X-Forwarded-Foraddress; behind a proxy, make sure the proxy overwrites that header. - Docker publishes the local Supabase ports on all interfaces; keep the macOS firewall on (or bind to localhost).
- Legal pages (privacy, terms, cookies) are drafts until a legal review.
Source: zoen/docs/tech/WEB_PLATFORM.md · 1,523 words · edit the Markdown, not this page.
