Operational deploy guide for VMP. For product architecture and package overview, see README.md. For agent/dev workflow, see AGENTS.md.
- Unified environment variables
- Multi-domain setup
- CI/CD flow
- Fresh infrastructure bootstrap runbook
- Rollback notes (staging/production)
- Livestream notes (Media over QUIC)
Use the root .env.example as the single source template for all runtime values.
Copy per environment:
.env.staging.env.production
For local API development, mirror required values into packages/api/.dev.vars (never commit secrets).
Keep domain-specific values as env overrides:
FRONTEND_URLALLOWED_ORIGINSR2_BASE_URL
Deploy each domain with its own Wrangler environment and secret set.
Canonical deploy workflow: .github/workflows/deploy.yml.
deploy.yml is the only supported deployment workflow.
The canonical workflow uses:
- pushes to
main→ staging API Worker (@vmp/api) + staging web Worker (vmp-web-worker-dev) - version tags (
v*.*.*) → production API Worker + production web Worker (vmp-web-worker-prod)
Deno Deploy backup API (@vmp/api-node) is not deployed from deploy.yml. Deno Deploy builds and deploys automatically from the linked GitHub repository on every push (including PR preview builds). CLI uploads via deno deploy from GitHub Actions have been observed to fail consistently while git-triggered builds succeed; rely on the Deno Deploy build status on the pull request and in console.deno.com instead.
Frontend deploy is Workers only (Nuxt cloudflare-module preset). Cloudflare Pages is deprecated in this repo.
The backup API runs the same @vmp/api handlers on Deno Deploy with a Postgres D1 shim.
Pull requests (.github/workflows/ci.yml, job api-node):
npm run verify:api-node— typecheck, unit tests (including Sentry/D1 shim compatibility), and esbuild bundle.- A PR that fails this job must not be merged if branch protection requires CI green.
Deno Deploy (git integration — not deploy.yml):
- Deno Deploy automatically builds and deploys from the linked repository on every push. PRs get preview deployments; merges to
mainupdate production. - Check the Deno Deploy build status on the PR (and preview URL in console.deno.com) before merging api-node changes. Git-triggered builds are the source of truth;
deno deployCLI uploads from CI are not used. - Optional manual smoke after a production deploy (when
vars.API_URL_BACKUPis set):.github/scripts/smoke-api-node-backup.shchecks:GET /api/health—mode: "deno-deploy", database checkok: trueGET /api/homepage/content— HTTP 200 (catches runtime DB/Sentry shim failures that build alone would miss)
Required for Deno backup API runtime (set in Deno Deploy dashboard, not GitHub Actions deploy):
DATABASE_URL(managed Postgres)- Worker-equivalent secrets (see
packages/api-node/.env.example) - Optional variable for manual smoke:
API_URL_BACKUP(public backup API origin, e.g.https://vmp-backup-api.tjm.sk)
Local parity before opening a PR:
npm run verify:api-nodeSee packages/api-node/README.md for runtime setup and replication ingest.
Required repository secrets:
CLOUDFLARE_API_TOKEN_STAGINGCLOUDFLARE_API_TOKEN_PRODCLOUDFLARE_ACCOUNT_ID_STAGINGCLOUDFLARE_ACCOUNT_ID_PRODSTAGING_SMOKE_AUTH_TOKEN(shared secret for staging/api/admin/smoke-authsmoke check)PROD_SMOKE_AUTH_TOKEN(shared secret for production/api/admin/smoke-authsmoke check)
Note: the deploy workflow reads these exact CLOUDFLARE_*_STAGING/PROD secret names.
Required environment variables (GitHub Environments/Repository Variables):
- Staging:
API_URL_STAGINGFRONTEND_URL_STAGING(must match the Worker custom domain or*.workers.devURL you route tovmp-web-worker-dev)ALLOWED_ORIGINS_STAGINGNUXT_PUBLIC_SENTRY_DSN(frontend Sentry project DSN, embedded at build time)
- Production:
API_URL_PRODFRONTEND_URL_PROD(custom domain routed tovmp-web-worker-prod)ALLOWED_ORIGINS_PRODNUXT_PUBLIC_SENTRY_DSN(frontend Sentry project DSN, embedded at build time)
Optional repository secret for Sentry source map uploads during web builds:
SENTRY_AUTH_TOKEN(org token withproject:releases+org:read)
The hardened workflows now enforce:
- API and web builds use the environment-specific
API_URL_*. - Deploy steps use environment-specific Cloudflare token/account secrets.
- Post-deploy smoke checks validate:
/api/healthpayload ({ status: "healthy" })- CORS
Access-Control-Allow-OriginagainstFRONTEND_URL_* - machine smoke-auth endpoint (
GET /api/admin/smoke-auth) viaX-Smoke-Token - frontend reachability on
FRONTEND_URL_* gitCommitbaked into deployed HTML matches${{ github.sha }}(see.github/scripts/smoke-frontend-build-revision.sh)
Smoke auth endpoint details:
- Route:
GET /api/admin/smoke-auth - Header:
X-Smoke-Token: <token> - Secrets used by workflow:
- Worker env secrets:
SMOKE_AUTH_TOKEN_STAGING/SMOKE_AUTH_TOKEN_PROD - GitHub Action secrets:
STAGING_SMOKE_AUTH_TOKEN/PROD_SMOKE_AUTH_TOKEN
- Worker env secrets:
- Token must match exactly (timing-safe compare); use a long random value and rotate as needed.
Use this when staging/production D1, KV, and/or R2 were intentionally reset.
- Freeze auto-deploys
- Confirm
.github/workflows/deploy.ymlis the only active deployment workflow (no separate Pages or experimental web workflow).
- Recreate bindings/resources (per environment)
- D1 database
- KV namespace(s)
- R2 bucket(s)
- Update Worker bindings to point to recreated resources.
- Restore required secrets (per environment)
JWT_SECRETBREVO_API_KEYSTRIPE_SECRET_KEYSTRIPE_WEBHOOK_SECRETTOTP_ENCRYPTION_KEYVAPID_PRIVATE_KEYRSS_SECRETSENTRY_DSN(Worker API project DSN for@sentry/cloudflare)
- Re-apply database migrations in order
- Run all SQL files in
packages/api/migrations/in ascending order. - Do not edit historical migration files; add new numbered migrations as needed.
Run these commands after migrations and before promoting traffic:
- Idempotent backfill/mapping script
- Local dry-run style check:
npm run db:migration-backfill --workspace=@vmp/api
- Remote target:
DB_NAME=video-subscription-db npm run db:migration-backfill --workspace=@vmp/api -- --remote
- Expected success signal:
- Output includes
[backfill] Completed successfully. - Re-running should produce the same success output with no errors.
- Output includes
- Failure interpretation:
- Any non-zero exit means schema drift (missing columns/tables) or DB connectivity issues.
- Resolve schema mismatch first, then rerun migrations and this backfill.
- Integrity verification script (hard PASS/FAIL gate)
- Local:
npm run db:migration-verify --workspace=@vmp/api
- Remote:
DB_NAME=video-subscription-db npm run db:migration-verify --workspace=@vmp/api -- --remote
- Expected PASS output:
- Row counts emitted for key tables.
- Final line:
[verify] PASS: all integrity checks are zero.
- Expected FAIL output:
- Integrity failures: Final line
[verify] FAIL: <n> integrity check(s) are non-zero.— command exits1; deployment should stop until non-zero checks are fixed. - Schema failures: Final line
[verify] FAIL: <n> required schema check(s) missing.— command exits1; deployment should stop until schema drift is resolved. - Operators should look for either failure message when triaging verification issues.
- Integrity failures: Final line
- Targeted deterministic regression suite (Task 12)
npm test --workspace=@vmp/api -- --test-name-pattern="clampNewsletterPollIntervalMs|isNewsletterSendFinished|fetchBrevoEmailCampaignsWithRetry|evaluateRoleChange|evaluateSelfRoleChange|evaluateSubscriptionStatusChange|segment analytics|normalizeLivestreamStatus|normalizeStripeStatus|placeHomepageVideos matrix|sortCategoriesForHomepage|placementTimestampMs"- Expected PASS signal:
- Node test runner summary reports all listed suites passing, zero failed tests.
- Failure interpretation:
- Any failing suite blocks deployment; fix regression and rerun full targeted command.
- Seed data policy
- Staging: seed freely for smoke testing.
- Production: seed only intentional baseline config; avoid test/demo data.
- Deploy order
- Deploy API first.
- Deploy web second.
- Post-deploy smoke checks
GET /api/healthreturns healthy.- Auth flow (magic link + session restore) works.
- Admin loads and can perform one write action.
- Homepage and watch routes render without server errors.
- If media was reset, validate expected behavior for missing/placeholder media.
Use the smallest rollback that restores service:
- Full rollback
- Re-run
.github/workflows/deploy.ymlfrom a known-good commit/tag (workflow_dispatch) targeting the affected environment. - Validate health, CORS, admin auth smoke checks, and homepage/watch rendering.
- Emergency containment
- If staging deploy is unstable, pause merges to
mainuntil smoke checks are green. - If production deploy is unstable, disable further production tags and roll back first, then investigate.
- Livestream entries are standard
videosrows plus alivestreamsmetadata row (provider = moq). - Admins create livestreams from Admin → Videos → Create new livestream, supplying MoQ endpoint URL and broadcast name (no hardcoded URLs in code).
- Playback runs in
/watch/:videoIdvia@moq/watchwhenmoq_endpointandmoq_broadcastare set. - After a stream ends, attach a recorded VOD via
recording_video_idon the livestream row (or swap in admin) so/watch/:videoIdserves the uploaded HLS asset.