SaaS / AWS2023

Subscription Streaming Platform

Architected a scalable mobile streaming system with secure content delivery, JWT authentication, and microservices deployed on AWS.

Year

2023

Category

SaaS / AWS

Stack

NestJS, Flutter, PostgreSQL

Subscription Streaming Platform

Key Results

8K

Subscribers

in first quarter

99.97%

Uptime

over launch period

0

Critical Bugs

in first 30 days

10 wks

Time to Launch

from kickoff

01

The Challenge

A media startup needed to launch a subscription streaming product for mobile audiences in Southeast Asia on a tight timeline. They had the content library but no platform, no billing system, and no mobile app.

02

What We Built

A NestJS microservices backend on AWS with CloudFront for CDN delivery and S3 for content storage. Stripe powers subscription billing with three tiers. JWT with refresh token rotation handles secure sessions. The Flutter app supports offline downloads for low-connectivity environments.

03

Results

Launched on schedule with 0 critical bugs in the first 30 days. The platform scaled to 8,000 subscribers in Q1 with 99.97% uptime. Stripe dunning recovered 18% of failed payments. The client expanded to two additional markets within six months.

04

Before & After

Before

Content existed only as raw files on a shared drive with no delivery infrastructure

No billing system — the team manually invoiced a handful of early users

No mobile app — users had no way to access content on their phones

Zero visibility into usage — no analytics on what content was being watched

After

Every title delivered globally via CloudFront CDN with adaptive bitrate streaming

Three-tier Stripe subscription with automatic proration, trials, and dunning management

Flutter app on iOS and Android with offline downloads for low-connectivity regions

Real-time watch events streamed to a dashboard showing performance per title

05

How We Built It

Ten weeks from kickoff to App Store. Infrastructure and app development ran in parallel after agreeing on the API contract in week one.

1

API contract & infrastructure setup

Defined the full REST and gRPC API contracts in week one — REST for Flutter-to-NestJS, gRPC for all service-to-service calls. Provisioned AWS ECS, S3, CloudFront, RDS, and the RabbitMQ cluster so both teams could work independently against mocked endpoints.

2

gRPC service mesh & NestJS microservices

Wrote .proto files defining all inter-service contracts. Built auth, content, and billing services as separate NestJS modules communicating via gRPC. This eliminated the chattiness of REST between services and caught schema mismatches at compile time.

3

RabbitMQ async media pipeline

Designed exchanges and queues for all async operations: download_requested, download_completed, sync_device, subscription_changed, and watch_progress. Built the download service as a dedicated NestJS RabbitMQ consumer with retry logic and dead-letter queues for failed jobs.

4

Flutter app — streaming, downloads & cross-device sync

Built the Flutter app with video_player for streaming, background_downloader for offline content, and a WebSocket subscription to RabbitMQ-driven events for real-time download progress and cross-device state sync.

5

Load testing & App Store submission

Ran k6 load tests simulating 10k concurrent users and chaos tests against RabbitMQ with simulated broker failures. Tuned CloudFront cache policies and ECS autoscaling. Submitted to App Store and Play Store in week nine.

06

System Architecture

Three independent NestJS services behind an API Gateway. Each owns its own DB schema and scales independently. CloudFront sits in front of S3 for all media delivery — the app never touches S3 directly.

1

API Gateway

AWS API Gateway

Single entry point for all Flutter client requests. Routes to auth, content, or billing service. Handles rate limiting before traffic hits NestJS.

2

gRPC Layer

gRPC + Protocol Buffers

All service-to-service communication uses gRPC — auth to content for subscription validation, content to download service for media requests. Strongly typed contracts via .proto files mean no runtime schema mismatches between services.

3

Auth Service

NestJS + JWT + Redis

Issues short-lived access tokens (15 min) and long-lived refresh tokens in Redis. Exposes gRPC endpoints consumed by content and billing services for token validation without HTTP round-trips.

4

Content Service

NestJS + S3 + CloudFront

Manages the content catalogue, generates signed CloudFront URLs for streaming, and publishes download_requested events to RabbitMQ when a user queues offline content.

5

Message Broker

RabbitMQ

All async media operations flow through RabbitMQ — download jobs, cross-device sync events, watch progress propagation, and playback state changes. Each operation type has its own exchange and queue with dead-letter handling for failed jobs.

6

Download Service

NestJS + RabbitMQ consumer

Dedicated NestJS worker that consumes download_requested messages from RabbitMQ. Fetches HLS segments from S3, packages them for offline use, and publishes download_completed events back so all user devices sync state in real time — exactly like Spotify's offline sync model.

7

Billing Service

NestJS + Stripe

Handles Stripe webhook events and updates access tier in PostgreSQL. Publishes subscription_changed events to RabbitMQ so the content and download services update tier enforcement without polling.

8

Media Storage

AWS S3 + CloudFront

HLS-encoded video files in S3, served via CloudFront with signed cookies. Multi-region edge caching gives Southeast Asian users sub-100ms time-to-first-byte.

9

Mobile App

Flutter + Riverpod

Single codebase for iOS and Android. Subscribes to RabbitMQ-driven push events via WebSocket for real-time download progress and cross-device playback state sync.

07

Tech Stack

Backend

NestJSTypeScriptNode.jsgRPCRabbitMQ

Mobile

FlutterDartRiverpod

Infrastructure

AWS ECSAWS S3CloudFrontAPI Gateway

Database

PostgreSQLRedis

Payments

Stripe

08

How We Approached the Problem

The biggest architectural decision was how services talk to each other. With five independent NestJS services, we needed a communication model that was fast, type-safe, and wouldn't collapse under load. gRPC for synchronous calls and RabbitMQ for all async media operations gave us the Netflix/Spotify-style architecture the client wanted — services are completely decoupled, failures in one don't cascade, and cross-device sync just works.

Alternatives considered & rejected

Single monolithic NestJS app

Content delivery and billing have very different scaling profiles. Separating them with gRPC service-to-service calls meant we could scale the content and download services independently during peak hours.

Kafka instead of RabbitMQ

Kafka is the right choice for high-throughput event streaming at Spotify scale. At our volume, RabbitMQ's per-queue routing, dead-letter handling, and simpler ops story were a better fit. We can migrate to Kafka if the client hits 100k+ concurrent users.

REST for inter-service communication

With 5 services calling each other, REST would have meant HTTP overhead on every internal call and no compile-time contract enforcement. gRPC with .proto files gave us typed contracts, bidirectional streaming for media ops, and ~7x faster serialisation than JSON.

React Native instead of Flutter

The client had an existing Flutter codebase for a separate product. Reusing the same framework and Dart developers was faster than switching ecosystems.

09

Data Modelling

Each NestJS service owns its schema. Auth owns users and sessions. Content owns titles, episodes, and watch history. Billing owns subscriptions and invoices. Cross-service queries go through API calls — never direct DB access.

10

API Layer

NestJS modules map to services. The content module validates the user's subscription tier before generating a signed CloudFront URL — the Flutter client never receives raw S3 paths.

11

Database Functions

Two PostgreSQL functions handle the most frequent read patterns: continue-watching list on app open, and upsert watch position called every 10 seconds during playback.

12

Frontend Connection

The Flutter app uses Riverpod with a VideoRepository layer. The PlaybackController fetches signed URLs from NestJS and passes them to video_player. Watch position syncs every 10 seconds via a background timer.

13

Lessons Learned

Sign URLs server-side, always

An early prototype generated signed URLs client-side using temporary AWS credentials, exposing key material in the app bundle. Moving signing to NestJS meant credentials never leave the server.

Stripe webhooks need idempotency checks

Stripe can deliver the same event more than once. Our first handler processed duplicate payment_succeeded events and granted double subscription extensions. Idempotency checks on the Stripe event ID fixed it.

Pre-initialise video_player before the user taps play

Cold-start latency was 2-3 seconds. Pre-initialising the controller when the user navigates to the title detail page — before they tap play — reduced perceived latency to under 500ms.

HLS segment size affects offline storage significantly

Default 10-second segments wasted storage by downloading a full segment before pausing. Switching to 2-second segments reduced wasted storage by ~40%.

Start a project

Have something similar in mind?