Current. The one place that says how to reach staging, what is on it, and how it stays in sync with main.
Staging is the Supabase project brifeyypjauyusmkptkx (named "DebateOwl Staging" in the dashboard). Production is uziwdtkzofptihahfooo ("DebateOwl"). Scripts identify projects by ref only; the names are display labels and can change. Staging has its own users, data, and secrets. Nothing on staging can read or write production.
On your phone or laptop, for a PR. A pull request does not get a Vercel preview unless someone requests one (operations.md). The preview to open is debatelab-staging: it is pinned to staging, whatever the browser's saved toggle says. Previews sit behind Vercel login, so sign in to Vercel once on each device. The permanent staging site is https://debatelab-staging.vercel.app, built from main.
Sign in. Two ways:
- Adam: Continue with Google with your own account. The seed made that account an admin on Pro with three debates.
- Agents and tests: a staging demo code. The seeded Pro debater's code is
STAGING_SEED_PRO_CODEin~/.config/debatelab/staging-access.envon the VPS (mode 600). Never paste it into GitHub, docs, or screenshots.
A production account, demo code, or email code does not work on staging, and a staging one does not work on production.
Without a PR. npm run dev:preview runs your working tree pinned to staging on the tailnet (development.md). Use it for uncommitted changes.
node scripts/staging/seed.mjs creates invented accounts and content. Every row has a fixed id, and a rerun restores the seed without duplicating it. The sync reruns the seed after each new commit on main.
| Account | Plan | Content |
|---|---|---|
seed-admin@staging.debatelab.test |
Pro, admin | none; creates the demo code |
seed-pro@staging.debatelab.test |
Pro | 3 debates, demo code STAGING_SEED_PRO_CODE |
seed-trial@staging.debatelab.test |
Trial, 5 days left at seeding | 1 debate |
seed-expired@staging.debatelab.test |
Expired trial | 1 debate |
STAGING_OWNER_EMAIL (Adam's Google address) |
Pro, admin | 3 debates |
The debates cover private cars in city centres, platform liability for misinformation, and standardised testing. Each has two or three sources and two or three saved results. Between them they cover cases, attacks, brainstorms, and factoids, and the liability debate also has an Ask chat. Seeded ids start with 5eed. The fixtures live in scripts/staging/fixtures.mjs; all of the text is made up.
A demo code turns its account into lifetime Pro when used, so the trial and expired accounts have none. To try those plans:
- Adam, on any device:
node scripts/staging/seed.mjs --owner-tier trial(orexpired) on the VPS, then reload. The next seed run, at the latest the next merge tomain, puts the account back on Pro, or run--owner-tier pro. - Agents:
node scripts/staging/seed.mjs --session trial > "$tmp/session.json"prints a session for any seeded account (admin,pro,trial,expired,owner) without sending mail. Put it in thedebate-auth-staginglocalStorage key before the app loads, ase2e/staging/auth.setup.tsdoes. It holds live tokens: keep it in a mode-600 file and delete it afterwards.
Staging deliberately has no Stripe or ElevenLabs secrets. Checkout, the billing portal, stripe-webhook, and the Stripe part of account deletion fail on staging, and voice rounds fall back to the browser voice. AI requests and web research use staging's own OpenRouter and Exa keys.
A user systemd timer on Adam's VPS, debatelab-staging-sync.timer, runs ops/staging-sync/run.sh every 5 minutes. When origin/main has a commit it has not synced, it:
- checks that commit out in a dedicated clone at
~/ops/debatelab-staging-sync/repo(it never runsnpm install); - runs
scripts/staging-sync.mjs, which refuses unless the Supabase CLI can seebrifeyypjauyusmkptkx(by ref, so renaming the project in the dashboard doesn't break it) andSUPABASE_PROJECT_ID, if set, is that same ref, passes--project-ref brifeyypjauyusmkptkxto every CLI call, applies pending migrations, deploys new or changed Edge Functions except the hiddenworkspace-invites, and then checks that nothing is left pending. A function counts as changed when git has changed it since its last deploy, or when the source staging runs (fromsupabase functions download) differs byte for byte from the commit. The second check catches an older checkout deployed by hand; - reruns the seed;
- sets the
staging-synccommit status on that commit, and opens or comments on the GitHub issue Staging is out of sync with main when anything fails. A later successful sync closes the issue.
An already-synced commit is rechecked hourly, so drift from a hand deploy also shows up. A failed commit is retried hourly. The script stops instead of pushing when staging has a migration the commit lacks. It never deletes a function: one that is deployed on staging but gone from the commit fails the sync until someone runs supabase functions delete <name> --project-ref brifeyypjauyusmkptkx. workspace-invites is the exception, because it is hidden and never deployed.
Protected staging E2E waits for a successful staging-sync status on its commit before testing, and the timer holds any sync while a browser job is running. It runs daily and on demand, on main's head. If main moves past the commit while it waits, the run skips: staging then runs the newer backend, which the next daily run or a dispatch tests. If no status arrives within 20 minutes, it fails with a pointer to this page.
PRs that change supabase/migrations/ or supabase/functions/ get a comment saying their staging preview still runs main's backend. Those changes reach staging only after merge. To try them before then, use the verify-debatelab skill against a local stack.
The timer uses the Supabase CLI login already on the VPS (~/.supabase/access-token). It is a classic personal access token for Adam's account, so it reaches production too. The scripts' ref checks keep the sync itself on staging, but they are not a security boundary. Anyone who can merge to main can change those scripts, and the timer will run the new code with that token within 5 minutes. A malicious or broken merge could read or change production, including its database and secrets. Today that is the same trust as the other agents on the VPS that use this token, and only Adam merges to main.
To shrink it, replace the token with a Supabase scoped token limited to the staging project brifeyypjauyusmkptkx (Database and Edge Functions read-write) and pass it to the service as SUPABASE_ACCESS_TOKEN. Then a bad merge can touch staging only.
systemctl --user list-timers debatelab-staging-sync.timer # next run
journalctl --user -u debatelab-staging-sync -n 100 --no-pager # last runs
systemctl --user start debatelab-staging-sync.service # sync now
node scripts/staging-sync.mjs --dry-run # what a sync would do, from your checkout
npm run backend:status -- --project-ref brifeyypjauyusmkptkx # read-only drift report
bash ops/staging-sync/install.sh # after changing run.sh or the unitsThe unit's PATH names ~/.nvm/versions/node/v24.20.0/bin, where node and supabase live. If nvm drops that version, the service fails before it can report, so update the unit and rerun install.sh. State lives in ~/ops/debatelab-staging-sync/state/ (last-ok, last-failed); delete a file to force a rerun.
- Production is never a fallback. When staging access fails, stop.
- Staging browser tests create uniquely named data and delete only what they created (testing.md).
- To run the staging browser suite locally, use the seeded Pro code as
STAGING_DEMO_CODE, and rerun the seed afterwards to restore that account's Current Debate. The CI code exists only as a GitHub secret. The copy in~/.config/debatelab/staging-e2e.envexpired on 2026-09-10. VERCEL_STAGING_BYPASS(GitHubstagingenvironment, and the VPS access file) lets CI and agents open protecteddebatelab-stagingpreviews with thex-vercel-protection-bypassheader. Treat it like the demo codes.