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.
Successors or amendments: ADR-011
Date: 2026-05-10
Status: Superseded by ADR-011 on 2026-05-20. The 3-branch Gitflow described below was never fully exercised in production; the project switched to single-branch trunk-based development before the staging environment was used at all. The content below is preserved as historical context.
Deciders: Daniel Meza
⚠️ Superseded. The decision below is no longer in force. Read ADR-011 for the current branching and CI/CD strategy.
Context and Problem Statement
Urza's Wallet needs a CI/CD (Continuous Integration/Continuous Deployment) strategy that:
- Prevents unreviewed code from reaching production.
- Provides a staging environment that mirrors production closely enough to catch integration issues.
- Enables PR (Pull Request)-level review against a real deployment (not just a local dev server).
- Keeps the deployment pipeline simple enough for a small team to maintain.
- Is managed reproducibly via Terraform (GitHub environments, branch protections, Railway deploy tokens).
The deployment infrastructure involves two platforms: GitHub (source of truth and CI runner) and Railway (deployment target). The strategy must define the mapping between git branches, GitHub Actions triggers, and Railway environments.
Decision Drivers
- Three Railway environments:
develop(connected todevelopbranch),staging(connected tostagingbranch), andproduction(connected tomainbranch). Developers run services locally using.env.localfor day-to-day development; thedevelopRailway environment serves as the first integration target after a PR merges. - Railway preview environments: Railway's GitHub App automatically creates an ephemeral Railway environment for every open PR. This is a first-class Railway feature that requires no GitHub Actions configuration — Railway handles it via the GitHub App integration.
- Merge-on-green, not push-on-commit: Deployments to staging and production are triggered only by merges to
developormain, not by every push. This prevents thrashing deployments from force-pushes or fixup commits. - Production requires human approval: GitHub environment protection for
productionrequires at least 1 reviewer to approve a deployment workflow before it executes. This prevents accidental deploys to production from a CI bug or a mistaken direct push tomain. - CI must pass before merge: Branch protection on both
developandmainrequires all GitHub Actions CI checks (lint, typecheck, tests, build) to pass before a PR can be merged. No bypasses. - Secrets scoped to environments: Railway deploy tokens, Clerk production keys, and SendGrid API keys are scoped to their respective GitHub environment (
staging,production). A job running against the staging environment cannot access production secrets. - Terraform manages the structure: Branch protection rules, GitHub environment definitions, Railway deploy token storage in GitHub Actions secrets, and Railway environment/service provisioning are all managed via Terraform. Manual GitHub or Railway dashboard changes are reflected back in Terraform state.
Considered Options
- Trunk-based development (single main branch) — All developers commit to
maindirectly; feature flags gate incomplete work. Single deployment pipeline to production. - Gitflow (main + develop + feature/ branches)* —
feature/*→develop→main; PR environments for features; staging fromdevelop; production frommain. - Environment branches (main + staging branches) — A
stagingbranch mirrorsmainbut with staging-specific configuration. Prone to divergence. - PR-only deployments (no long-lived staging) — Every deployment is a PR preview environment; no dedicated staging.
Decision Outcome
Option 2: Gitflow (simplified — main + develop + feature/ branches)* is adopted.
Branch structure:
| Branch | Lifetime | Purpose | Deploys to |
|---|---|---|---|
main |
Permanent | Production-ready code | Production (Railway) |
staging |
Permanent | Pre-production validation | Staging (Railway) |
develop |
Permanent | Integration branch for merged features | Develop (Railway) |
feature/{ticket}-{description} |
Temporary | Feature development | PR preview (Railway) |
fix/{ticket}-{description} |
Temporary | Bug fixes | PR preview (Railway) |
hotfix/{description} |
Temporary | Urgent production fixes | PR to main directly |
Promotion path: feature/* → develop → staging → main
CI/CD pipeline (.github/workflows/):
workflows/
├── ci.yml ← Runs on every PR (lint, typecheck, tests, build check)
├── deploy.yml ← Runs on merge to develop (→ staging) or main (→ production)
└── preview.yml ← Runs on PR open/update (Railway preview deploy via Railway GitHub App)ci.yml (every PR):
on: [pull_request]
jobs:
lint: npm run lint
typecheck: npm run typecheck
test: npm run test
build: npm run build (next build + tsc for api)All four jobs must pass for the PR to be mergeable (enforced via branch protection).
deploy.yml (merge to develop, staging, or main):
on:
push:
branches: [develop, staging, main]
jobs:
deploy-develop:
if: github.ref == 'refs/heads/develop'
environment: develop
steps:
- Install Railway CLI
- railway up --service web --environment develop
- railway up --service api --environment develop
- railway up --service ingest --environment develop
deploy-staging:
if: github.ref == 'refs/heads/staging'
environment: staging
steps:
- Install Railway CLI
- railway up --service web --environment staging
- railway up --service api --environment staging
- railway up --service ingest --environment staging
deploy-production:
if: github.ref == 'refs/heads/main'
environment: production # GitHub environment with required_reviewers = 1
steps:
- Install Railway CLI
- railway up --service web --environment production
- railway up --service api --environment production
- railway up --service ingest --environment production
- Post-deploy: trigger sitemap regeneration via /api/revalidateRailway preview environments:
Railway's GitHub App is configured to create preview environments automatically when a PR is opened. Each preview environment gets its own Railway project subdomain (https://web-pr-{number}.up.railway.app). Preview environments connect to a shared staging PostgreSQL with a per-PR database schema prefix (pr_{number}_) to isolate data. Preview environments are automatically destroyed when the PR is closed or merged.
GitHub branch protection rules (managed via Terraform):
| Branch | Required checks | Required reviewers | Direct push | Force push |
|---|---|---|---|---|
main |
ci/lint, ci/typecheck, ci/test, ci/build | 1 | Blocked | Blocked |
staging |
ci/lint, ci/typecheck, ci/test, ci/build | 1 | Blocked | Blocked |
develop |
ci/lint, ci/typecheck, ci/test, ci/build | 1 | Blocked | Blocked |
Terraform configuration (infra/terraform/github/environments.tf):
resource "github_repository_environment" "develop" {
environment = "develop"
repository = var.github_repo
}
resource "github_repository_environment" "staging" {
environment = "staging"
repository = var.github_repo
}
resource "github_repository_environment" "production" {
environment = "production"
repository = var.github_repo
reviewers {
users = [data.github_user.owner.id]
}
deployment_branch_policy {
protected_branches = true
custom_branch_policies = false
}
}Database migration strategy:
Schema migrations run as part of the deployment pipeline using node-pg-migrate or drizzle-kit push:
- Migrations are applied to staging automatically on
developmerge. - Migrations are applied to production as the first step of the production deploy job, before the application services are updated.
- All migrations must be backward-compatible (no dropping columns or changing types without a deprecation period) to support Railway's rolling restart model.
- The previous Railway deployment is pinned for 30 minutes after a production deploy to enable fast rollback via the Railway dashboard if a migration causes issues.
Positive Consequences
- Clear promotion path:
feature/* → develop → main. Code that reachesmainhas been reviewed, tested, and validated against staging. No shortcuts. - Clear promotion path with a staging gate:
feature/* → develop → staging → main. Code passes through a dedicated Railway staging environment before it can reach production. Integration issues are caught in staging, not production. - PR preview environments: Every PR has a live deployment for design and QA review. This is provided by Railway's GitHub App with no additional pipeline configuration.
- Production protected by human approval: The GitHub environment protection for
productionrequires a reviewer to approve the deploy workflow. This prevents runaway CI automation from deploying broken code. - Secrets scoped to environments: Staging secrets (Clerk dev keys, SendGrid sandbox key) are only accessible in the
stagingGitHub environment. Production secrets are only accessible in theproductionenvironment. - Terraform-managed structure: Branch protection rules, GitHub environments, and Railway tokens are all in version control. Onboarding a new team member or recovering from accidental dashboard changes is a
terraform apply.
Negative Consequences / Trade-offs
developbranch management overhead: Gitflow requires developers to keepfeature/*branches up to date withdevelop, resolve merge conflicts, and maintain a healthydevelopbranch. For a very small team (1–2 engineers), this can feel like overhead. Mitigation: the team can simplify to trunk-based development with feature flags when the team is ≤ 2 engineers; the branch structure documented here is the target for a team of 3+.- Two PRs for hotfixes: An urgent production fix requires a PR to
main(hotfix) and a back-port PR todevelop. Mitigation: hotfix branches (hotfix/*) can targetmaindirectly; back-porting is a separategit cherry-pickPR todevelop. The back-port can be low-ceremony if the fix is small. - Staging database divergence: If the staging database accumulates manual test data or developer experiments, it may diverge from production behavior. Mitigation: a
scripts/seed-staging.tsscript resets staging data to a known fixture set; this is run before major feature testing on staging. - Preview environment database sharing: PR preview environments share a staging PostgreSQL instance (with a schema prefix). A preview environment's schema migrations could conflict if two PRs run migrations concurrently. Mitigation: PR migrations use a per-PR schema prefix; migrations are applied in isolation. For major schema migrations, preview environments are disabled temporarily.
Pros and Cons of the Options
| Option | Review Safety | Staging Fidelity | Simplicity | Preview Envs |
|---|---|---|---|---|
| Trunk-based + feature flags | Low (no staging) | N/A | High | Manual |
| Gitflow simplified (chosen) | High | High | Medium | Railway native |
| Environment branches | Medium | Variable | Low | Manual |
| PR-only deployments | Medium | High (per-PR) | High | Native |
Trunk-based development was considered for its simplicity. For a team of 1–2, it is appropriate. The Gitflow model is documented here as the target pattern because the project is designed for a team of 3+ and the branch structure is managed in Terraform (so switching back to trunk-based requires only a Terraform and branch protection rule change, not application changes).
References
- Richards & Ford, Fundamentals of Software Architecture, Ch. 4 (Architecture Characteristics — deployability), Ch. 6 (Fitness Functions — deployment pipeline as a fitness function)
- Railway documentation: GitHub App integration, preview environments, Railway CLI deploy
- GitHub documentation: environments, branch protection rules, environment secrets
- Architecture reference §10.3 (Deployment Strategy), §10.4 (Secret Management)