Architecture decision ADR-013

Staging and Production Environments with Manual Promotion

Accepted · 2026-05-25

Historical decision text is preserved. Accepted describes the decision, not deployment of every capability discussed. Consult the Architecture and Infrastructure tabs for the verified running system.

Supersedes or amends: ADR-011

Successors or amendments: ADR-017

Date: 2026-05-25
Status: Accepted
Supersedes: ADR-011 (Trunk-Based with Single Production Environment)
Deciders: Daniel Meza


Context and Problem Statement

ADR-011 (May 2026) collapsed the environment topology to a single production Railway environment driven by every push to main. The decision was rational at the time: zero traffic, a team of one, and no GitHub Actions meant a staging environment would have been an unused expense.

Since then, three things changed:

  1. Bank affiliate integrations are entering scope. Several Costa Rican issuers (BAC, BCR) need a non-prod URL we can hand them to validate the apply-out lead flow (FEAT-012 / FEAT-015) before it goes live. Re-creating that environment ad-hoc per integration would burn more time than running one permanently.
  2. The "every merge is a prod deploy" model creates a chilling effect on merging. In practice, PRs sit unmerged while the developer babysits the deploy. The single-env model amplified merge friction rather than reducing it.
  3. AI-assisted change volume is higher than anticipated. Multiple Claude Code sessions land changes daily. Each merge is small but the cumulative blast radius of "main = prod" makes every merge a small ritual instead of a routine. A staging buffer lets merges resume being routine.

A simple "merge to main, see it in staging, smoke-test, promote when ready" loop gives us the integration-testing environment we need for partners and the merge-velocity we want for ourselves, at the cost of one extra Railway environment.


Decision Drivers


Considered Options

  1. Keep ADR-011 (single env, main → prod). Lowest cost. Doesn't solve partner-URL need or merge-friction problem.
  2. Two envs, main → staging, manual promotion to production via git tag + production branch. (chosen) main auto-deploys to staging. Promotion = update a long-lived production branch via a script that runs smoke tests and tags the release. Railway watches production for prod deploys.
  3. Two envs, main → staging, manual promotion via Railway "redeploy from environment". Same envs, but promotion happens in the Railway UI by copying the staging build to production rather than via git.
  4. Two envs, with GitHub Actions managing promotion on tag push. Introduce Actions purely for the promote step. Adds CI dependency and effort.
  5. Three envs (develop, staging, production) as ADR-010 originally prescribed. Heavy, and we removed it for good reason.

Decision Outcome

Option 2: two environments (staging, production) with manual promotion via git tag + production branch.

Environment topology

Environment Branch tracked Deploy trigger Purpose
staging main Every push to main (Railway-native GitHub integration) Continuous integration target; affiliate-facing pre-prod URL; smoke-test surface
production production (long-lived branch) Every push to production (Railway-native GitHub integration) Live customer-facing

Branch structure

feature/FEAT-XXX-name ┐
bug/short-description ┼─► main ──► staging
                      │              │
                      │              │ (manual promotion via scripts/promote.sh)
                      │              ▼
                      └─► production ──► production
                          ▲
hotfix/short-description ─┘
Branch Cut from Merges to Auto-deploys to Skill pair
feature/* main main — (staging via main) /feature-creator → /spec-generator → /feature-implementor
bug/* main main — /bug-planner → /bug-implementor
hotfix/* production production only — /hotfix-planner → /hotfix-implementor
main — — staging —
production — — production promoted via scripts/promote.sh

Promotion mechanism

A repo script scripts/promote.sh:

  1. Curl-checks staging API and web health (/health, /)
  2. Runs a Playwright smoke subset against the staging URL: pnpm --filter @urza/web test:e2e:smoke -- --base-url=$STAGING_WEB
  3. Computes and displays the diff between current production HEAD and main HEAD
  4. Prompts for confirmation with the diff summary in view
  5. Tags main as vYYYY.MM.DD-N
  6. Force-updates production to point at the new tag
  7. Pushes both production and the tag
  8. Watches the production deploy in Railway until it reports healthy
  9. Prompts the operator to run any pending manual migrations per the migrations handbook

The script is the single sanctioned promotion path. Rollback is documented in the same handbook: re-point production to the prior tag, force-push, watch the redeploy.

Hotfix path

Hotfixes are urgent prod bugs that cannot wait for the next staging cohort. They:

  1. Branch from production, not main
  2. Add a failing test that reproduces the issue, then fix it
  3. Merge directly to production (no main round-trip)
  4. Are promoted out-of-band (production health check only, smoke test skipped because the fix may be the cause of the smoke-test failure being addressed)
  5. Auto-open a follow-up cherry-pick PR against main so the fix isn't silently dropped on the next promotion from main. The /hotfix-implementor skill is responsible for opening this PR.

This is a known drift hazard. The auto-cherry-pick PR is non-negotiable; without it the next main promotion regresses the hotfix.

Migration timing during promotion

Per the migrations handbook (Obsidian Handbooks/Infra/db-migrations.md):

What is explicitly NOT in scope


Positive Consequences

Negative Consequences / Trade-offs


Pros and Cons of the Options

Option Iteration Speed Pre-prod safety Partner URL Cost Forward-compat to 3+ engineers
1. Keep single env (ADR-011) High None No Low Poor
2. main→staging, manual promote (chosen) High Moderate Yes Medium Good
3. main→staging, Railway-UI promote High Moderate Yes Medium OK (no git audit trail)
4. Promotion via GitHub Actions Medium High Yes Medium-high Good
5. Three envs (ADR-010) Low High Yes High Good

Option 4 is the natural next step once we have other reasons to run GitHub Actions (e.g., gated PR checks, dependabot autopilot). For now, the marginal value of a CI-gated promotion isn't worth the workflow file maintenance.


Migration from ADR-011

Concrete changes accompanying this ADR (across feature/infra-docs-and-railway-detach, feature/promotion-tooling, feature/claude-skills-and-obsidian-integration):

After this ADR lands:


References