Live Streaming Platform
Video live-streaming reference architecture: broadcaster ingest via IVS, transcoding, CloudFront delivery to viewers, WebSocket chat backed by DynamoDB, and VOD
Open & fork this on Zstem →Architecture
- Broadcaster (OBS): Streams RTMPS to the ingest endpoint using a per-channel stream key
- Amazon IVS Ingest: Managed RTMPS ingest. Authenticates the stream key and originates stream-state events.
- IVS Transcoder (ABR): Transcodes the source feed into an adaptive bitrate ladder (1080p/720p/480p/audio-only).
- CloudFront CDN: Delivers HLS playlists and segments globally; ~3-5s glass-to-glass latency.
- VOD Recordings Bucket: IVS auto-record-to-S3 target. Lifecycle rule transitions old recordings to Glacier.
- VOD Publisher: S3-event triggered: writes VOD metadata (duration, key, thumbnail) once a recording completes.
- EventBridge: Receives IVS Stream State Change events (START/END).
- Stream Events Handler: Flips stream status live/ended, stamps playback URL, kicks off VOD registration on END.
- Viewer: Watches the HLS stream and chats in real time
- Web App (player + chat): React SPA with an HLS.js player and a WebSocket chat pane.
- WebSocket API (Chat): API Gateway WebSocket: $connect / $disconnect / sendMessage routes.
- Chat Handler: Persists messages, tracks connections, fans out via postToConnection.
- Chat Tables: DynamoDB: chat_messages (by stream, time-ordered) and chat_connections (TTL'd).
- REST API: Streams, VODs, users. Cognito JWT authorizer; stream discovery is public.
- Streams Service: Channel CRUD, stream-key rotation, live directory, VOD listing.
- Streams & Users Tables: DynamoDB: users, streams (GSI on status for the live directory), vods, follows.
- Cognito User Pool: Auth for broadcasters and chatting viewers; anonymous playback allowed.
Sequence
Participants: Broadcaster (OBS), REST API / Streams Service, Amazon IVS, EventBridge, Stream Events Handler, DynamoDB, CloudFront CDN, Viewer, WebSocket API, Chat Handler
- Broadcaster (OBS)→REST API / Streams ServicePOST /streams {title, category}
- REST API / Streams Service→Amazon IVSCreateChannel + stream key
- REST API / Streams Service→DynamoDBPutItem stream (status=offline)
- REST API / Streams Service→Broadcaster (OBS)201 ingest endpoint + stream key
- Broadcaster (OBS)→Amazon IVSRTMPS push (stream key)
- Amazon IVS→EventBridgeStream State Change: START
- EventBridge→Stream Events Handlerrule target invoke
- Stream Events Handler→DynamoDBUpdateItem status=live + playback URL
- Amazon IVS→CloudFront CDNpublish HLS renditions (ABR ladder)
- Viewer→REST API / Streams ServiceGET /streams?status=live
- REST API / Streams Service→DynamoDBQuery GSI status=live
- REST API / Streams Service→Viewer200 stream list + playback URLs
- Viewer→CloudFront CDNGET master.m3u8
- CloudFront CDN→Viewer200 playlist + segments (loops)
- Viewer→WebSocket APIwss $connect (streamId, JWT)
- WebSocket API→Chat Handler$connect route
- Chat Handler→DynamoDBPutItem connection (TTL)
- Viewer→WebSocket APIaction: sendMessage
- WebSocket API→Chat HandlersendMessage route
- Chat Handler→DynamoDBPutItem chat message + query connections
- Chat Handler→WebSocket APIpostToConnection (fan-out)
- WebSocket API→Viewerchat message delivered
- Amazon IVS→EventBridgeStream State Change: END
- EventBridge→Stream Events Handlerrule target invoke
- Stream Events Handler→DynamoDBUpdateItem status=ended; VOD publisher registers recording
API contract
- POST
/streamsCreate a channel and get ingest credentials - GET
/streamsList streams (live directory) - PATCH
/streams/{streamId}Update title/category - GET
/streams/{streamId}Get stream detail - POST
/streams/{streamId}/stream-keyRotate the stream key - GET
/streams/{streamId}/chatRecent chat history - GET
/vodsList VODs - GET
/vods/{vodId}Get VOD detail - GET
/users/meGet my profile
Database
| user_id | Cognito sub |
|---|---|
| username | |
| display_name | |
| avatar_url | |
| is_broadcaster | |
| created_at |
| stream_id | |
|---|---|
| broadcaster_id | |
| title | |
| category | GSI with status for browse-by-category |
| status | offline | live | ended — GSI partition key for the live directory |
| ingest_endpoint | RTMPS endpoint handed to OBS |
| stream_key_secret_arn | stream key lives in Secrets Manager, never in this table |
| playback_url | CloudFront HLS master playlist |
| started_at | |
| ended_at | |
| peak_viewers | |
| created_at |
| message_id | |
|---|---|
| stream_id | partition key in DynamoDB; sent_at is the sort key |
| user_id | |
| username | denormalized for render without a user lookup |
| body | max 500 chars, moderated client + server side |
| sent_at |
| connection_id | API Gateway WebSocket connection id |
|---|---|
| stream_id | GSI for fan-out: all connections watching a stream |
| user_id | null for anonymous viewers (read-only chat) |
| connected_at | |
| ttl | epoch seconds; reaps zombie connections |
| vod_id | |
|---|---|
| stream_id | |
| broadcaster_id | GSI for a channel page VOD list |
| title | |
| s3_key | master playlist key in the recordings bucket |
| thumbnail_url | |
| duration_seconds | |
| published | |
| created_at |
| follow_id | |
|---|---|
| follower_id | |
| broadcaster_id | GSI for follower counts and go-live notifications |
| created_at |
About this design
What this is
A reference architecture for a Twitch-style live streaming platform on AWS. Broadcasters push RTMPS into Amazon IVS (modeled as custom nodes — ingest and ABR transcoder), viewers pull HLS through CloudFront, chat rides an API Gateway WebSocket backed by Lambda and DynamoDB, and every finished stream is auto-recorded to S3 and published as a VOD. A small REST API (streams, VODs, users) and Cognito round out the control plane. The sequence diagram walks the two paths that matter most: a broadcaster going live and a viewer joining to watch and chat.
How it works
Go live. A broadcaster calls POST /streams, which provisions an IVS channel and returns the RTMPS ingest endpoint plus a stream key (the key itself lives in Secrets Manager — only its ARN is stored). When OBS starts pushing, IVS emits a Stream State Change: START event onto EventBridge; the stream-events handler flips the stream to live and stamps the CloudFront playback URL. Viewers discover it via GET /streams?status=live, which reads a GSI on stream status.
Watch. Playback is plain HLS: the player fetches the master playlist from CloudFront and adapts across the transcoder's bitrate ladder. Video never touches our Lambdas — the data plane is entirely IVS + CloudFront, so viewer count doesn't affect API load.
Chat. The web app opens a WebSocket ($connect stores the connection id keyed by stream). sendMessage persists to chat_messages and fans out with postToConnection to every connection watching that stream. Connections carry a TTL so zombies get reaped.
VOD. On Stream State Change: END, IVS finalizes the auto-record to S3; the S3 event triggers the VOD publisher, which writes duration, playlist key, and thumbnail into the vods table.
How to extend
Go-live notifications — the follows table already has the GSI you need; add an EventBridge target on START that pushes via SNS/Pinpoint.
Chat moderation — insert a moderation Lambda (banned-word list, or Comprehend toxicity scoring) between the sendMessage route and persistence.
Viewer counts — aggregate chat_connections per stream on a 10s schedule and expose it on the stream record.
Clips — a MediaConvert job that cuts a time range from the S3 recording; add a clips table.
Low-latency mode — swap the HLS path notes for IVS low-latency playback (~2s) and note the CDN implications.
Chat fan-out is the first thing to shard when a single stream gets big — batch postToConnection calls and consider moving hot streams to a dedicated fan-out worker fed by a Kinesis stream.