Back to portfolio
Case Study

RabbitHole

An event-driven, AI-augmented video platform on AWS. Upload a clip; an autoscaling fleet transcodes it into adaptive-bitrate HLS, a vision model writes its title and tags, and speech-to-text makes every spoken word searchable - then you stream it back through a CDN with live status the whole way. Shipped continuously through CI/CD, with a dashboard that tracks cost per transcode.

Architecture note: this case study documents an earlier RabbitHole iteration that I implemented and deployed. The live product has continued to evolve, so the current interface and behavior may not map one-for-one to every component described below. The repository preserves the infrastructure and delivery evidence for this architecture.

Built to demonstrate cloud architecture (event-driven design, a serverless + container hybrid, autoscaling-to-zero, real-time, cost-awareness), AI integration (vision + speech + semantic search, three different invocation patterns), and delivery maturity (tests, OIDC-based CI/CD, remote-state IaC, observability) - all in Terraform.
Current product: Under Construction GitHub

The problem

A streaming service is a textbook asynchronous workload: uploads are fast, but transcoding is slow and bursty. That mismatch is exactly what event-driven, autoscaling infrastructure exists to solve - so RabbitHole is built to demonstrate that architecture rather than fake it with a CRUD app. The goal: take a raw upload all the way to adaptive playback through a fully decoupled pipeline that costs nothing when no one is using it - then layer real AI on top of it without compromising that.

What it does

Architecture

An event-driven pipeline that decouples the fast path (upload) from the slow path (transcode), with a serverless API and a container worker fleet - the right tool for each job. The AI work hangs off that same backbone: synchronous vision at the worker, async event-driven speech-to-text, and an embedding model inside the API.

Upload → transcode → stream

BrowserReact · hls.js
1 · presigned URL
API Gateway → LambdaFastAPI · container image
2 · PUT file
S3 - uploadsraw video
3 · ObjectCreated event
EventBridge → SQS+ DLQ · retries
4 · poll · autoscale 0→N
ECS Fargate workersffmpeg · Graviton · HLS
HLS renditions
S3 - streamingprivate · OAC
CloudFrontadaptive playback → UI
Real-time path  DynamoDB (videos) → Stream → Broadcaster Lambda → API Gateway WebSocket → live status in the UI
AI metadata  worker samples frames → Claude vision → title / description / tags (key in SSM SecureString)
Speech & search  worker → AWS Transcribe → EventBridge → post-processor Lambda → caption cues; a local embedding model in the API indexes transcripts for cross-video semantic search

Highlights

How it ships & runs

The operational layer is part of the project, not an afterthought - it's what turns "a deployed app" into something you can actually run and evolve.

Engineering decisions & trade-offs

Lambda API + Fargate workers

Right tool per job: serverless for the lightweight, bursty API; containers for the long-running, CPU-heavy ffmpeg transcode that would never fit Lambda's runtime and size limits.

AWS Transcribe over self-hosted Whisper

Managed and event-driven (job-complete → EventBridge → Lambda), with native word-level timestamps. It keeps the worker lean and the transcode path fast; the trade is per-minute cost versus running a model in-container.

A local embedding model, not a managed vector DB

Search embeds with a small ONNX model baked into the API and runs brute-force cosine over vectors in DynamoDB. At portfolio scale that's instant and costs nothing; OpenSearch or pgvector would be the move at a far larger corpus - and would also reintroduce idle cost the rest of the system works hard to avoid.

Direct-to-S3 upload (presigned)

The API issues a presigned URL and the browser uploads straight to S3, so the API never proxies file bytes - cheaper, faster, and far friendlier to a Lambda execution model.

No NAT gateway (a documented cost trade-off)

Workers run in public subnets with a zero-ingress security group instead of private subnets behind a NAT gateway. That keeps idle cost near zero for a demo; the production trade-off is noted below.

What I'd change at scale

The demo deliberately optimizes for cost and clarity. Honest production trade-offs that remain:

Stack at a glance

LayerTech
FrontendReact + TypeScript (Vite), hls.js → S3 + CloudFront
APIFastAPI on Lambda (container image) + API Gateway
WorkersECS Fargate + ffmpeg (ARM64/Graviton), step-autoscaling on SQS depth (min 0)
AI / MLClaude vision (auto-metadata) · AWS Transcribe (captions) · local embeddings (bge-small/ONNX) for semantic search
Real-timeDynamoDB Streams → Lambda → API Gateway WebSocket
MessagingSQS + DLQ, EventBridge (S3 + Transcribe events)
DataS3 (uploads + streaming), DynamoDB
CDN / secretsCloudFront (Origin Access Control) · SSM Parameter Store (SecureString)
IaCTerraform (remote S3 state + native locking)
Testspytest + moto (API + caption pipeline) · Vitest (frontend), coverage-gated
CI/CDGitHub Actions - tests + coverage gate, OIDC-based deploy, infra plan/apply
ObservabilityCloudWatch dashboard (incl. $/transcode) · X-Ray tracing
My role - Sole architect and engineer: the event-driven AWS architecture, the FastAPI/Lambda API, the Fargate ffmpeg worker, the AI metadata + speech-to-text + semantic-search pipelines, the real-time WebSocket layer, the React + hls.js frontend, the OIDC-based CI/CD and observability, and the Terraform that provisions all of it.

Current product: Under Construction  ·