Raxx · internal docs

internal · gated

Principle: Staging Is a Runtime Duplicate, Not a Parallel Admin Plane

Status: Adopted
Date: 2026-05-10 UTC
Closes: #1581
Refs: PR #1579 (env-gate stopgap), PR #1580 (RBAC V2 schema add), PR #1582 (toggle taxonomy)
Supersedes (partial): project_environments_mental_model.md — staging = playground for code, NOT for prod-affecting admin


The Principle

Staging is a runtime duplicate of production: same code, same config shape, same infra topology. It exists so code changes can soak before they touch real users. It is not a second admin plane. Staging operators can observe staging state and exercise staging infrastructure, but they cannot queue, approve, or trigger actions that land in production. The boundary between staging and prod is a hard write-gate, not a UI affordance. Any console feature that renders prod-affecting controls on console-staging.raxx.app is a design defect, not a missing permission check.

"Staging is usually just a runtime dup of production — we shouldn't have too much reliance there. Wiring these together shouldn't be a coding project." — Operator, 2026-05-10 UTC


What This Means Concretely

console-staging.raxx.app — allowed / not allowed

Allowed Not allowed
Read staging flags, staging token state, staging audit log Submit a flag promotion that targets prod
Trigger staging rotations (rotate staging Heroku key) Approve or reject a promotion with target_app = prod
View customer records that exist in the staging database Access, modify, or view real customer records from prod
Test new console features end-to-end Execute any workflow that calls a prod API, prod vault path, or prod Heroku app
Reproduce a reported bug against staging data Initiate a scheduled job that writes to prod infrastructure

console.raxx.app (production) — allowed

Everything in staging's allowed column, scoped to prod infrastructure, plus all prod-affecting write actions (flag promotions, live rotations, customer admin).

The clear line

Prod-affecting actions are only reachable from console.raxx.app.
If an operator is on console-staging.raxx.app and believes they are taking a prod action, the system has failed them. The env is hostname-derived and immutable (retired switcher: project_console_env_switcher.md). Display the env-pill clearly on every page. Gate every write path on active_env.


The RBAC Primitive Shape

Roles follow <app>-<resource>-<level> (RBAC V2, rbac-design.md). Staging-scoped groups compose differently from prod-scoped groups — this is the mechanism that enforces the write-gate without custom code per feature.

staging-admins group
  → console-flag-reader       (read-only on flags, any env)
  → console-token-user        (read tokens)
  → console-audit-user        (read audit log)
  # NOT console-flag-admin — staging admins never hold the write role

production-admins group
  → console-flag-admin        (read + promote + approve flags)
  → console-token-admin       (read + rotate tokens)
  → console-audit-user
  → console-invite-admin

An operator onboarded to staging-admins only gets reader roles. The write roles (console-flag-admin, console-token-admin) are assigned exclusively in groups that are gated to active_env = prod. RBAC V2 schema (#1580) adds the env_scope column to console_admin_group_roles to make this assignment explicit at the data layer rather than in application logic.


Bootstrap Is Config, Not Code

Adding a new staging admin should take under 60 seconds with no engineering involvement:

heroku config:set ADMIN_EMAIL=<email> --app raxx-console-staging >/dev/null 2>&1
# Console reads the config and auto-enrols the address into staging-admins
# on first passkey registration. No migration, no code ship.

The same is true for a new prod admin via raxx-console-prod. Role assignment flows from group membership, which is config-driven. The RBAC group seed (migration 0021, PR #1500 RV-1) provisions staging-admins and production-admins at deploy time. No bespoke admin-provisioning code path.


Designer / Architect Checklist

When designing any new console feature that touches prod-affecting state:


Anti-Patterns

1. Both consoles showing an identical "pending prod promotion" queue.
The case that triggered this principle. A staging operator sees the same list of pending promotions as a prod operator, including items awaiting prod approval. Staging has no business displaying pending prod promotions — it cannot act on them and the display creates a false sense of authority. Fix: target_app column on the queue table scoped at query time to the current env. PR #1579 (env-gate stopgap) addresses this directly.

2. UI elements that imply a prod action is reachable from staging.
A "Promote to prod" button that is visible but disabled on staging is still wrong. The button implies the action is locally available — the operator learns the wrong mental model ("I need a different permission" instead of "I need to be on the prod console"). Remove the button entirely when active_env != "prod".

3. Scheduled jobs in the staging console that touch prod state.
A cron digest or rotation sweep that runs inside raxx-console-staging must not write to raxx-console-prod's database, vault paths, or Heroku apps. These jobs share code — env scope is determined by which app the job runs in, verified via active_env. If the job does not check active_env before every write, it is a latent cross-env contamination risk.


Example Applications

Flag promotion queue (lit example — #1579 stopgap → #1580 long-term)

Before this principle was written, console_flag_promotions had no target_app column. Both consoles rendered all pending promotions. Staging operators could see — and in some configurations act on — prod-targeted items.

Customer admin (#1044 / #1045 / #1047)

console-staging.raxx.app can VIEW customers who exist in the staging database (seeded test accounts, QA fixtures). It must never display, search, or modify real customer records from production. The data model for customer admin uses separate DB connections per env — there is no shared customer table between staging and prod. Any feature that joins across envs (e.g., "show me if this email exists in prod") is a design violation of this principle and must be escalated before implementation.


Reference

Artifact Role
project_staging_is_runtime_dup.md (memory) Operator framing that drove this principle
project_console_env_switcher.md (memory) Env-switcher retired; env is hostname-derived
project_rbac_v2_decisions_2026_05_09.md (memory) RBAC V2 decisions; env_scope column
project_rbac_as_feature_dispatch.md (memory) Role/permission flip as config, not code
project_environments_mental_model.md (memory) Partial supersede: staging = code playground, not prod-affecting admin
project_staging_scope.md (memory) Staging only exists for console + api
docs/architecture/rbac-design.md RBAC V2 role taxonomy + group composition
docs/architecture/console-env-isolation.md Vault path env-scoping (complements this principle)
ADR-0020 Manual prod deploys via workflow_dispatch — same intentional-friction spirit
PR #1579 Env-gate stopgap on flag promotion queue (in flight)
PR #1580 RBAC V2 schema add — env_scope column on group roles (queued)
PR #1582 Toggle taxonomy — independent but topical