Game guide · source of truth
Tech

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.

AppDevTests / production buildNotes
Wiki43104311static export, unchanged
Site43404341Node server (instant publishing)
Admin43504351dynamic, installable app (PWA)
Supabase API · DB · Studio56321 · 56322 · 56323—pnpm db:status
Mailpit (local e-mail)56324 (SMTP 56325)—every e-mail lands here locally

◆Start everything (owner runbook)

  1. pnpm web:preflight — checks ports and Docker memory (the stack needs about 1 GB on top of the other two stacks).
  2. pnpm db:start, then pnpm db:env — writes the .env.local files 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.
  3. pnpm db:seed:staff (nine test staff, one per role, local only) and pnpm db:seed:demo (generated demo players, security events and economy; see Demo data).
  4. 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).
  5. pnpm site:dev and pnpm admin:dev; optionally pnpm 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

DataHomeChanged by
Game rules and content (skills, items, world, rulesets…)JSON in git (packages/game-data/data), shown by the wikiagents and the owner, through tasks
What the site may show of the gameallow-listed export (packages/game-data/public-data), no proposed records, no tuning numberspnpm data:public
News, FAQ, legal, status, site settingsSupabase cmsadmin → Website
Sign-ups, newsletter, support requests, bug reportsSupabase community, qualitysite forms (through checked functions only)
Staff, roles, audit log, approvals, notificationsSupabase adminadmin
Live switches, flags, ruleset overrides, releasesSupabase opsadmin → Live controls, Config, Releases
AI fix jobsSupabase ai + branches and pull requests on GitHubadmin → Bugs / AI jobs, the worker
Site analytics (first-party, no cookies)Supabase analyticsthe site
Player accounts, characters, items, receipts, rankingsthe 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:

  1. Any change that is or becomes public (a post, FAQ, settings, status, a site switch) writes its cache tags to the cms.revalidation_outbox table in the same transaction.
  2. Right after the change, the admin drains the outbox to the site's signed webhook (/api/revalidate, HMAC with REVALIDATE_SECRET, at most 50 tags per request, batch after batch until empty).
  3. 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 profileFresh forKept at mostUsed by
cms5 min1 hnews, patch notes, FAQ, legal
settings2 min15 minmain button, banner, sign-ups, routes, platforms
status30 s2 minstatus 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

WhoCredentialCan
Visitorsanonymous keycall public read functions only (api schema); no table access anywhere
Site serversite_server tokenwrite only through rate-limited functions: sign-ups, support and bug reports, analytics
Staffsession 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, testsservice key (server side only, never in the site)everything, audited as "system"
Workerjob_worker tokenclaim 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:leakscan checks the built site for internal names and secrets; pnpm web:secrets-scan checks 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

  1. On a bug, Fix with AI creates a job waiting for approval; the owner gets a phone notification.
  2. 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).
  3. 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.
  4. 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

WhatCommand
Database rules (pgTAP)pnpm db:test
Public data is fresh / allow-listedpnpm 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 secretspnpm site:leakscan · pnpm web:secrets-scan
Wiki untouchedpnpm 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-For address; 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.