Configuration
All configuration is done through environment variables. Copy .env.example to .env and adjust as needed.
cp .env.example .envApplication
| Variable | Default | Description |
|---|---|---|
PORT | 3000 | API server port |
CORS_ORIGIN | * | Allowed origins for CORS. * for all, or comma-separated list |
DASHBOARD_URL | http://localhost:3001 | Dashboard base URL |
Database
| Variable | Default | Description |
|---|---|---|
DATABASE_URL | — | PostgreSQL connection string (Neon-compatible): postgresql://user:pass@host:5432/database?sslmode=require |
Neon pooled endpoints: point
DATABASE_URLat the direct endpoint (drop-poolerfrom the host). The API manages its own pool, and pgBouncer's transaction mode can hand out connections with an emptysearch_path. SeeDEPLOYMENT-juninho.mdfor the full gotcha.
Redis
| Variable | Default | Description |
|---|---|---|
REDIS_URL | redis://redis:6379 | Redis connection string for BullMQ + Redis Streams |
S3 / R2 Storage
| Variable | Default | Description |
|---|---|---|
S3_ENDPOINT | — | S3-compatible API endpoint |
S3_REGION | us-east-1 | Region (auto for Cloudflare R2) |
S3_BUCKET | strum-vod | Bucket name |
S3_ACCESS_KEY_ID | — | Access key |
S3_SECRET_ACCESS_KEY | — | Secret key |
S3_FORCE_PATH_STYLE | false | Use path-style URLs. true for MinIO; false for AWS S3 and Cloudflare R2 |
S3_PUBLIC_ENDPOINT | same as S3_ENDPOINT | Public 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-readorPutBucketCorsvia the S3 API.- Enable public access and configure CORS rules via
pnpm r2:cors:set(config ininfra/r2/).- Set
S3_FORCE_PATH_STYLE=falseandS3_REGION=autofor R2.
Security
| Variable | Default | Description |
|---|---|---|
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 |
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.
| Variable | Default | Description |
|---|---|---|
VITE_API_BASE_URL | http://localhost:13002 | API 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)
| Variable | Default | Description |
|---|---|---|
VITE_API_BASE_URL | — | API base URL. Set in apps/player/.env or at build time |
Registration
| Variable | Default | Description |
|---|---|---|
REGISTRATION_ENABLED | true | Set to false to disable new account creation |
REGISTRATION_ALLOWED_DOMAINS | — | Comma-separated allowed email domains (e.g. company.com,partner.org) |
AI Processing (optional)
Omit all AI variables to disable AI features entirely. These can also be set (or overridden) from the dashboard's Settings → AI Provider Configuration.
| Variable | Description |
|---|---|
TRANSCRIPTION_PROVIDER | local (default) | deepgram | modal |
WHISPER_API_URL | OpenAI-compatible transcription endpoint |
WHISPER_API_KEY | API key for Whisper service |
WHISPER_MODEL | Model name (e.g. whisper-1) |
DEEPGRAM_API_KEY | Deepgram API key (when TRANSCRIPTION_PROVIDER=deepgram) |
DEEPGRAM_MODEL | Deepgram model (e.g. nova-2) |
LLM_PROVIDER | Chapter generation: openai, anthropic, groq, or custom |
LLM_API_KEY | API key for LLM service |
LLM_MODEL | Model name (e.g. gpt-4o-mini, llama-3.3-70b-versatile) |
LLM_API_URL | Custom LLM endpoint (leave empty for provider default) |
AI_ENABLED | Set to false to disable AI even if keys are configured |
Async (modal) transcription — extra vars
| Variable | Description |
|---|---|
WHISPER_WEBHOOK_SECRET | HMAC secret Modal signs the callback with (X-Signature). Must match the whisper-webhook-secret Modal Secret. Generate with openssl rand -hex 32 |
API_PUBLIC_URL | Publicly reachable base URL of apps/api — 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 both apps/api and apps/worker |
Transcoding Ladder (Go transcoder)
| Variable | Default | Description |
|---|---|---|
RENDITION_CODEC | h264 | h264 or hevc — base codec stamped on every rendition |
MAX_RENDITION_HEIGHT | 0 | Cap the tallest rendition (0 = full 360p–4320p). E.g. 2160 drops the 4320p rung |
HEVC_MIN_HEIGHT | 0 | Hybrid ladder: renditions at/above this height encode in HEVC, lower rungs keep the base codec (0 = disabled) |
FFMPEG_HWACCEL | auto | auto | vaapi | disabled — hardware acceleration for the ladder |
Audio Extraction (optional)
| Variable | Default | Description |
|---|---|---|
AUDIO_PLAYBACK_BITRATE_KBPS | 128 | AAC bitrate for playback audio (32–320) |
AUDIO_PLAYBACK_SAMPLE_RATE | 48000 | Sample rate in Hz (8000–96000) |
AUDIO_PLAYBACK_CHANNELS | 2 | 1=Mono, 2=Stereo |
AUDIO_AI_BITRATE_KBPS | 64 | MP3 bitrate for Whisper AI (32–128) |
AUDIO_AI_SAMPLE_RATE | 16000 | Sample 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.
| Variable | Used by | Default | Description |
|---|---|---|---|
WORKER_CONCURRENCY | Transcoder (Go) | auto | Concurrent transcode jobs |
FFMPEG_THREADS | Transcoder (Go) | auto | Threads per FFmpeg process |
DB_POOL_SIZE | API, Transcoder | auto | PostgreSQL connection pool size |
AI_WORKER_CONCURRENCY | Worker (Node) | 3 | ai-process BullMQ queue concurrency |
Node ↔ Go Bridge (Redis Streams)
| Variable | Default | Description |
|---|---|---|
TRANSCODE_STREAM_KEY | go:transcode:jobs | BullMQ transcode → Go transcoder stream |
AI_DISPATCH_STREAM_KEY | go:ai:dispatch | Go transcoder → ai-process queue stream |
WEBHOOK_DISPATCH_STREAM_KEY | go:webhook:dispatch | Go transcoder → webhook-delivery queue stream |
Defaults are fine for a single stack; only override if running multiple independent stacks against the same Redis instance.
Fly.io Worker Wake (optional)
| Variable | Description |
|---|---|
FLY_WORKER_APP | Fly app name of apps/worker — used to construct the internal /wake URL |
WORKER_HTTP_PORT | Internal HTTP port (fly.worker.toml, default 8080) |
FLY_API_TOKEN | Fly Machines API token — enables /diagnostics machine listing + force-start |
Email / OTP (optional)
| Variable | Default | Description |
|---|---|---|
EMAIL_PROVIDER | auto-detect | resend | smtp | console |
EMAIL_FROM | noreply@strum-vod.local | From 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.
| Variable | Description |
|---|---|
STRIPE_SECRET_KEY | Stripe secret key |
STRIPE_WEBHOOK_SECRET | Stripe webhook secret |
STRIPE_PRO_PRICE_ID / STRIPE_BUSINESS_PRICE_ID | Plan price IDs |
Webhooks (optional)
| Variable | Default | Description |
|---|---|---|
WEBHOOK_URL | — | Default org webhook target |
Error Tracking (optional — GlitchTip via Docker)
| Variable | Description |
|---|---|
GLITCHTIP_SECRET_KEY | GlitchTip Django secret |
GLITCHTIP_DB_PASSWORD | GlitchTip Postgres password |
GLITCHTIP_DOMAIN | GlitchTip public URL |
SENTRY_DSN | API error reporting DSN |
VITE_SENTRY_DSN | Dashboard build-time DSN |
Example Configurations
Docker Compose (local dev — MinIO)
DATABASE_URL=postgresql://strum_vod:strum-vodpassword@postgres:5432/strum_vod
REDIS_URL=redis://redis:6379
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:19000
S3_PUBLIC_BASE_URL=http://localhost:19000/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-meProduction — Cloudflare R2 + Neon
DATABASE_URL=postgresql://user:pass@ep-xyz.region.aws.neon.tech/neondb?sslmode=require
REDIS_URL=redis://default:pass@redis.upstash.io:6379
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://pub-<hash>.r2.dev
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
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
The API and Worker validate all env vars at startup via Zod (apps/api/src/env.ts, apps/worker/src/env.ts). If any required variable is missing or invalid, the process fails to start with a descriptive error.
TUS resumable upload (apps/api/src/routes/tus.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.