Staging Environment
A full mirror of production so you can test a deploy (code and the infra it touches) before it reaches production. Staging is fully isolated — its own Workers, database, R2 buckets, Redis, and trigger.dev project. Nothing you do in staging can touch production data or infrastructure.
How it's wired
Branch-based, driven by GitHub Actions + GitHub Environments:
| Branch | Environment | What deploys |
|---|---|---|
main | production | base wrangler env (prod Workers, domains, buckets) |
staging | staging | [env.staging] wrangler envs + Pages staging branch |
Workflows:
.github/workflows/deploy-backend.yml—api-edge+tus-edge(automatic backend deploy)..github/workflows/deploy-frontends.yml—dashboard,player,landing(per-envVITE_*build, Pages branch deploy).
Domains
| Service | Production | Staging |
|---|---|---|
VOD API (api-edge) | vapi.usestrum.app | staging.vapi.usestrum.app |
Upload (tus-edge) | upload.usestrum.app | upload-staging.usestrum.app |
| Dashboard | vod.usestrum.app | vod-staging.usestrum.app |
| Player | player.usestrum.app | player-staging.usestrum.app |
| Public media | media.usestrum.app | media-staging.usestrum.app |
Custom domains are auto-created by custom_domain = true in the [env.staging] wrangler configs on first deploy; the dashboard/player staging subdomains need a DNS CNAME/proxy record pointing at the staging Worker/Pages project.
One-time provisioning
Run once to bring staging up. Do not point any of these at production.
1. Database (Neon)
Use a Neon branch of your prod DB (cheapest, no copy needed), or a separate staging DB. Grab its DATABASE_URL and set it on the staging Worker.
2. R2 buckets
wrangler r2 bucket create strum-vod-staging
wrangler r2 bucket create strum-vod-backups-staging
wrangler r2 bucket create strum-vod-landing-media-stagingSet the S3 endpoint + a staging R2 access key/secret (or reuse the account creds — the bucket name strum-vod-staging already isolates it) on the staging Worker. Apply CORS to the staging bucket too (pnpm r2:cors:set targets the prod bucket — run the equivalent for strum-vod-staging).
3. Redis (Upstash)
Create a staging Redis instance; set REDIS_URL on the staging Worker.
QStash is no longer used — all live paths (transcode dispatch, webhook delivery, crons) run on trigger.dev now.
qstash.tsis dead code; no staging (or prod) QStash provision is needed.
4. trigger.dev
Free tier has no staging project/env — a second project is a paid feature. So staging shares the production trigger.dev project. That means:
- Set the prod
tr_prod_...secret key asTRIGGER_SECRET_KEYon the staging Worker (it authenticates against the same project). apps/trigger-tasksis not deployed separately — the prod deployment is the one staging triggers against. (Caveat: a transcode started from staging dispatches through the prod trigger.dev project; the job payload carries the staging asset/URL, but orchestration machinery is shared. This is the accepted trade-off of the free tier.)
If you later upgrade trigger.dev, revisit this: create a staging project, set the staging TRIGGER_SECRET_KEY, and deploy apps/trigger-tasks to it with the staging env values (see deployment.md §1).
5. Quick aliases (root package.json)
Staging helpers added to the root package.json:
pnpm deploy:staging # deploy everything (api-edge, tus-edge, dashboard,
# player, landing, docs, transcoder, trigger, modal) to staging
pnpm deploy:all # deploy everything to production (requires --yes under the hood)
pnpm --filter @strum-vod/api-edge run deploy:staging # only api-edge --env staging
pnpm --filter @strum-vod/tus-edge run deploy:staging # only tus-edge --env staging
pnpm --filter @strum-vod/api-edge run deploy:prod # only api-edge (prod)
pnpm --filter @strum-vod/tus-edge run deploy:prod # only tus-edge (prod)
pnpm db:migrate:staging # migrate the STAGING Neon branch (reads .env.staging)
pnpm r2:cors:set:staging # apply staging CORS to strum-vod-staging bucketdeploy:staging and deploy:all both go through scripts/deploy-all.sh, which drives each app's deploy:prod/deploy:staging entrypoint (always an explicit wrangler deploy --env production|staging / Pages --branch — no bare wrangler deploy). deploy:all (prod) refuses to run unless you pass --yes:
pnpm deploy:all # dry check: prints what would deploy, exits
pnpm deploy:all --yes # actually deploys productiondb:migrate:staging reads DATABASE_URL from scripts/migrate-staging.sh, which sources a gitignored .env.staging (or falls back to STAGING_DATABASE_URL). It's idempotent, so safe to re-run.
6. Worker secrets (Cloudflare)
Every secret is per-env. Set the staging values once:
cd apps/api-edge
wrangler secret put DATABASE_URL --env staging
wrangler secret put JWT_SECRET --env staging
wrangler secret put S3_ACCESS_KEY_ID --env staging
wrangler secret put S3_SECRET_ACCESS_KEY --env staging
wrangler secret put S3_ENDPOINT --env staging
wrangler secret put TRIGGER_SECRET_KEY --env staging
wrangler secret put WHISPER_WEBHOOK_SECRET --env staging
wrangler secret put DEMUCS_WEBHOOK_SECRET --env staging
wrangler secret put SUPERADMIN_BOOTSTRAP_KEY --env staging # optional
cd ../tus-edge
wrangler secret put SHARED_AUTH_SECRET --env stagingSHARED_AUTH_SECRET must be the same base64 value on staging api-edge and staging tus-edge (they sign/verify the upload-token JWT together).
7. GitHub Environments
In repo Settings → Environments, create staging and production. Add to both:
CLOUDFLARE_API_TOKENCLOUDFLARE_ACCOUNT_IDDATABASE_URL— the same Neon connection string you set on the matching Worker secret (staging → the staging branch, production → the main branch). The backend deploy workflow runspnpm db:migrateagainst this before deploying, sopackages/dbschema changes reach the database automatically. The migration step only runs when a push changedpackages/db/**(it always runs on a manualworkflow_dispatch).
On production, add a required reviewer (protection rule) so production deploys need an explicit approval; leave staging automatic.
8. Staging worker machine
For staging to actually transcode, run the Go worker against the staging broker (production and staging must never share a worker token/broker):
./strum-transcoder --api-url https://staging.vapi.usestrum.app --token mt_staging_...Mint the mt_staging_... token against the staging API. Raise STRUM_CLAIM_POLL_INTERVAL=120 if it's an always-on box (see worker-setup.md).
Routine
- Push to
staging→ backend + frontends deploy automatically. - Test end-to-end against
https://staging.vapi.usestrum.app(upload viaupload-staging.usestrum.app, transcode on the staging worker, play in the staging dashboard). - When satisfied, merge
stagingintomain(or open a PR) → production deploys after the approval gate.
Notes / caveats
- The base (top-level) wrangler config is production. Never add staging secrets to it; staging secrets only ever go through
--env staging. - No Cloudflare crons anywhere (
wrangler.tomlsetscrons = []) — all scheduled work is on trigger.dev. Staging shares the prod trigger.dev project, so the scheduled tasks (incl.reclaim-stale-dispatch-jobs, the dispatch reaper) run against whatever DBDEPLOY_DATABASE_URLpoints at — which, for the shared project, is the prod DB. So staging has no scheduled self-healing against the staging DB; deploy the shared project withDEPLOY_DATABASE_URLpointed at the DB you want those tasks to reap. - Migrations are still manual, same as prod: apply schema changes to the staging DB before staging tests, then to prod at deploy time (
deployment.md§4).