Skip to content

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:

BranchEnvironmentWhat deploys
mainproductionbase wrangler env (prod Workers, domains, buckets)
stagingstaging[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-env VITE_* build, Pages branch deploy).

Domains ​

ServiceProductionStaging
VOD API (api-edge)vapi.usestrum.appstaging.vapi.usestrum.app
Upload (tus-edge)upload.usestrum.appupload-staging.usestrum.app
Dashboardvod.usestrum.appvod-staging.usestrum.app
Playerplayer.usestrum.appplayer-staging.usestrum.app
Public mediamedia.usestrum.appmedia-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 ​

bash
wrangler r2 bucket create strum-vod-staging
wrangler r2 bucket create strum-vod-backups-staging
wrangler r2 bucket create strum-vod-landing-media-staging

Set 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.ts is 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 as TRIGGER_SECRET_KEY on the staging Worker (it authenticates against the same project).
  • apps/trigger-tasks is 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:

bash
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 bucket

deploy: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:

bash
pnpm deploy:all           # dry check: prints what would deploy, exits
pnpm deploy:all --yes     # actually deploys production

db: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:

bash
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 staging

SHARED_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_TOKEN
  • CLOUDFLARE_ACCOUNT_ID
  • DATABASE_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 runs pnpm db:migrate against this before deploying, so packages/db schema changes reach the database automatically. The migration step only runs when a push changed packages/db/** (it always runs on a manual workflow_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):

bash
./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 ​

  1. Push to staging → backend + frontends deploy automatically.
  2. Test end-to-end against https://staging.vapi.usestrum.app (upload via upload-staging.usestrum.app, transcode on the staging worker, play in the staging dashboard).
  3. When satisfied, merge staging into main (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.toml sets crons = []) — 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 DB DEPLOY_DATABASE_URL points 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 with DEPLOY_DATABASE_URL pointed 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).

STRUM Proprietary License — © 2026 Strum. All rights reserved.