Async pipeline that turns uploads and YouTube links into transcripts and summaries.
- NestJS
- Python
- RabbitMQ
- PostgreSQL
- Docker
- GitHub Actions
- Nginx
- AWS S3
- Prometheus
- Grafana
01 / Overview
What it is
- Role
- Solo: backend, frontend, and infrastructure
- Status
- Live at mediaflow.emersonic.dev
- Runs on
- A OVHcloud VPS, five services behind one Nginx
MediaFlow AI turns audio and video files, or YouTube links, into searchable transcripts, summaries, and keywords. Uploads can reach about 500 MB, so the system is built so that heavy work never blocks a request: files go straight to object storage and processing happens asynchronously.
It is a distributed system of independent services connected by RabbitMQ: a NestJS API gateway that authenticates and rate-limits every request, an ingestion service that registers uploads and starts the pipeline, a Python processing worker, and a real-time notification service, all behind a single Nginx entry point.
- Live
- deployed on a self-hardened VPS at mediaflow.emersonic.dev
- 500 MB
- uploads go straight to storage, not through the API
- 59
- tests (23 gateway, 36 worker) gate every deploy
- 5
- container images, tagged by commit SHA
02 / Architecture
How it fits together
A look at the system architecture and how its components work together.
03 / Technical decisions
Key technical decisions
A gateway that only authenticates, authorizes, and routes
The gateway is a thin proxy in front of the services. It validates the JWT, checks permissions, applies rate limiting, and forwards the request. It holds no business logic, so protecting the system and doing the work stay separate concerns, and each can change without touching the other.
Alternative considered
Mixing authentication and business logic in a single API.
A dedicated ingestion service with direct-to-storage uploads
The ingestion service owns everything about getting media into the system. It generates a presigned URL, the browser uploads straight to object storage, and the service only registers the media. The file never touches the gateway or the ingestion service, so files of hundreds of megabytes don't consume their memory, bandwidth, or request time.
Alternative considered
Proxying large files through the API.
An event-driven pipeline with RabbitMQ
After an upload, the ingestion service publishes a media.uploaded event and responds immediately. The worker consumes it when it has capacity, so slow processing never blocks users. Failed jobs are retried and, when they keep failing, land in a dead-letter queue instead of disappearing. RabbitMQ delivers at least once, so the worker is designed to tolerate redelivery, which makes retries safe. The worker can be scaled without touching the API.
Alternative considered
Processing the media inside the HTTP request.
The right runtime for each service
NestJS handles the gateway and the ingestion service, Python handles the processing (FFmpeg for audio extraction, then transcription, summary, and keywords with Whisper and Gemini), and a small Node service with Socket.io handles real-time delivery. Each service uses the ecosystem that fits its job.
Alternative considered
Forcing the AI pipeline into the API's runtime.
Metrics that show a stuck pipeline
Prometheus and Grafana track CPU, memory, latency, and RabbitMQ queue depth across the gateway, the ingestion service, the worker, and the broker. A stuck pipeline shows up on a dashboard as a queue that keeps growing.
Alternative considered
Finding out about stuck jobs from users.
04 / Production
How it runs in production
MediaFlow is live at mediaflow.emersonic.dev on a OVHcloud VPS that I provisioned and hardened myself. Every release is an immutable image tied to a commit, tested before it ships, and able to roll back on its own.
Deploy pipeline
- 1
Push to main
GitHub Actions starts on every push.
- 2
Test
23 gateway tests and 36 worker tests. A failure stops the pipeline here.
- 3
Build
Five service images, each tagged with its commit SHA, never latest.
- 4
Publish
Images go to GitHub Container Registry (GHCR).
- 5
Deploy
Over SSH: the VPS pulls the new tags and Docker Compose restarts the services.
- 6
Verify
Healthchecks decide whether the release stays up.
Access
- Key-only SSH, root login disabled, non-default port
- Dedicated non-root deploy user
Network and edge
- Firewall allowing only 80, 443, and SSH
- Nginx is the only public service: it serves the React app and proxies the API and WebSocket traffic
- TLS from Certbot, auto-renewed, with HTTP redirected to HTTPS, HSTS, and TLS 1.2 and 1.3 only
Runtime
- Docker Compose with healthchecks, restart policies, and resource limits
- Internal networks between services
System
- Automatic security updates
- Secrets live on the server, never in the repository
05 / What's included
What's included
Group 01
Pipeline
- Upload audio or video files, or submit YouTube links
- Audio extraction with FFmpeg
- Transcript with timestamped segments, summary, and keywords
- Retries and dead-letter queues
- Live per-job progress in the dashboard
- In-browser playback through presigned URLs
Group 02
Platform
- JWT authentication and rate limiting at the gateway
- React, Vite, and Tailwind dashboard
- Gateway and worker unit tests
- Prometheus and Grafana dashboards for the gateway, worker, and RabbitMQ
06 / Trade-offs
Trade-offs and next steps
Constraint 01
Production runs on a single VPS with Docker Compose. That keeps operations simple and cheap, but there is no redundancy.
Next step
Managed orchestration for real scale.
Constraint 02
Rollback restores the previous images, not the data, so message formats and the schema have to stay compatible with the release before them.
Next step
Version message formats and make schema changes backward compatible.
Constraint 03
Transcription and summaries depend on external AI providers, so latency and cost follow theirs.
Next step
Put a provider abstraction in front of them to make them swappable.