← All case studies

// case study 02 · video saas · production systems

ClipMix Twitch highlight reels, rendered on my own fleet

Paste Twitch clip URLs, trim and reorder them, and get back a single rendered MP4 to download or share. Identity, data, and storage run on managed cloud services. The expensive part, video encoding, runs on Apple-silicon machines I own, with deploy safety you'd normally get from a PaaS.

Role
Sole engineer: product, frontend, backend, infra, ops
Timeline
September 2024 to present
Status
Free public launch; Flutter mobile client in progress
Core stack
Next.js, Fastify, BullMQ, ffmpeg, Supabase, R2

TL;DR

  • Hybrid architecture: I bought identity, data, and storage, and built the compute. Rendering runs on Mac minis using the hardware H.264 encoder.
  • Distributed queue: any worker can take any job. Progress streams from whichever machine is encoding back to the exact browser tab, over Redis pub/sub and SSE.
  • Safe deploys on owned hardware: symlink-flip releases, a health gate that checks for the new commit SHA, automatic rollback, and parallel worker rollout.
  • Cheap by design: the app never proxies video bytes. Downloads and playback go straight to object storage.

01Problem

Creators and clip editors constantly want "best of" reels: a stream's funniest moments, a tournament's top plays. Doing that by hand means downloading each clip, lining them up in an editor, normalizing mismatched resolutions, exporting, and re-uploading. On a phone it's worse.

The engineering problem is cost. Transcoding is expensive to rent, and video egress is expensive to serve. Run naively on managed cloud services, a free creator tool loses money on every render. ClipMix's bet was to make the editing trivial (paste, trim, render) and make the economics work by owning the compute and keeping video bandwidth off the application entirely.

02Build

There are two repositories and three tiers: a Next.js web app, a Fastify API, and BullMQ workers. The API and workers share a codebase but run as separate entrypoints and communicate only through Redis, so rendering scales independently of web traffic.

One render, end to end

  1. Browser sends POST /merge-clips with clips, trims, and resolution, using the user's Supabase JWT as the bearer token.
  2. API verifies the token, enforces the per-user storage limit and the one-active-job rule, enqueues a BullMQ job, and returns immediately.
  3. Browser opens GET /progress/:userId as Server-Sent Events. The API checks that the caller owns that user ID before streaming.
  4. A free worker takes the job. It downloads each unique clip once via yt-dlp, trims, and runs one ffmpeg graph (scale, pad, concat, watermark) on the hardware encoder. Optional subtitles are transcribed and burned in.
  5. Worker uploads the MP4 to the private R2 bucket and records the mix in Postgres, reporting progress at every step.
  6. Progress flows through BullMQ QueueEvents (Redis pub/sub) to whichever API process holds the browser's SSE connection.
  7. Browser gets a short-lived presigned URL and plays or downloads straight from R2.

03Challenges

Challenge

Live progress from a machine that doesn't hold the connection

The worker running ffmpeg is almost never the process holding the user's HTTP connection. Polling adds latency and load.

Approach

Workers call job.updateProgress(). BullMQ's QueueEvents turns that into a Redis pub/sub event that any API process can receive, and the one holding this user's stream forwards it. Workers don't need to know about HTTP at all, so adding a worker just means another machine draining the queue.

Challenge

Zero-downtime deploys without a PaaS

An API box and three render workers on hardware I own, with no platform underneath to handle releases or rollbacks.

Approach

A push to production runs GitHub Actions on self-hosted runners. Each box stages releases/<sha>, flips a current symlink, and reloads under pm2. The script then polls /health until it reports the new SHA, and flips back automatically if it doesn't. The API deploys first; the three workers fan out in parallel. The last five releases stay on disk, and a one-click workflow rolls back to any of them.

Challenge

Stuck renders and noisy neighbors

One hung encode or one heavy user can pin a worker and starve everyone else.

Approach

One active job per user. Two attempts with exponential backoff. A 120-second worker lock, a 5-minute SIGKILL timeout per clip download, and a 20-minute wall-clock limit per mix. Scratch directories are wiped on failure. Workers write a heartbeat key with a 45-second TTL, so /health/queue shows whether the fleet is alive.

Challenge

Bandwidth and attack surface

A video product can drown its own origin in egress, and every clip URL is untrusted input to a shell tool.

Approach

A hard rule: the app never proxies video bytes. Mixes live in a private bucket behind presigned URLs, and thumbnails are served from a public CDN bucket. Every external process runs through execFile, never a shell. Routes are protected by JWT verification, ownership checks, Postgres row-level security, global rate limiting, Helmet, and profanity filtering on public names.

04Impact

  • 3Apple-silicon render workers in the deploy matrix, plus the API box
  • 239automated tests passing: 137 on the API and workers, 102 on the web app, plus a Playwright smoke suite
  • 323commits across the web and API repositories, all solo
  • 0 bytesof video proxied through the application. All delivery goes through R2.
  • autorollback on every backend deploy whose health check doesn't report the new SHA

Stripe Checkout, the customer portal, and webhook-synced plans were built, then deliberately switched off behind a server-side flag for the free launch, with the handler kept in history for re-enabling.

Sources: the highlight-reel-creator-server and highlight-reel-creator-stripe repositories (deploy workflows, queue config, worker code). Test counts are from local runs in October 2026. Usage, render-time, and cost figures are omitted until they can be pulled from production logs.

05Stack

Frontend
Next.js 14React 18TypeScriptTailwindRadixshaka-playerfetch-event-source
Backend
Fastify 5BullMQioredisfluent-ffmpegyt-dlpTwitch Helix APIOpenAI / Replicate
Data
Supabase Postgres + RLSSupabase AuthCloudflare R2 (S3 SDK)Stripe (flagged off)
Infra & ops
Mac mini fleetVideoToolboxpm2GitHub Actions self-hosted runnersSentry
Mobile
FlutterRiverpodDiogo_router
Testing
VitestTesting LibraryPlaywrightgit hooks
Next case studyRandomSeedGen → Read