Skip to content

Strum VOD

Self-Hosted Video Platform

Stop paying per-minute for video infrastructure. Own your video pipeline.

DockerLicense

Quick StartFeaturesAPIConfigurationDocker GuideDocumentation



What is Strum VOD?

Strum VOD is a self-hosted alternative to Mux. Upload a video, get adaptive HLS streams from 360p up to 4320p (8K), and deliver them from your own infrastructure. No vendor lock-in, no per-minute pricing.

Upload via dashboard, REST API, or resumable TUS protocolTranscode automatically to adaptive HLS (360p–4320p, H.264/HEVC)
Stream via any HLS player or the standalone embeddable playerScale with hardware-adaptive workers that auto-tune to your machine

Why Strum VOD?

No Per-Minute FeesOne Command DeployOwn Your Data
Self-host at your own cost. No usage-based billing, no vendor lock-in.Single Docker image with embedded PostgreSQL & queue. Just add S3.Your server, your storage, your videos. Full control.
AI-PoweredR2 & S3-CompatibleEmbeddable Player
Optional transcription (Whisper/Deepgram/Modal), subtitles, chapters & highlight clips.First-class Cloudflare R2 support plus AWS S3, MinIO, Backblaze B2, and more.Standalone player app (Cloudflare Workers + Assets) with quality selector, thumbnail seek preview, comments & reactions.

How It Works

 Upload                Transcode             Deliver
+-----------+      +--------------+      +------------+
|  Browser  |----->|  FFmpeg HLS  |----->|  R2 / S3   |
|  TUS/PUT  |      |  (Go)        |      |  (Stream)  |
+-----------+      +--------------+      +------------+
     |                    |                    |
     v                    v                    v
  Dashboard            Postgres +         Player app
  (CF Workers)          Redis             (CF Pages)
  1. Upload via presigned URL or resumable TUS directly to R2/S3
  2. Transcode automatically to adaptive HLS (360p–4320p)
  3. Stream via the standalone player app or any HLS-capable player

Video playback is served directly from R2/S3. The API only handles metadata — R2 absorbs all bandwidth.


Deployment Targets

AppTechnologyRecommended Host
apps/apiFastify (Node.js) — includes resumable TUS video uploadFly.io, Railway, any VPS
apps/workerNode (bridge + AI pipeline)Fly.io
apps/transcoderGo — the FFmpeg HLS ladderFly.io, dedicated server (CPU-bound)
apps/dashboardVite + React SPA (PWA)Cloudflare Workers + Assets
apps/playerVite + React SPA (PWA)Cloudflare Pages

Cloudflare resources (R2 buckets + CORS, Dashboard, Player) are deployed with wrangler (apps/*/wrangler.toml). R2 CORS is managed from the repo via the pnpm r2:* scripts (see infra/r2/).


Quick Start

One image, everything included (API, worker, transcoder, dashboard, PostgreSQL, Redis). Storage is Cloudflare R2 — set your R2 credentials in the environment variables.

bash
docker run -d \
  --name strum-vod \
  -p 3000:3000 \
  -v strum-vod-data:/data \
  -e S3_ENDPOINT=https://s3.amazonaws.com \
  -e S3_REGION=us-east-1 \
  -e S3_BUCKET=my-strum-vod-bucket \
  -e S3_ACCESS_KEY_ID=AKIA... \
  -e S3_SECRET_ACCESS_KEY=... \
  -e S3_PUBLIC_BASE_URL=https://my-strum-vod-bucket.s3.amazonaws.com \
  synapsr/strum-vod

That's it. Open http://localhost:3000 — dashboard and API on the same port.

PostgreSQL and Redis run inside the container automatically. Data is persisted in the /data volume.


Features

Dashboard & Player

FeatureDescription
DashboardWeb UI to upload, manage, and preview all your video assets (installable PWA)
Standalone Player AppIndependently deployable Cloudflare Pages app with embeddable HLS player (installable PWA)
Resumable Upload (TUS)Browser-side TUS protocol, served by the API → R2 (survives disconnects, resumes on retry)
Standard UploadPre-signed S3/R2 URLs for efficient browser-to-storage uploads
URL ImportImport videos from any public URL
CollectionsOrganize videos into color-coded folders
Transcript & SubtitlesAI-generated transcription and WebVTT/SRT subtitles (Whisper / Deepgram / Modal)
ChaptersAuto-generated chapters from transcript (via LLM)
Highlight ClipsAuto-generated highlight clips with downloadable MP4s
Comments & ReactionsPublic engagement on watch pages
WebhooksOutgoing asset.ready / asset.error / ai.completed events with delivery log & retry

Video Pipeline

FeatureDescription
Adaptive BitrateHLS output 360p → 4320p (7 renditions, H.264 or HEVC, CPU or VA-API)
Single-Pass LadderOne ffmpeg process encodes every rendition at once
Thumbnail SpritesTiled scrub-bar thumbnails with VTT metadata
Shared Audio TrackOne EXT-X-MEDIA AAC track shared by all renditions
Downloadable MP4sFast remux (-c copy) of any rendition + audio
Hardware-AdaptiveWorkers auto-detect CPU/RAM (cgroup-aware) to optimize concurrency and threading
Horizontal ScalingRun multiple stateless workers against the same Redis queue
REST APIClean JSON endpoints for full programmatic control
Cloudflare R2First-class R2 support — no per-object ACLs needed

Platform

FeatureDescription
Multi-tenantOrganizations, members, roles, API keys
AnalyticsViews, watch time, retention, engagement, heatmap, live viewer count (SSE)
Billing (optional)Stripe tiered plans + usage metering
DB Backups (optional)Scheduled pg_dump → R2
AdminSuperadmin dashboards (orgs, errors, webhook failures, BullMQ queues)
OTP LoginEmail magic-code sign-in (Resend / SMTP / console)

Deployment

Full Docker guide: See DOCKER.md for architecture diagrams, all deployment modes, scaling, and networking reference.

All-in-One (simplest)

Everything in a single container. PostgreSQL and Redis are embedded. Only S3 storage is external.

bash
docker run -d \
  --name strum-vod \
  -p 3000:3000 \
  -v strum-vod-data:/data \
  -e S3_ENDPOINT=https://s3.amazonaws.com \
  -e S3_REGION=us-east-1 \
  -e S3_BUCKET=my-bucket \
  -e S3_ACCESS_KEY_ID=AKIA... \
  -e S3_SECRET_ACCESS_KEY=... \
  -e S3_PUBLIC_BASE_URL=https://my-bucket.s3.amazonaws.com \
  -e S3_FORCE_PATH_STYLE=false \
  synapsr/strum-vod

Local dev

bash
git clone https://github.com/Synapsr/strum-vod.git && cd strum-vod
cp .env.example .env
pnpm install
pnpm run dev:web          # dashboard + player with live reload

For the full stack (API, worker, transcoder) see the per-app dev scripts in the project's CLAUDE.md.

Production (split deployment)

Frontends deploy to Cloudflare Workers + Assets with wrangler; the API, Node worker, and Go transcoder deploy separately to Fly.io or any VPS. R2 buckets and their CORS are managed with wrangler (see pnpm r2:* and infra/r2/).

ComponentDeploy with
Dashboard (apps/dashboard)wrangler deploy --config apps/dashboard/wrangler.toml
Player (apps/player)pnpm deploy:player (wrangler pages)
API + Workerfly deploy --config apps/api/fly.toml / fly deploy --config fly.worker.toml
Transcoder (Go)fly deploy --config fly.transcoder.toml
R2 buckets + CORSpnpm r2:* (wrangler)

One-Click Deploy

Works out of the box with your favorite platforms:

PlatformHow to deploy
EasyPanelAdd Docker app → synapsr/strum-vod
DokployImport from Docker Hub
CoolifyOne-click from Docker image
PortainerCreate stack from compose
RailwayDeploy from Docker image

API at a Glance

bash
# Create an asset
curl -X POST http://localhost:13002/v1/assets \
  -H "Content-Type: application/json" \
  -d '{"title": "My Video"}'

# Standard upload: get presigned URL, upload, confirm
curl -X POST http://localhost:13002/v1/assets/{id}/upload-url
curl -X PUT "<uploadUrl>" --data-binary @video.mp4
curl -X POST http://localhost:13002/v1/assets/{id}/upload-complete

# OR resumable upload: get TUS token (requires SHARED_AUTH_SECRET)
curl -X POST http://localhost:13002/v1/assets/{id}/upload-token \
  -d '{"fileSize": 104857600}'
# ... then upload via tus-js-client to VITE_TUS_SERVER_URL/upload/videos

# Start transcoding
curl -X POST http://localhost:13002/v1/assets/{id}/process

# Get playback URL
curl http://localhost:13002/v1/assets/{id}/playback
EndpointMethodDescription
/v1/assetsPOSTCreate a new asset
/v1/assetsGETList all assets (optional collectionId filter)
/v1/assets/:idGETGet asset details + renditions + AI job
/v1/assets/:idPATCHUpdate title, description, public settings, collection
/v1/assets/:idDELETEHard-delete asset (DB row + R2 objects)
/v1/assets/:id/upload-urlPOSTGet pre-signed upload URL (standard)
/v1/assets/:id/upload-tokenPOSTGet JWT for TUS resumable upload
/v1/assets/:id/upload-completePOSTConfirm upload landed in storage
/v1/assets/:id/importPOSTImport from external URL
/v1/assets/:id/processPOSTStart transcoding
/v1/assets/:id/playbackGETGet HLS manifest + player URL
/v1/playback/:playbackIdGETPublic playback endpoint
/v1/collectionsGET/POSTList / create collections

Most endpoints require auth (Authorization: Bearer <JWT> or X-Api-Key). Public endpoints: /v1/playback/*, /v1/auth/signup|login, /v1/config, /health/*.


Configuration

Required

VariableDescription
DATABASE_URLPostgreSQL connection string (e.g. Neon: postgresql://...?sslmode=require)
S3_ENDPOINTS3-compatible endpoint URL
S3_REGIONS3 region (auto for R2)
S3_BUCKETS3 bucket name
S3_ACCESS_KEY_IDS3 access key
S3_SECRET_ACCESS_KEYS3 secret key
S3_PUBLIC_BASE_URLPublic URL prefix for HLS playback
JWT_SECRETRequired for auth-protected routes (openssl rand -hex 32)
Optional variables
VariableDefaultDescription
REDIS_URLembedded RedisRedis connection string
PORT3000API port
S3_FORCE_PATH_STYLEfalseSet true for path-style S3 endpoints; false for R2 and AWS S3
S3_PUBLIC_ENDPOINTsame as S3_ENDPOINTPublic S3 endpoint for presigned URLs
CORS_ORIGIN*Allowed CORS origins (comma-separated)
SHARED_AUTH_SECRETBase64 key the API signs/verifies the resumable upload JWT with (openssl rand -base64 32)
VITE_TUS_SERVER_URLDashboard build-time: base URL for TUS uploads, normally same as the API (enables TUS upload toggle)
VITE_PLAYER_BASE_URLDashboard build-time: player app URL (for embed link generation)
Registration control
VariableDefaultDescription
REGISTRATION_ENABLEDtrueSet to false to disable new account registration
REGISTRATION_ALLOWED_DOMAINSComma-separated list of allowed email domains
AI Processing (optional)

Strum VOD can auto-generate transcripts, subtitles, chapters, and highlight clips. Omit these variables to disable AI features.

VariableDescription
TRANSCRIPTION_PROVIDERlocal (default) | deepgram | modal
WHISPER_API_URLOpenAI-compatible transcription endpoint
WHISPER_API_KEYAPI key for Whisper service
WHISPER_MODELModel name (e.g. whisper-1)
DEEPGRAM_API_KEY / DEEPGRAM_MODELDeepgram provider config
LLM_PROVIDERChapter generation provider (openai, anthropic, groq, custom)
LLM_API_KEYAPI key for LLM service
LLM_MODELModel name (e.g. gpt-4o-mini)
AI_ENABLEDSet to false to disable AI even if configured

Async transcription via the self-hosted Modal provider (infra/modal-whisper, scale-to-zero GPU) — set these only when using it:

VariableDescription
WHISPER_WEBHOOK_SECRETHMAC secret Modal signs the callback with. Must match the whisper-webhook-secret Modal Secret — the API verifies it on POST /v1/ai/whisper-callback
API_PUBLIC_URLPublicly reachable base URL of the API — Modal calls back to <API_PUBLIC_URL>/v1/ai/whisper-callback from its own cloud (a tunnel or deployed API is required; localhost won't work)

Instead of holding an HTTP connection open across cold start + inference, the worker dispatches the audio to Modal (202 + callId), and Modal reports the result back via the signed webhook. The API then resumes the pipeline (subtitles/chapters/highlights) and a 5-minute reaper job re-dispatches any transcription whose callback never arrived. See infra/modal-whisper/README.md.

Transcoding ladder
VariableDefaultDescription
RENDITION_CODECh264h264 or hevc — base codec for every rendition
MAX_RENDITION_HEIGHT0Cap the tallest rendition (0 = full 360p–4320p)
HEVC_MIN_HEIGHT0Hybrid ladder: renditions at/above this height encode in HEVC
FFMPEG_HWACCELautoauto | vaapi | disabled
Scaling (auto-detected)
VariableDefaultDescription
WORKER_CONCURRENCYautoConcurrent transcode jobs (Go transcoder)
FFMPEG_THREADSautoThreads per FFmpeg process
DB_POOL_SIZEautoPostgreSQL connection pool size
S3 Provider Examples

Cloudflare R2 (recommended for production)

env
S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
S3_REGION=auto
S3_BUCKET=strum-videos
S3_ACCESS_KEY_ID=...
S3_SECRET_ACCESS_KEY=...
S3_PUBLIC_BASE_URL=https://pub-<hash>.r2.dev
S3_FORCE_PATH_STYLE=false

R2 does not support per-object ACLs or PutBucketCors via the S3 API. Enable public access and configure CORS in the Cloudflare dashboard instead.

AWS S3

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

Backblaze B2

env
S3_ENDPOINT=https://s3.us-west-004.backblazeb2.com
S3_REGION=us-west-004
S3_BUCKET=strum-vod
S3_ACCESS_KEY_ID=...
S3_SECRET_ACCESS_KEY=...
S3_PUBLIC_BASE_URL=https://f004.backblazeb2.com/file/strum-vod
S3_FORCE_PATH_STYLE=true

Tech Stack

TechnologyPurpose
TypeScriptType-safe development across all packages
FastifyHigh-performance REST API with Zod validation
GoFFmpeg HLS ladder (apps/transcoder)
FFmpegVideo transcoding (H.264/HEVC HLS, 360p–4320p)
PostgreSQLRelational database for state (Neon-compatible)
BullMQ + Redis StreamsJob queue + Node↔Go interop
React + ViteDashboard and Player SPAs with Tailwind CSS v4
hls.jsAdaptive HLS player in the browser
tus-js-client / @tus/server + @tus/s3-storeResumable upload client / server (server lives in apps/api)
Cloudflare Workers + AssetsDashboard and Player SPA hosting
wranglerDeploy R2 buckets, CORS, and frontends to Cloudflare
Drizzle ORMType-safe database queries
RedisJob queue backend (BullMQ)

Project Structure

strum-vod/
├── apps/
│   ├── api/                    # Fastify REST API (Fly.io / Docker)
│   ├── worker/                 # Node: bridge + AI pipeline (Fly.io / Docker)
│   ├── transcoder/             # Go: FFmpeg HLS ladder (Fly.io / Docker)
│   ├── dashboard/              # React SPA — management UI (CF Workers + Assets)
│   └── player/                 # React SPA — public embeddable player (CF Workers + Assets)
├── packages/
│   ├── db/                     # Shared Drizzle schemas & constants (Postgres)
│   ├── email/                  # Transactional email templates (OTP)
│   ├── subtitles/              # VTT/SRT generation
│   ├── workbench/              # BullMQ admin dashboard
│   ├── player-ui/              # Shared player components (heatmap, etc.)
│   └── design-tokens/          # Design tokens / CSS variables
├── infra/
│   ├── r2/                     # R2 bucket CORS configs (used by pnpm r2:cors:set)
│   └── modal-whisper/          # Async transcription provider (Modal, scale-to-zero GPU)
├── pnpm-workspace.yaml         # pnpm workspace config
├── Dockerfile                  # All-in-one image (api + worker + transcoder + dashboard)
└── .env.example

Development Setup

bash
# Clone the repo
git clone https://github.com/Synapsr/strum-vod.git
cd strum-vod

# Install dependencies (uses pnpm workspaces)
pnpm install

# Configure environment
cp .env.example .env

# Frontends (dashboard + player) with live reload
pnpm run dev:web
# → Dashboard: http://localhost:1337
# → Player:    http://localhost:1338

Or run individual apps directly (requires external PostgreSQL, Redis, and R2 credentials in .env):

bash
pnpm run build -w @strum-vod/db      # build shared package first
pnpm run dev -w @strum-vod/api       # API
pnpm run dev -w @strum-vod/worker    # Worker
pnpm run dev:transcoder              # Go transcoder (needs Go toolchain + ffmpeg)
pnpm run dev -w @strum-vod/dashboard # Dashboard on :1337
pnpm run dev:player              # Player app

Testing

The API and worker have E2E test suites that exercise real HTTP routes against real infrastructure.

bash
# Requires Docker — containers start automatically
pnpm test:e2e                          # API suite (PostgreSQL + Redis via Testcontainers)
pnpm --filter @strum-vod/worker run test:e2e   # Worker suite (Postgres + Redis + MinIO)

Tests use Vitest with Testcontainers: PostgreSQL 16 and Redis 7 spin up automatically per run, migrations are applied, and every test fires real SQL. The API suite mocks S3; the worker suite runs a real MinIO container.

Test file (API)Coverage
health.test.ts/health/*, /v1/config
auth.test.tsSignup, login, OTP, /v1/auth/me
assets.test.tsAsset CRUD, upload flow, import, process, org isolation
playback.test.tsPlayback resolution, comments, reactions
whisper-callback.test.tsAsync Modal transcription callback (HMAC, resume/retry)
settings.test.tsOrg settings & AI provider config

The worker suite additionally covers the Redis Streams bridge, the full transcode → AI pipeline (with a real Go transcoder binary + real ffmpeg encode against MinIO), and the Modal async flow.


100% Local & Sovereign Setup

Strum VOD can run entirely on your infrastructure with zero external API calls.

Zero Cloud DependenciesAir-Gap ReadyGPU-Accelerated
No OpenAI, no third-party APIs required. Everything can run on your hardware.Works completely offline after setup.Leverage NVIDIA GPUs for fast transcription with faster-whisper.

License

STRUM Proprietary License — © 2026 Strum. All rights reserved. This software is the exclusive property of STRUM and is not open source. See LICENSE for the full terms.

Documentation

Full technical documentation (architecture, configuration, deployment, API reference) lives in the docs/ directory and is also rendered as a VitePress site. Run it locally with:

bash
pnpm docs:dev      # dev server with hot reload
pnpm docs:build    # static build → docs/.vitepress/dist
pnpm docs:preview  # serve the built site (default: http://localhost:4173)

The docs site is deployed to Cloudflare Pages automatically via CI (.github/workflows/docs.yml) — production on every main push that touches docs, plus a preview URL per PR. Manual deploy: pnpm deploy:docs (see docs/deployment.md).

The dashboard sidebar links to these docs via the VITE_DOCS_URL build-time env var (default: http://localhost:4173). Set it to your deployed docs URL in production.


If Strum VOD is useful to you, consider starring the repo!

Star on GitHub


Built with FFmpeg, Fastify, Go, and Postgres by Synapsr

Report BugRequest FeatureDocker Guide

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