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
- Browser sends
POST /merge-clipswith clips, trims, and resolution, using the user's Supabase JWT as the bearer token. - API verifies the token, enforces the per-user storage limit and the one-active-job rule, enqueues a BullMQ job, and returns immediately.
- Browser opens
GET /progress/:userIdas Server-Sent Events. The API checks that the caller owns that user ID before streaming. - 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. - Worker uploads the MP4 to the private R2 bucket and records the mix in Postgres, reporting progress at every step.
- Progress flows through BullMQ
QueueEvents(Redis pub/sub) to whichever API process holds the browser's SSE connection. - Browser gets a short-lived presigned URL and plays or downloads straight from R2.
03Challenges
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.
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.
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.
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.
Stuck renders and noisy neighbors
One hung encode or one heavy user can pin a worker and starve everyone else.
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.
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.
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