AzloTV Streaming Platform
Subscription streaming platform: web, iOS, tvOS and Android, plus payments and live infra

At a glance
- Audience
- 80,000+ registered users, 18,000+ paying subscribers (client-reported, 2026)
- Role
- Lead engineer, Dec 2025 to present
- Platforms
- Web, admin and creator dashboards, iPhone/iPad, Apple TV, Android, backend/cloud
- Commits
- About 2,300 of my commits excluding merges (2,575 with merges) across 9 repositories, Dec 2025 to Sep 2026
- Commits by codebase
- API 1,015 · viewer site 405 · admin 310 · MediaMTX 223 · creator dashboard 192 · payment server 146 · Apple TV 109 · Flutter app 94 · iOS 81
- API surface
- 67 route files, 126 controllers and 186 data models in the core API, plus a separate payment service with 11 route groups
- Tests
- About 1,640 iOS test cases, about 125 tvOS tests and about 105 backend and payment-server test files
Overview
AzloTV is a subscription streaming platform for on-demand series, creator live streaming, multi-camera reality shows, merch and ticketed events. I took over the production codebase in December 2025 and have been its lead engineer since. My work spans the Node/Express API, a new payment microservice, the MediaMTX live pipeline, the AWS media stack, three Next.js web apps and the apps for iPhone, iPad, Apple TV and Android.
The challenge
The platform came to me as an Express monolith, a Next.js 14 site and an admin panel without a shared git history, so my first step was rebuilding it into version control. Payments and their background jobs ran in the same process that served content and live chat. Every change had to ship without breaking web, mobile and TV clients that were already in users' hands. Live streams had to hold up on creators' 4G/5G phone connections and survive AWS refusing GPU capacity in an availability zone. Media had to stay behind signed URLs, which each player handles differently. Creator monetization (SuperChat, memberships, affiliate and marketplace payouts) needed money movement that stays correct when webhooks arrive late, twice or out of order.
My role
I inherited the API, viewer site and admin CMS in December 2025, restored them into version control, and have been the only engineer working on it day to day since. I started and own six new codebases: the payment server, the MediaMTX live-ingest service, the creator streaming dashboard, the native iPhone/iPad app (App Store versions 4.0.1 to 5.1.0), the Apple TV app and the mongobetween deployment. I also maintain the Flutter Android app and am building its native Kotlin replacement, as well as the AzloTV Studio broadcasting app, which has its own entry in this portfolio, and I built Team Azlo, the internal staff app. On the backend, the AWS media pipeline, signed delivery, the Stripe/Connect/PayPal integration and CI/CD are mine.
Architecture
Clients
Viewer web app
Next.js 16 App Router, React 18, TypeScript, TanStack Query, hls.js, Stripe.js, next-intl
About 101 routes (browse, playback, live rooms, checkout, merch, events, account) with middleware for access rules; it replaced the inherited Next.js 14 site in a gated cutover in May 2026.
Admin CMS and creator streaming dashboard
Next.js 16, shadcn/ui, Tailwind v4, Socket.IO, hls.js, mux.js, WHIP/WebRTC
About 147 admin pages for content, chunked video uploads up to 50 GB, ads, merch, payouts and analytics; about 30 creator pages for going live, browser-side clipping, OBS overlays and the Show House control room.
iPhone and iPad app
Swift 6 strict concurrency, SwiftUI, AzlotKit SPM package, WidgetKit, ActivityKit, App Intents
Native viewer app on iOS 18+ with a shared four-library package (core/networking, UI, player, live), plus widgets, Live Activities, offline downloads and an iPad sidebar layout.
Apple TV app
SwiftUI, tvOS 17+, @Observable MVVM, AVPlayerViewController, TVServices Top Shelf
A separate tvOS codebase with device-code sign-in, custom focus-engine button styles, a live chat overlay and a Top Shelf extension that shares a Keychain group with the app.
Android apps
Flutter (GetX, media_kit) in production; Kotlin 2.1, Jetpack Compose, Media3, Hilt, Room in development
The Flutter app ships today with live chat, a clips feed, offline downloads, merch, events and tickets. The native Compose app (12 Gradle modules, Media3, Hilt, Room) keeps the same applicationId so it can replace it.
Edge/Delivery
Load balancer path routing
AWS Application Load Balancer, weighted target groups
Path rules send subscription, affiliate, analytics and SuperChat traffic to the payment server and everything else to the monolith, so the extraction needed no client release.
Signed media delivery
CloudFront signed URLs, Cloudflare R2 + Workers with HMAC path tokens
The API signs playback URLs per request, for either CloudFront or Cloudflare R2, so HLS segments stay private in every player.
Services
Core API monolith
Node.js, Express 5, Mongoose, BullMQ, Socket.IO on an arm64 (Graviton) EC2 Auto Scaling Group with PM2
Content, auth, live control, ads, merch and creator tools under /api/v1, the live and support Socket.IO channels, and a Claude-based support assistant that proposes refunds for a human agent to approve. It subscribes to entitlement events published by the payment server.
Payment server
Node.js/Express on ECS Fargate; Stripe Billing, Connect, Tax; PayPal
Owns subscriptions, creator memberships, SuperChat, affiliate and marketplace payouts, ticket and merch checkout, disputes and a double-entry ledger, and runs the payment background jobs.
FairPlay license service (development)
Apple FPS Server SDK KSM sidecar, Express /api/v1/drm endpoints
FairPlay license service built and validated end to end with Apple's test stream (rollout pending).
Media
Live ingest and GPU transcode
MediaMTX (RTMP, SRT, WebRTC/WHIP), FFmpeg with NVENC on g4dn EC2, r2-sync to Cloudflare R2
Publishers are authenticated against the API, streams are encoded without upscaling past the source resolution or frame rate, and 2-second HLS segments are synced to R2 with partial segments trimmed. On a capacity refusal the ingest box fails over to another availability zone.
VOD pipeline and ads
S3 multipart upload, AWS MediaConvert, Lambda webhook; client-side ad insertion
Admin uploads go straight to S3 and MediaConvert produces QVBR HLS ladders. Ads moved from MediaTailor server-side insertion to client-side insertion in January 2026.
Data
Document database behind a connection proxy
Amazon DocumentDB Serverless, mongobetween on ECS Fargate
mongobetween pools 150+ application connections into about 50 connections to DocumentDB, shared by the monolith and the payment server.
Redis: cache, locks, pub/sub and socket fan-out
ElastiCache Redis, ioredis, @socket.io/redis-adapter
Holds entitlement hashes, the cross-service pub/sub channels, job locks and the Socket.IO adapter, with every key and channel namespaced per environment.
Key decisions
- 01
Extract payments with a Strangler Fig behind the load balancer
I moved subscriptions, affiliates, analytics and SuperChat into a separate Fargate service and routed their paths to it at the ALB. My plan for the move was a seven-phase runbook: shadow tests comparing both services, weighted read traffic at 10, 50 and 100%, then writes, then a single cutover of Stripe webhooks and background jobs so the two services never ran the same job. Clients did not change and each phase could be rolled back by changing a rule. The cost is that both services share one database and one Redis, so shared keys and channels must stay in sync across two repos.
- 02
Redis pub/sub as the contract between the two services
The payment server does not call the monolith for every entitlement change. It writes a per-user Redis hash through a Lua script that rejects writes carrying an older updatedAt, then publishes an event; the monolith drops its cached user and pushes a subscriptionChanged Socket.IO event to that user's devices. The Lua check stops concurrent Stripe webhooks from writing state out of order. Every key and channel carries an environment prefix, because Redis pub/sub ignores the logical database.
- 03
Layered payment reliability instead of trusting webhooks
Webhook events are deduplicated by event id. An hourly poller replays Stripe events that failed delivery through the same handlers, and reconcilers compare Stripe with local state. The safety net skips a trialing subscription until its payment method is confirmed. SuperChat uses destination charges to the streamer's Connect account, with transfer reversals on refunds and disputes. Affiliate payout transfers use the SHA-256 of the sorted commission ids as the idempotency key, so a retry replays the original transfer instead of paying twice.
- 04
Fail the ingest server over to another availability zone
AWS refuses GPU capacity per instance type and availability zone, but Elastic IPs belong to the region. When a start fails with InsufficientInstanceCapacity, the API walks a list of (AZ, instance type) pairs, launches a tagged clone from the AMI baked on every MediaMTX deploy and moves the Elastic IP to it, so DNS, the TLS certificate and the WebRTC host stay the same. A Redis lock, start backoff and an orphan sweep prevent leaked GPU instances, and a 23-case test suite covers the failover rules.
- 05
Make signed media work in every player
AVPlayer does not read shared cookies for HLS segment requests, so the Apple apps turn CloudFront signing parameters into cookies passed through AVURLAssetHTTPCookiesKey. The native Android app builds Media3 sources on the same OkHttp client and cookie jar as the API. Offline downloads are encrypted and their keys are gated by subscription.
- 06
Rebuild the viewer site next to the old one and cut over by image swap
I rewrote the inherited Next.js 14 site as a separate Next.js 16 app that pushed to the same container registry and ECS service. Going live was a change of image SHA in the task definition, and rolling back was the same change in reverse. The cutover checklist gated it on a Playwright smoke suite, an accessibility audit of the top routes, a Lighthouse budget (LCP under 2.5 s, CLS under 0.1) and a 48-hour staging soak. The cost was maintaining two codebases in parallel until the May 2026 cutover.
Live streaming on MediaMTX
I built AzloTV's live pipeline from an empty repo in February 2026 and have shipped 223 commits to it since. Creators go live from a phone, OBS, a browser or a multi-camera set; viewers watch HLS from Cloudflare's edge and never touch the ingest server. My first milestone was cutting glass-to-glass delay from about 7 minutes to about 5 seconds.
- Ingest over RTMP, RTMPS, SRT and WebRTC/WHIP on MediaMTX. Every publish is authorized by an HTTP hook into the API, each Show House camera has its own hashed token, and internal callbacks are checked with a constant-time shared secret.
- GPU transcoding: FFmpeg decodes with NVDEC and encodes H.264 with NVENC, with 1-second keyframes, constant frame rate up to 60 fps and a 6 Mbps cap. It never upscales past the source, and it falls back to x264 automatically if the GPU driver breaks.
- A Node service I wrote (about 2,900 lines) syncs 2-second HLS segments to Cloudflare R2 every 200 ms with 16 parallel uploads. It writes its own master and variant playlists, marks encoder restarts with discontinuities and reports stream health back to the API.
- Built for 4G/5G phone uplinks: 30-second read timeouts, transcoder retries that wait up to 45 seconds for the publisher to return, and a 45-second grace period before a stream is declared over, so a tunnel or a cell handover doesn't end the broadcast.
- Hardened from real incidents: a bound on transcoder restart storms that pages an operator, a cap on runaway re-uploads (one stuck segment had been uploaded 2,158 times in 8 minutes), a playlist that never renumbers published segments (which broke iOS playback), and "live" only shown once a stream is actually playable.
- The GPU box starts on demand when a creator creates a stream (about 60 seconds to wake), pre-warms 10 minutes before scheduled shows and stops itself after 20 idle minutes. When AWS refused GPU capacity for three days in staging (962 refusals), I added failover that launches a clone from the image baked on every deploy in another availability zone and moves the Elastic IP to it.
- Show House multi-camera events: each camera publishes on its own path over RTMP or SRT, the API tracks camera health every 10 seconds over sockets, and the whole show is kept as a compressed DVR playlist. When a stream ends, its segments become a VOD inside R2 with no re-encode.
- Measured capacity on an L4 GPU: 16 simultaneous 1080p broadcasts in real time, breaking at 20 when the NVENC engine hit 100%. About 570 backend live tests cover the pipeline, including 23 for the failover rules.
Cutting delivery costs with Cloudflare
Video egress was the bill that grows with every viewer. In February 2026 I moved AzloTV's media delivery from Amazon CloudFront to Cloudflare R2 behind Workers, because R2 charges nothing for egress while CloudFront charges per GB. At list prices that is roughly $92 versus $19 a month per terabyte served, about 80% less, and the saving grows with traffic.
- Added a CDN provider switch to the API and replaced CloudFront's RSA-signed URLs with HMAC-SHA256 tokens checked in a Cloudflare Worker. The token can sit in the path, so native iOS and Android players work without cookies.
- Migrated production behind a Redis-backed maintenance switch: uploads go straight to R2, and a reversible script with a dry-run mode rewrote the stored media URLs. CloudFront stays only so old links keep working.
- Live streaming uses a second bucket and Worker: manifests are served no-cache and segments immutable for a year, so the edge absorbs viewer load. I moved both Workers into CI after a hand-deployed Worker caused an outage.
- Edge caching for API responses (plans, genres, featured rows, listings) and unsigned public assets (thumbnails, avatars, trailers), because signed URLs rotating every 15 minutes were defeating both the browser cache and Cloudflare's cache.
- Other cost work: the API moved to ARM64 Graviton instances, the GPU ingest server sleeps when nobody is live, trailers are remuxed with ffmpeg instead of re-encoded in MediaConvert, and I removed a duplicate MediaConvert output that was doubling spend on download jobs.
- I also priced Cloudflare Stream Live, built it behind a provider flag and left it off: for our traffic it cost more than running our own MediaMTX box on R2.
DevOps and infrastructure
I run AzloTV's infrastructure on AWS and Cloudflare myself: CI/CD, environments, deploys, monitoring and on-call. Everything deploys from GitHub Actions, with a staging environment on the develop branch and production on main.
- API deploys: a CI gate (unit, integration, live, payments and support-contract tests plus a banned-pattern check), then a rolling, fail-fast deploy across the EC2 Auto Scaling group over AWS SSM, one instance at a time with health checks. Secrets come from SSM Parameter Store.
- The MediaMTX pipeline (about 1,600 lines of workflow) wakes the sleeping GPU box, checks and repairs the NVIDIA driver, deploys and verifies the services, and bakes a failover machine image on every release. A twice-weekly job wakes the box to check and renew its TLS certificate.
- The payment service was provisioned with idempotent scripts (container registry, security groups, secrets, IAM, logs, target groups), then moved over with weighted load-balancer routing, shadow tests comparing both services, and a one-command rollback.
- The web apps (viewer site, admin CMS, creator dashboard) build ARM64 Docker images to ECR and roll out on ECS Fargate. A mongobetween proxy on Fargate pools connections to DocumentDB, and I rebuilt its images off end-of-life base images with a pinned upstream for reproducible builds.
- Alerting over SES: live-ops incidents (stalled streams, transcoder crash loops) email an operator once per incident, even across the Auto Scaling group, using atomic Redis claims, and every incident sends a resolved message.
- Staging sits behind Cloudflare Access: the API verifies the Access JWT on HTTP and on every Socket.IO namespace, so the load balancer can't be reached directly. Redis keys and channels are isolated per environment, with a startup check that refuses a misconfigured environment.
- Break-glass access that doesn't depend on the normal path, scripted database exports to S3 with 30-day retention, and a production configuration audit that turned on HSTS and enforced the content access gates.
Team Azlo: the staff app
The people behind AzloTV (film crews, event staff, support and admins) needed the admin panel in their pocket. I built Team Azlo, a Skip/SwiftUI app of about 97,000 lines, and shipped it internally through TestFlight in July 2026. It has its own page in this portfolio.
- Role-aware from the first screen: four staff levels and 16 permission areas mirror the backend's rules, so crew see a focused My Work portal (tasks, gear, pay, deal memos, expenses) while managers get production, events, merch, support, legal and live moderation.
- Film production on a phone: projects, approvals, purchase orders, timecards, payroll runs, a gear cage with check-out, check-in and custody history, and shooting schedules with call sheets.
- An anonymous door kiosk for event check-in by QR scan and an in-person NDA signing kiosk, both backed by new API endpoints I added.
Results
- Subscriptions, memberships, SuperChat, affiliates, marketplace and ticket checkout, disputes and payouts run on a separate payment service on ECS Fargate, routed behind the load balancer with no client release.
- Creator monetization: SuperChat with six server-defined tiers, per-creator membership subscriptions, Stripe Connect onboarding with scheduled daily payouts, and a multi-vendor merch store. PayPal was added as a second processor behind a kill switch and a percentage rollout, and a double-entry ledger is built to reconcile against Stripe balance transactions every hour.
- Live pipeline: MediaMTX takes RTMP, SRT and WebRTC/WHIP, encodes on GPU and serves HLS from Cloudflare R2. It is tuned for phone uplinks (30 s timeouts, a 45 s grace period before ending a stream), runs multi-camera Show House events and fails over across availability zones.
- Native Swift 6 iPhone/iPad app with widgets, Live Activities, Siri shortcuts, offline downloads and about 1,640 automated tests, released through TestFlight and the App Store (versions 4.0.1 to 5.1.0 by July 2026). The separate Apple TV app took on iOS features (shop, account, live chat, Show House recordings, Top Shelf deep links) in eleven parity tracks in September 2026, integrated on a release branch that hasn't shipped yet.
- The Next.js 16 viewer site went live on 15 May 2026. Operators use an admin CMS of about 147 pages; creators use a dashboard for browser (WHIP) or OBS publishing, clipping in the browser and on-stream alert overlays.
- FairPlay license service built and validated end to end with Apple's test stream (rollout pending).
- Before starting the native Android rewrite, I ported the iPhone app's features to the Flutter app in phases during spring 2026: the merch shop, events and tickets, multi-camera Show House, full offline downloads, QR sign-in for TVs and English/Spanish localization.
- The native Android app is a 12-module Kotlin/Jetpack Compose project (about 54,000 lines, 21 feature packages) with offline downloads, clip capture, Google Cast, home-screen widgets, a Media3 player and a baseline-profile benchmark module.
Stack
- Backend
- Node.jsExpressMongooseBullMQSocket.IORedis (ioredis)Lua scriptsAnthropic Claude API
- Payments
- Stripe BillingStripe ConnectStripe TaxPayPalDouble-entry ledger
- Web
- Next.js 16ReactTypeScriptTanStack QueryTailwind CSSshadcn/uiNextAuthnext-intlhls.jsmux.js
- Apple platforms
- Swift 6SwiftUIAVFoundationWidgetKitActivityKitApp IntentsTVServices Top ShelfSwift TestingUniversal Links
- Android
- FlutterGetXmedia_kitKotlinJetpack ComposeMedia3 ExoPlayerHiltRoomRetrofitGoogle Cast
- Live and media
- MediaMTXFFmpeg (NVENC)SRTWebRTC/WHIPHLSAWS MediaConvertFairPlay Streaming (dev)
- Cloud and delivery
- AWS ECS FargateEC2 Auto Scaling (Graviton, g4dn GPU)ALBS3CloudFrontLambdaDocumentDBElastiCacheCloudflare R2 + WorkersDockerGitHub ActionsCloudflare AccessAWS SSMECRAuto-suspending GPU ingest
- Observability
- Firebase CrashlyticsFirebase PerformanceRemote ConfigMetricKitCloudWatch