CI/CD setup — staging + prod
How the GitHub Actions workflows in .github/workflows/ fit together
and what you need to configure on the GitHub / Railway / Vercel /
Supabase side to make them work.
Branching model
feature work
↓
staging branch → staging env (auto-deploy, auto-migrate)
↓ PR + review
main branch → prod env (auto-deploy, manual-approval migrate)
- Push to
staging→ migrations apply automatically + Railway/Vercel deploy. - Push to
main→ migrations dry-run automatically; the apply step pauses for your approval before running. Railway/Vercel deploy on push.
Workflows
| File | Trigger | What it does |
|---|---|---|
migrate-staging.yml | push to staging (only when supabase/migrations/** changes) | dry-run + apply migrations to staging Supabase |
migrate-prod.yml | push to main (only when supabase/migrations/** changes) | dry-run automatically; apply pauses for manual approval in the production-db GitHub Environment |
deploy-backend.yml | push to main/staging (when apps/backend/** changes) | typecheck + build (gate). Railway's GitHub integration handles the actual deploy. |
deploy-frontend.yml | push to main/staging (when apps/frontend/** changes) | builds + deploys to Vercel. main → production target; staging → preview target. |
schema-drift.yml | weekly Mondays + on demand | diffs the public schema between prod and staging. Opens / comments on a schema-drift-labelled GitHub issue if they diverge. |
One-time setup
1. Create the staging branch
git checkout -b staging
git push -u origin staging
This is what triggers the staging workflows.
2. GitHub repo secrets
Settings → Secrets and variables → Actions → New repository secret.
Supabase (used by all migrate-* and schema-drift workflows)
| Secret | Value |
|---|---|
SUPABASE_ACCESS_TOKEN | Personal access token from supabase.com/dashboard/account/tokens |
SUPABASE_PROD_PROJECT_REF | pfyocleyejmtxmbxrdcm |
SUPABASE_STAGING_PROJECT_REF | nndgywvhhuialhkzlsgh |
SUPABASE_PROD_DB_PASSWORD | direct-connection DB password (Settings → Database → password) |
SUPABASE_STAGING_DB_PASSWORD | same, but for the staging project |
SUPABASE_PROD_DB_URL | postgresql://postgres:<prod-pw>@db.pfyocleyejmtxmbxrdcm.supabase.co:5432/postgres |
SUPABASE_STAGING_DB_URL | postgresql://postgres:<staging-pw>@db.nndgywvhhuialhkzlsgh.supabase.co:5432/postgres |
Vercel (used by deploy-frontend.yml)
| Secret | Value |
|---|---|
VERCEL_TOKEN | from vercel.com/account/tokens |
VERCEL_ORG_ID | from your Vercel project's .vercel/project.json (org ID) |
VERCEL_PROJECT_ID | from the same file (project ID) |
Frontend env vars (per environment)
The frontend workflow injects these at build-time into Vercel.
Prod values (no suffix):
| Secret | Value |
|---|---|
NEXT_PUBLIC_API_URL | https://hivejournalbackend-production.up.railway.app |
NEXT_PUBLIC_SUPABASE_URL | https://pfyocleyejmtxmbxrdcm.supabase.co |
NEXT_PUBLIC_SUPABASE_ANON_KEY | from prod Supabase Settings → API |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY | live pk_live_… |
Staging values (_STAGING suffix):
| Secret | Value |
|---|---|
NEXT_PUBLIC_API_URL_STAGING | staging Railway URL |
NEXT_PUBLIC_SUPABASE_URL_STAGING | https://nndgywvhhuialhkzlsgh.supabase.co |
NEXT_PUBLIC_SUPABASE_ANON_KEY_STAGING | from staging Supabase Settings → API |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_STAGING | test pk_test_… |
3. GitHub Environment for prod-migration approval
This is what makes prod migrations pause for your approval.
- Settings → Environments → New environment
- Name:
production-db - Required reviewers → add yourself (and any team members who should be able to approve)
- Save
Now any push to main that touches supabase/migrations/** will run the dry-run, then pause and email you to click Approve and deploy in the Actions UI.
4. Railway — point staging environment at the staging branch
In Railway:
- Make sure your staging environment exists (Project → top-left environment dropdown → New environment →
staging) - Service settings for the staging service → Source → set the branch to
staging - Confirm prod's service is on
main
Railway will auto-deploy each environment from its assigned branch. The deploy-backend.yml workflow runs typecheck + build as a quality gate but does NOT call railway up (Railway's GitHub integration handles it).
5. Vercel — branch-targeted environment variables
In Vercel:
- Project → Settings → Environments
- Production env → set to deploy from
main. The runtime env vars here are used at request time and should mirror the secrets above. - Preview env → set "automatically expose system environment variables" + (optionally) add
stagingas a "branch" preview rule with a custom domain likestaging.hivejournal.com.
The deploy-frontend.yml workflow injects build-time env vars from secrets, so the Vercel-dashboard env vars are mostly for runtime (server components, edge functions). Keep them in sync.
6. Stripe — separate test webhook
In Stripe test mode dashboard:
- Developers → Webhooks → Add endpoint
- URL:
<staging-railway-backend-url>/api/payment/webhook - Events:
checkout.session.completed,customer.subscription.updated,customer.subscription.deleted,account.updated - Copy signing secret → set
STRIPE_WEBHOOK_SECRETin Railway staging environment
Repeat for live mode against the prod Railway URL.
How a typical change flows
- Work on a feature branch off
staging. - PR into
staging→ review/merge. stagingbranch CI runs: backend builds, frontend deploys to Vercel preview, migrations apply to staging Supabase. Test in staging.- When staging looks good, PR
staging→main→ review/merge. mainbranch CI runs: backend builds, frontend deploys to Vercel production, migration dry-run runs.- You get a GitHub email: "production-db needs approval." Click into the Actions tab → Approve and deploy.
- Migration applies. Railway has already deployed the new backend code. App's running on the new schema.
Operational notes
- Migrations + idempotency: every migration must be safe to re-run. We already use
IF NOT EXISTSeverywhere; the CLI tracks applied state insupabase_migrations.schema_migrations. If state drifts (e.g. someone hand-applied a migration via SQL editor), the CLI will try to re-apply and idempotency catches it. - No down-migrations: Postgres rollbacks aren't reliable. Fix forward with a new migration that undoes the change.
- Long-running backfills: don't put a backfill that touches >100k rows inside a migration — it locks tables. Migration adds the column; a worker script populates it.
- Hot-fixes via SQL editor: every once in a while you may need to fix something live. That's fine — but immediately commit a migration to
stagingthat brings the repo into sync, so the schema-drift check passes on Monday. The drift workflow exists exactly to surface these. - Accidentally pushing a bad migration: dry-run should catch syntax errors. If a migration applies but breaks the app: write a fix-forward migration ASAP. If it's truly catastrophic: the only safe rollback is a database restore from PITR (Supabase Pro+). Plan accordingly.
Upgrading later
Patterns we explicitly didn't build but might want eventually:
- Preview environments per PR (Vercel-style ephemeral DBs via Supabase branching) — adds a per-PR environment but doubles operational surface area. Revisit when the team grows.
- Strict deploy ordering via Railway CLI — replace Railway's auto-deploy with
railway upfrom GH Actions so migrations and code deploys are strictly serialized. We don't need it today (migrations are fast; race window is small) but it's the right move once real revenue is at stake. - Slack notifications on drift detection — easy add: drop a
slackapi/slack-github-action@v1step intoschema-drift.ymland feed it a webhook secret.