setup-guides

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

FileTriggerWhat it does
migrate-staging.ymlpush to staging (only when supabase/migrations/** changes)dry-run + apply migrations to staging Supabase
migrate-prod.ymlpush to main (only when supabase/migrations/** changes)dry-run automatically; apply pauses for manual approval in the production-db GitHub Environment
deploy-backend.ymlpush to main/staging (when apps/backend/** changes)typecheck + build (gate). Railway's GitHub integration handles the actual deploy.
deploy-frontend.ymlpush to main/staging (when apps/frontend/** changes)builds + deploys to Vercel. main → production target; staging → preview target.
schema-drift.ymlweekly Mondays + on demanddiffs 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)

SecretValue
SUPABASE_ACCESS_TOKENPersonal access token from supabase.com/dashboard/account/tokens
SUPABASE_PROD_PROJECT_REFpfyocleyejmtxmbxrdcm
SUPABASE_STAGING_PROJECT_REFnndgywvhhuialhkzlsgh
SUPABASE_PROD_DB_PASSWORDdirect-connection DB password (Settings → Database → password)
SUPABASE_STAGING_DB_PASSWORDsame, but for the staging project
SUPABASE_PROD_DB_URLpostgresql://postgres:<prod-pw>@db.pfyocleyejmtxmbxrdcm.supabase.co:5432/postgres
SUPABASE_STAGING_DB_URLpostgresql://postgres:<staging-pw>@db.nndgywvhhuialhkzlsgh.supabase.co:5432/postgres

Vercel (used by deploy-frontend.yml)

SecretValue
VERCEL_TOKENfrom vercel.com/account/tokens
VERCEL_ORG_IDfrom your Vercel project's .vercel/project.json (org ID)
VERCEL_PROJECT_IDfrom the same file (project ID)

Frontend env vars (per environment)

The frontend workflow injects these at build-time into Vercel.

Prod values (no suffix):

SecretValue
NEXT_PUBLIC_API_URLhttps://hivejournalbackend-production.up.railway.app
NEXT_PUBLIC_SUPABASE_URLhttps://pfyocleyejmtxmbxrdcm.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEYfrom prod Supabase Settings → API
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYlive pk_live_…

Staging values (_STAGING suffix):

SecretValue
NEXT_PUBLIC_API_URL_STAGINGstaging Railway URL
NEXT_PUBLIC_SUPABASE_URL_STAGINGhttps://nndgywvhhuialhkzlsgh.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY_STAGINGfrom staging Supabase Settings → API
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_STAGINGtest pk_test_…

3. GitHub Environment for prod-migration approval

This is what makes prod migrations pause for your approval.

  1. Settings → Environments → New environment
  2. Name: production-db
  3. Required reviewers → add yourself (and any team members who should be able to approve)
  4. 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:

  1. Make sure your staging environment exists (Project → top-left environment dropdown → New environment → staging)
  2. Service settings for the staging service → Source → set the branch to staging
  3. 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:

  1. Project → SettingsEnvironments
  2. Production env → set to deploy from main. The runtime env vars here are used at request time and should mirror the secrets above.
  3. Preview env → set "automatically expose system environment variables" + (optionally) add staging as a "branch" preview rule with a custom domain like staging.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:

  1. Developers → Webhooks → Add endpoint
  2. URL: <staging-railway-backend-url>/api/payment/webhook
  3. Events: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, account.updated
  4. Copy signing secret → set STRIPE_WEBHOOK_SECRET in Railway staging environment

Repeat for live mode against the prod Railway URL.

How a typical change flows

  1. Work on a feature branch off staging.
  2. PR into staging → review/merge.
  3. staging branch CI runs: backend builds, frontend deploys to Vercel preview, migrations apply to staging Supabase. Test in staging.
  4. When staging looks good, PR stagingmain → review/merge.
  5. main branch CI runs: backend builds, frontend deploys to Vercel production, migration dry-run runs.
  6. You get a GitHub email: "production-db needs approval." Click into the Actions tab → Approve and deploy.
  7. 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 EXISTS everywhere; the CLI tracks applied state in supabase_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 staging that 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 up from 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@v1 step into schema-drift.yml and feed it a webhook secret.
CI CD SETUP — Docs | HiveJournal