Staging to prod cutover
Why
Everything is on Railway staging while it’s actively developed. When we stand up a prod environment with real customers, we copy the env but swap a set of things. The risk is missing one (Stripe test keys in prod, or staging sharing infra that lets it clobber prod data).
R2: separate buckets per environment
Decision: prod gets its own R2 bucket, not the shared staging one.
Reasoning is blast radius, not storage. Staging churns hard (test uploads, product deletions, re-imports). A shared bucket means a staging cleanup or migration can overwrite or delete an object prod is serving. Medusa’s file module deletes the backing R2 object when a product/image is removed, so “delete all products” on staging would nuke live covers if shared. Covers being byte-identical across envs (ISBN-keyed) is a real but weak argument to share; covers are cheap to reproduce by re-running the scraper, not worth the risk.
So prod = its own bucket + its own credentials + its own public domain. Staging keeps assets.dungeonbooks.com or we flip it: prod takes assets.dungeonbooks.com, staging moves to a staging domain or the *.r2.dev URL. Decide which domain prod owns at cutover.
Env diff checklist
Per service, what changes from staging to prod:
Stripe (highest risk if missed)
-
STRIPE_API_KEYtest (sk_test_) to live (sk_live_) -
STRIPE_WEBHOOK_SECRETfrom the prod webhook endpoint
Databases + cache (separate instances)
- Medusa
DATABASE_URL -
REDIS_URL - Payload
PAYLOAD_DATABASE_URL - catalog
CATALOG_DB_URL(prod imports from its own queue so staging re-scrapes don’t flip prod sync state)
R2 (separate bucket, see above)
- backend
S3_*(endpoint, bucket, keys,S3_FILE_URL) - payload
S3_* - scraper
R2_*(endpoint, keys,R2_BUCKET_NAME,R2_PUBLIC_URL)
Secrets (generate fresh)
-
JWT_SECRET,COOKIE_SECRET(backend) -
PAYLOAD_SECRET
URLs + CORS
- backend
MEDUSA_BACKEND_URL,STORE_CORS,ADMIN_CORS,AUTH_CORS - storefront
NEXT_PUBLIC_MEDUSA_BACKEND_URL,NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY(prod channel’s key),PAYLOAD_URL - payload
PAYLOAD_PUBLIC_SERVER_URL
Search
-
MEILISEARCH_HOST/MEILISEARCH_API_KEY(separate instance/index so staging reindexes don’t touch prod search)
Notes
- The catalog scraper’s
IPAGE_COOKIEis the same Ingram account regardless of env; not an env-diff item. - Migrations must run against each prod DB before first boot (Medusa + Payload).
- See also: the per-service Dockerfile / minimal-build work (separate backlog) makes prod deploys faster and is worth doing around the same time.