Owner: Operator (Kristerpher) + agents Last updated: 2026-05-03 Anchor epic: #871 Anchor card (this doc): #877
The Raxx Console operates in a "two cockpits, one cabin" model — console-staging is the daily-driver, console-prod is the wall-display backup, and there is no session-level env switcher. The host you visit determines the env. This document captures that model + the promotion flow that replaced the old dropdown.
If you reach for the old "switch env" dropdown — it's gone. See §3 for the replacement flow.
Two separate Heroku apps:
| URL | Heroku app | Postgres DB | Purpose |
|---|---|---|---|
console.raxx.app |
raxx-console-prod |
d860pa1ulpacce |
Production cockpit (wall display, attestation surface) |
console-staging.raxx.app |
raxx-console-staging |
d76no3asiepgbk |
Daily-ops cockpit (where operators live) |
Both apps deploy the same code from main. Staging auto-deploys on every push to main. Prod requires explicit dispatch (gh workflow run deploy-console.yml -f environment=production) OR the console version manager (§3).
Both use separate Postgres add-ons on the same RDS cluster — different logical DBs, no cross-env data leak. The "Recent rotations" / "Recent deploys" panels query their own app's DB only.
Verified the separate-DB model on 2026-05-03 03:50 UTC: heroku config:get DATABASE_URL on each app returns different DB names.
The banner above the dashboard reads PRODUCTION (red) or STAGING (purple). It is derived from the hostname at request time — request.host matches against the known mapping. The pill is not clickable. There is no dropdown.
Implementation:
- console/app/middleware/env_guard.py writes g.env from request.host (the Flask request hook ships in PR #919; was previously read from a session-stored value).
- The audit envelope still carries selected_env for backward compatibility; the value is the hostname-derived env, not a session toggle.
If you want to operate against the other env, open a different browser tab to the other host.
The previous behaviour: env-switcher dropdown changed your session env, then "Deploy" routed to whatever surface in that session. Removed in PR #919 because surface_id alone determines target env (verified by PR #889 / #873 — every entry in SURFACE_WORKFLOW_MAP carries an explicit target_env).
The new behaviour:
console-staging.raxx.app/admin/console-versions (in flight as #874 / #875 — see §6 for current break-glass path)PROMOTE to confirm + 6-digit TOTP codemain SHA staging is runningconsole.version.promoted with {from_sha, to_sha, requested_by_admin_id, promoted_at_utc} (sub-card #876)Until #874 / #875 ship, use the break-glass path in §6.
Each surface_id has a fixed workflow + target env baked into console/app/services/deploys.py:SURFACE_WORKFLOW_MAP. Six current surfaces:
| surface_id | workflow | target_env | Heroku app |
|---|---|---|---|
api-prod |
deploy-heroku.yml |
production |
raxx-api-prod |
api-staging |
deploy-heroku.yml |
staging |
raxx-api-staging |
console-prod |
deploy-console.yml |
production |
raxx-console-prod |
console-staging |
deploy-console.yml |
staging |
raxx-console-staging |
getraxx |
deploy-customer-docs.yml |
production |
CF Pages (raxx-getraxx) |
raxx-mockups |
deploy-customer-docs.yml |
production |
CF Pages (raxx-mockups) |
The map is the canonical env-binding authority (PR #889 #890). The probe layer queries /actions/workflows/{file}/runs filtered by workflow file + dispatch input environment to pin builds to the right tile.
Every console action writes to console_audit_log (the per-app DB; see §1 about per-env separation). Three event families relevant to deploy + promotion:
console.deploy.intent — modal-click → POST /api/internal/deploys → row createdconsole.deploy.callback — GH workflow's notify-deploy-status action → POST /api/internal/deploys/<id>/callback → status transitionconsole.version.promoted — version-manager promote click (sub-card #876)Break-glass deploys (§6) write console.deploy.intent.breakglass with the actor's email captured at dispatch time.
When the console version manager is down, locked, or you need to deploy a specific non-main ref:
# Deploy console code (this app) to prod
gh workflow run deploy-console.yml -f environment=production -f ref=main
# Deploy console code to staging (rare — staging auto-deploys on push)
gh workflow run deploy-console.yml -f environment=staging -f ref=main
# Deploy backend (api) — same workflow_dispatch pattern
gh workflow run deploy-heroku.yml -f environment=production -f ref=main
If HEROKU_API_KEY is rejected as invalid in the workflow output, you have token drift — see heroku-api-key-drift-recovery.md.
Console code promotion (this doc) — staging-SHA → prod-SHA via the version manager.
Console feature flag promotion (#798 / /flags/promotions) — separate flow. Each flag has its own staging-on / prod-on state. Flag flips happen WITHIN an env, not across envs. Do not conflate.
The two flows: - Code promotion uses the deploy workflow (Heroku push), updates the running binary - Flag promotion uses Heroku config var writes, takes effect on next request
If you want to flip a flag in prod, you don't need a code promotion — just toggle the flag in the flag manager.
| Switch | Where | Effect |
|---|---|---|
FLAG_CONSOLE_DEPLOY_UI |
Heroku config | Disables the entire deploy modal flow; break-glass only |
FLAG_CONSOLE_DEPLOY_ASYNC |
Heroku config | When true, dispatch returns 202 immediately + lazy run_id resolution. When false, falls back to synchronous 30s run_id poll (the H12 risk path, default until staging soak passes) |
FLAG_CONSOLE_DEPLOY_CHIP |
Heroku config | Cross-page in-flight deploy indicator (#892) |
FLAG_CONSOLE_ROTATE_UI |
Heroku config | Enables the rotate-from-console button on /secrets rows |
FLAG_CONSOLE_ROTATION_MODE_A |
Heroku config | Per-secret Mode A handler dispatch (auto-rotate vs manual SOP) |
Toggle via heroku config:set <flag>=true -a <app> >/dev/null 2>&1 (always silence stdout — heroku config:set echoes the secret in unredacted form by default; SECRET_KEY leaked from a default echo on 2026-05-01).
heroku-api-key-drift-recovery.mdcf-access-service-token-provisioning.md