Skip to content

Configuration ​

All configuration is done through environment variables. Copy .env.example to .env and adjust as needed.

bash
cp .env.example .env

Application ​

VariableDefaultDescription
PORT3000API server port
CORS_ORIGIN*Allowed origins for CORS. * for all, or comma-separated list. Fallback only — when a global CORS allowlist is configured in Admin → CORS & Embeds (GET/PATCH /v1/admin/settings/cors, stored on the global settings row), the DB list takes precedence and applies without a redeploy
DASHBOARD_URLhttp://localhost:3001Dashboard base URL (always allowed as a CORS/embed origin)
PLAYER_BASE_URL—Standalone player app base URL (always allowed as a CORS/embed origin)

Timezone ​

Set the TZ environment variable to control the timezone used by all processes (API, worker, transcoder) and PostgreSQL sessions. The value is passed as the Postgres TimeZone connection parameter by packages/db/src/client.ts and apps/transcoder/internal/db/db.go, ensuring now(), CURRENT_TIMESTAMP, and all timestamp comparisons are consistent.

VariableDefaultDescription
TZAmerica/Sao_PauloOS-level timezone identifier (e.g. America/Sao_Paulo, UTC, Europe/London). Brazil abolished DST in 2019, so America/Sao_Paulo is UTC-3 year-round

In Docker Compose and Fly.io deployments, TZ is set per-service/environment automatically. For self-hosted or alternative deployments, set TZ in your container/environment config.

CORS & embed allowlists (Admin → CORS & Embeds) ​

Superadmins can manage two instance-wide lists from the dashboard instead of touching CORS_ORIGIN:

  • Allowed origins (API CORS) — which browser origins may call the API. Empty = falls back to CORS_ORIGIN; * = allow all.
  • Allowed embed domains — which origins may embed the player. Enforced as frame-ancestors on the /embed and /watch pages (API-served pages via apps/api-edge's response hook, the standalone player app via its Pages middleware functions/_middleware.ts — set the player's API_BASE var) and as an Origin/Referer check on the public /v1/playback/* endpoints. Empty = unrestricted.

Entries accept exact origins (https://example.com), subdomain wildcards (*.example.com), or *. The platform's own frontends (DASHBOARD_URL / PLAYER_BASE_URL) are always allowed — set those env vars on the API to match the real deployed origins, or add them to the lists manually (otherwise a restricted embed list blocks the platform's own apps). Changes apply immediately (5s cache in apps/api-edge/src/services/cors-config.ts).

Scope note: by default, this protects the API metadata surface (playback JSON, audio/download URLs) and iframe framing (frame-ancestors). The HLS segments themselves are served from the public R2 bucket (S3_PUBLIC_BASE_URL). However, if you configure the Cloudflare Integration (using CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID), the platform will automatically synchronize your allowed embed list with a Cloudflare WAF Custom Rule on your media custom domain, blocking byte-level hotlinking of segments and playlists for unauthorized referers.

Database ​

VariableDefaultDescription
DATABASE_URL—PostgreSQL connection string (Neon-compatible): postgresql://user:pass@host:5432/database?sslmode=require

Neon pooled endpoints: point DATABASE_URL at the direct endpoint (drop -pooler from the host). The API manages its own pool, and pgBouncer's transaction mode can hand out connections with an empty search_path. See DEPLOYMENT-juninho.md for the full gotcha.

Redis ​

VariableDefaultDescription
REDIS_URLredis://redis:6379Redis connection string for BullMQ + Redis Streams

S3 / R2 Storage ​

VariableDefaultDescription
S3_ENDPOINT—S3-compatible API endpoint
S3_REGIONus-east-1Region (auto for Cloudflare R2)
S3_BUCKETstrum-vodBucket name
S3_ACCESS_KEY_ID—Access key
S3_SECRET_ACCESS_KEY—Secret key
S3_FORCE_PATH_STYLEfalseUse path-style URLs. true for MinIO; false for AWS S3 and Cloudflare R2
S3_PUBLIC_ENDPOINTsame as S3_ENDPOINTPublic endpoint used when generating presigned upload URLs
S3_PUBLIC_BASE_URL—Public URL prefix for HLS playback (e.g. https://pub-hash.r2.dev)
S3_BACKUP_BUCKET—Bucket for daily pg_dump backups (optional)

Cloudflare R2 notes:

  • R2 does not support per-object ACL: public-read or PutBucketCors via the S3 API.
  • Enable public access and configure CORS rules via pnpm r2:cors:set (config in infra/r2/).
  • Set S3_FORCE_PATH_STYLE=false and S3_REGION=auto for R2.

Cloudflare Integration (Optional) ​

Automates Cloudflare caching (Tiered Cache) and WAF protection rules to prevent HLS hotlinking. When configured, changing allowed embed domains in the Dashboard (Admin -> CORS & Embeds) automatically synchronizes the WAF custom rules on your R2 custom domain in the background.

VariableDefaultDescription
CLOUDFLARE_API_TOKEN—Cloudflare API token with Zone Settings:Edit and Zone WAF:Edit permissions
CLOUDFLARE_ZONE_ID—Cloudflare Zone ID for the custom domain
CLOUDFLARE_MEDIA_DOMAIN—R2 Custom Domain (e.g. media.yourdomain.com). Optional: parsed from S3_PUBLIC_BASE_URL if not set
CLOUDFLARE_ALLOWED_DOMAINS—Comma-separated domains always allowed to play HLS. Optional: parsed from DASHBOARD_URL/VITE_API_BASE_URL

A manual synchronization can be run using the CLI command pnpm cf:configure or by sending a POST request to /v1/admin/settings/cloudflare/sync (requires superadmin token).

Security ​

VariableDefaultDescription
JWT_SECRET—Secret for signing session JWTs (required for auth). Generate with openssl rand -hex 32
SHARED_AUTH_SECRET—Base64-encoded 32-byte key the API uses to sign and verify the resumable TUS upload JWT (both ends live in apps/api now — no second service to share it with). Required to enable the resumable TUS upload mode. Generate with: openssl rand -base64 32
TRIGGER_SECRET_KEY—trigger.dev prod secret key (tr_prod_..., from the trigger.dev dashboard's API Keys page) — set on apps/api-edge (Cloudflare Workers secret). Without it, POST /v1/assets/:id/process and every AI-pipeline trigger 500 immediately (no fallback path). See Deployment
SUPERADMIN_BOOTSTRAP_KEY—Optional. Gates POST /v1/admin/bootstrap-superadmin (X-Bootstrap-Key header) — the only way to flip users.is_superadmin without already holding a superadmin JWT, for bootstrapping the first superadmin on a fresh stack. Left unset the route 501s (disabled). apps/cli's strum dev promote/demote/whoami use it. Generate with openssl rand -base64 32; set on apps/api-edge only

Unified worker fleet (machine tokens) ​

The transcode/AI worker binary (apps/transcoder/cmd/transcoder) authenticates to the broker (apps/api-edge/src/routes/worker-agent.ts, /v1/worker-agent/*) with a machine token (mt_live_...), not any of the env vars above — there's no static credential to put in an env file. A superadmin mints one via POST /v1/admin/machine-tokens, then the binary is started with --api-url <api base> --token mt_live_.... One token can be shared across many physical machines (each self-registers its own machines row on first heartbeat/claim, keyed by a locally-generated machine ID persisted to disk). See Deployment.

apps/trigger-tasks's own env vars (separate from everything above) ​

apps/trigger-tasks runs inside trigger.dev's own hosted environment, not this app's — its env vars (DATABASE_URL, S3_*, DEEPGRAM_API_KEY, API_PUBLIC_URL, TRANSCRIPTION_PROVIDER, etc.) are configured on the trigger.dev dashboard (or synced at deploy time via trigger.config.ts's syncEnvVars extension — the CLI has no env set command), not in this repo's .env. See Deployment for the exact deploy command.

MCP Server (optional) ​

Exposes the API as a Model Context Protocol server so AI assistants/clients can read and manage your org's assets through the same API keys and JWTs the dashboard uses. See the MCP Server page for the full tool reference and client setup.

VariableDefaultDescription
MCP_ENABLEDfalseSet to true to register the MCP endpoint (fastify-mcp-server plugin)
MCP_ENDPOINT/mcpHTTP path the MCP streamable-HTTP endpoint is mounted at

Dashboard Build-Time Variables (Vite) ​

These are injected at build time by Vite. They must be set before running pnpm run build -w @strum-vod/dashboard.

VariableDefaultDescription
VITE_API_BASE_URLhttp://localhost:13002API base URL
VITE_TUS_SERVER_URL—Base URL the TUS client appends /upload/videos to — normally the same as VITE_API_BASE_URL, since TUS is served by the API. When set, enables the "Resumable (TUS)" upload toggle in the dashboard
VITE_PLAYER_BASE_URL—Player app URL (e.g. https://player.strum-vod.dev). Used to generate embed codes pointing to the player app
VITE_SENTRY_DSN—Browser error reporting DSN (GlitchTip/Sentry-compatible)

Player App Build-Time Variables (Vite) ​

VariableDefaultDescription
VITE_API_BASE_URL—API base URL. Set in apps/player/.env or at build time

Registration ​

VariableDefaultDescription
REGISTRATION_ENABLEDtrueSet to false to disable new account creation
REGISTRATION_ALLOWED_DOMAINS—Comma-separated allowed email domains (e.g. company.com,partner.org)

AI Processing (optional — now a trigger.dev project, not apps/worker) ​

Omit all AI variables to disable AI features entirely. These run inside the apps/trigger-tasks tasks, so they're set as trigger.dev project env vars (dashboard or trigger.config.ts's syncEnvVars), not this app's .env — the legacy apps/worker AI path that read them from a local env was deleted (2026-08-17). They can also be set (or overridden) from the dashboard's Settings → AI Provider Configuration, which writes the DB config packages/ai-pipeline's resolveAiConfig merges over these env-var defaults.

VariableDescription
TRANSCRIPTION_PROVIDERlocal (default) | deepgram | assemblyai | modal
WHISPER_API_URLOpenAI-compatible transcription endpoint
WHISPER_API_KEYAPI key for Whisper service
WHISPER_MODELModel name (e.g. whisper-1)
DEEPGRAM_API_KEYDeepgram API key (when TRANSCRIPTION_PROVIDER=deepgram)
DEEPGRAM_MODELDeepgram model (e.g. nova-2)
ASSEMBLYAI_API_KEYAssemblyAI API key (when TRANSCRIPTION_PROVIDER=assemblyai)
LLM_PROVIDERChapter generation: openai, anthropic, groq, or custom
LLM_API_KEYAPI key for LLM service
LLM_MODELModel name (e.g. gpt-4o-mini, llama-3.3-70b-versatile)
LLM_API_URLCustom LLM endpoint (leave empty for provider default)
LLM_CHAPTERS_MODELPer-step override for chapter generation — falls back to LLM_MODEL when unset
LLM_HIGHLIGHTS_MODELPer-step override for highlight generation (single-pass and long-video Scout/Curator) — falls back to LLM_MODEL
LLM_CONTENT_TYPE_DETECTION_MODELPer-step override for the content-type auto-detect classification pass — a cheaper model is a good fit here (e.g. gpt-4o-mini)
LLM_SMART_METADATA_MODELPer-step override for smart metadata generation — falls back to LLM_MODEL
LLM_TRANSLATION_MODELPer-step override for subtitle translation — falls back to LLM_MODEL
AI_ENABLEDSet to false to disable AI even if keys are configured

Async (modal) transcription — extra vars ​

VariableDescription
WHISPER_WEBHOOK_SECRETHMAC secret Modal signs the callback with (X-Signature). Must match the whisper-webhook-secret Modal Secret. Generate with openssl rand -hex 32
API_PUBLIC_URLPublicly reachable base URL of apps/api-edge — Modal calls back to <API_PUBLIC_URL>/v1/ai/whisper-callback from its own cloud (tunnel or deployed API required; localhost won't work). Set on apps/api-edge and the trigger.dev project

Subtitle Translation (optional — requires LLM) ​

VariableDefaultDescription
AI_AUTO_TRANSLATEfalseGlobal setting: enable auto-translation to English when AI processing runs. Can be overridden per-asset via aiOptions.translations
aiOptions.translations (API)—Per-asset target languages for subtitle translation (ISO-639-1 codes, e.g. ["en", "es", "fr"]). Requires subtitles + LLM configured
aiOptions.translationGlossary (API)—Optional glossary for domain-specific terms (term → translation mapping)

Transcoding Ladder (Go transcoder) ​

VariableDefaultDescription
RENDITION_CODECh264h264 or hevc — base codec stamped on every rendition
MAX_RENDITION_HEIGHT0Cap the tallest rendition (0 = full 360p–4320p). E.g. 2160 drops the 4320p rung. Code default is 0; the production deploy and Docker Compose cap at 2160 (cost cut — see docs/costs.md Fase 4)
HEVC_MIN_HEIGHT0Hybrid ladder: renditions at/above this height encode in HEVC, lower rungs keep the base codec (0 = disabled)
FFMPEG_HWACCELautoauto | vaapi | disabled — hardware acceleration for the ladder

Audio Extraction (optional) ​

VariableDefaultDescription
AUDIO_PLAYBACK_BITRATE_KBPS128AAC bitrate for playback audio (32–320)
AUDIO_PLAYBACK_SAMPLE_RATE48000Sample rate in Hz (8000–96000)
AUDIO_PLAYBACK_CHANNELS21=Mono, 2=Stereo
AUDIO_AI_BITRATE_KBPS64MP3 bitrate for Whisper AI (32–128)
AUDIO_AI_SAMPLE_RATE16000Sample rate in Hz (8000–48000)

Scaling (auto-detected) ​

The Go transcoder auto-detects CPU cores and RAM (cgroup-aware) at startup. Override only if the auto-detected values are wrong.

VariableUsed byDefaultDescription
WORKER_CONCURRENCYTranscoder (Go)autoConcurrent transcode jobs
FFMPEG_THREADSTranscoder (Go)autoThreads per FFmpeg process
DB_POOL_SIZEAPI, TranscoderautoPostgreSQL connection pool size
AI_WORKER_CONCURRENCYWorker (Node)—Removed (Phase 4) — no BullMQ worker concurrency anymore

QStash Job Layer (replaces BullMQ queues + Redis Streams) ​

VariableDefaultDescription
QSTASH_TOKEN— (required)Publish authorization token (console.upstash.com/qstash) — env/config validation fails at boot without it
QSTASH_CURRENT_SIGNING_KEY / QSTASH_NEXT_SIGNING_KEY— (required)Verify inbound Upstash-Signature headers (current + next for rotation)
QSTASH_URLhttps://qstash.upstash.ioSDK base URL override (local dev/tests point this at a stub)
TRANSCODER_PUBLIC_URLhttps://<FLY_TRANSCODER_APP>.fly.devQStash publish target for the transcoder's /qstash/jobs
WORKER_PUBLIC_URLhttps://<FLY_WORKER_APP>.fly.devQStash publish target for the worker's /qstash/* consumers
API_PUBLIC_URL—QStash publish target for the API's own consumers/schedules

Local dev without a real Upstash account: pnpm dev:qstash runs @upstash/qstash-cli dev, an in-memory local QStash emulator that pushes straight to localhost — no tunnel needed. It prints 4 sets of test credentials on startup; paste one set's token/signing keys into .env for all three processes (api/worker/transcoder — the signing keys must match to verify inbound requests), point QSTASH_URL at it (http://localhost:42932), and point the *_PUBLIC_URL vars above at each process's local port instead of a Fly hostname. Full recipe, including the port-collision note (worker/transcoder both default WORKER_HTTP_PORT / TRANSCODER_HTTP_PORT to 8080, same as the emulator's own default) and the docker-compose variant, is in .env.example's "QStash local dev" block.

Fly.io Worker / Transcoder Wake (optional) ​

Scale-to-zero machines are woken on every job enqueue. The API (apps/api) fires worker + transcoder wakes in parallel (prewarmFleet in services/fly-machine.ts), preferring the Fly Machines API (POST /machines/:id/start) when FLY_API_TOKEN is set and falling back to the public /wake proxy. apps/worker's transcode bridge re-POSTs /wake after each relay as a belt-and-suspenders fallback.

VariableDescription
FLY_WORKER_APPFly app name of apps/worker (set on apps/api) — used to construct the /wake URL / Machines API app
FLY_TRANSCODER_APPFly app name of apps/transcoder (set on apps/api and apps/worker) — enables the parallel transcoder wake on job enqueue
WORKER_HTTP_PORTInternal HTTP port (fly.worker.toml, default 8080)
FLY_API_TOKENFly Machines API token — enables direct machine start (parallel wake) + /diagnostics machine listing/force-start

Email / OTP (optional) ​

VariableDefaultDescription
EMAIL_PROVIDERauto-detectresend | smtp | console
EMAIL_FROMnoreply@strum-vod.localFrom address
RESEND_API_KEY—Resend API key
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS / SMTP_SECURE—SMTP server config

Billing (optional) ​

Adding Stripe keys enables tiered billing, usage metering, and plan limits. Without Stripe, all features are available with no usage restrictions.

VariableDescription
STRIPE_SECRET_KEYStripe secret key
STRIPE_WEBHOOK_SECRETStripe webhook secret
STRIPE_PRO_PRICE_ID / STRIPE_BUSINESS_PRICE_IDPlan price IDs

Webhooks (optional) ​

VariableDefaultDescription
WEBHOOK_URL—Default org webhook target

Error Tracking (optional — GlitchTip via Docker) ​

VariableDescription
GLITCHTIP_SECRET_KEYGlitchTip Django secret
GLITCHTIP_DB_PASSWORDGlitchTip Postgres password
GLITCHTIP_DOMAINGlitchTip public URL
SENTRY_DSNAPI error reporting DSN
VITE_SENTRY_DSNDashboard build-time DSN

Example Configurations ​

Docker Compose (local dev — MinIO) ​

env
DATABASE_URL=postgresql://strum_vod:strum-vodpassword@postgres:5432/strum_vod
PORT=3000

S3_ENDPOINT=http://minio:9000
S3_REGION=us-east-1
S3_BUCKET=strum-vod
S3_ACCESS_KEY_ID=minioadmin
S3_SECRET_ACCESS_KEY=minioadmin
S3_FORCE_PATH_STYLE=true
S3_PUBLIC_ENDPOINT=http://localhost:42943
S3_PUBLIC_BASE_URL=http://localhost:42943/strum-vod

DASHBOARD_URL=http://localhost:13003
CORS_ORIGIN=http://localhost:13003,http://localhost:13002
VITE_API_BASE_URL=http://localhost:13002

JWT_SECRET=dev-secret-change-me

Production — Cloudflare R2 + Neon ​

env
DATABASE_URL=postgresql://user:pass@ep-xyz.region.aws.neon.tech/neondb?sslmode=require

S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
S3_REGION=auto
S3_BUCKET=strum-videos
S3_ACCESS_KEY_ID=<R2 key>
S3_SECRET_ACCESS_KEY=<R2 secret>
S3_FORCE_PATH_STYLE=false
S3_PUBLIC_BASE_URL=https://media.usestrum.app

DASHBOARD_URL=https://dashboard.strum-vod.dev
CORS_ORIGIN=https://dashboard.strum-vod.dev,https://player.strum-vod.dev
VITE_API_BASE_URL=https://api.strum-vod.fly.dev
VITE_TUS_SERVER_URL=https://api.strum-vod.fly.dev
VITE_PLAYER_BASE_URL=https://player.strum-vod.dev

JWT_SECRET=<openssl rand -hex 32>
SHARED_AUTH_SECRET=<openssl rand -base64 32>

Production — AWS S3 + RDS ​

env
DATABASE_URL=postgresql://admin:password@mydb.us-east-1.rds.amazonaws.com:5432/strum_vod
REDIS_URL=redis://my-redis.cache.amazonaws.com:6379

S3_ENDPOINT=https://s3.us-east-1.amazonaws.com
S3_REGION=us-east-1
S3_BUCKET=my-strum-vod-bucket
S3_ACCESS_KEY_ID=AKIA...
S3_SECRET_ACCESS_KEY=...
S3_FORCE_PATH_STYLE=false
S3_PUBLIC_BASE_URL=https://my-strum-vod-bucket.s3.us-east-1.amazonaws.com

VITE_API_BASE_URL=https://api.example.com
JWT_SECRET=<openssl rand -hex 32>

Validation ​

apps/worker validates its env vars at startup via Zod (apps/worker/src/env.ts) — a missing/invalid required variable fails startup with a descriptive error. apps/api-edge (Cloudflare Workers) has no such runtime validation; its vars come from wrangler.toml [vars]/wrangler secret, checked only implicitly by whatever code path reads them.

TUS resumable upload (apps/tus-edge/src/index.ts) is optional: without SHARED_AUTH_SECRET set, the API logs a warning at startup and doesn't register /upload/videos at all (404, not 401) — the dashboard falls back to presigned-URL upload automatically.

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