A production-ready live streaming backend built with Ruby on Rails, Nginx RTMP, and a real-time WebSocket layer. LiveAPI handles the full lifecycle of a live stream, from ingestion via OBS to HLS playback, real-time event broadcasting, and live chat, all orchestrated through a containerized, event-driven architecture.
LiveAPI solves the infrastructure problem behind user-generated live streaming: accepting a raw RTMP feed from a broadcaster, transcoding it to HLS for viewers, keeping clients in sync via WebSockets, and persisting stream metadata and chat.
Intended audience: mobile and web clients consuming a REST + WebSocket API. The system is designed to support a social streaming platform where users can broadcast, follow each other, and participate in live chat.
Primary stack: Ruby on Rails 7.2 · PostgreSQL · Redis · Sidekiq · Nginx + RTMP module · MinIO (S3-compatible storage) · Action Cable · Docker Compose
| Feature | Description |
|---|---|
| RTMP Stream Ingestion | Nginx compiled with the nginx-rtmp-module accepts RTMP streams. Stream keys are validated via a webhook callback before the stream is admitted, preventing unauthorised publishing. |
| HLS Playback | Nginx slices incoming RTMP into HLS fragments and serves them over HTTP. Each stream gets a nested HLS path keyed to the broadcaster's username, so playback URLs are stable and predictable. |
| Real-Time Stream Events | When a stream goes live or ends, an Action Cable broadcast is pushed to all subscribed WebSocket clients via StreamChannel. The broadcast is dispatched through Sidekiq so it works across multiple Rails processes. |
| Live Chat | Authenticated users send messages over ChatChannel. Messages are validated through a handler chain, stored in Redis sorted sets, and broadcast to all channel subscribers. |
| JWT Authentication | Stateless JWT tokens scoped to a user + device. Tokens are stored per device in the devices_tokens table, enabling per-device logout. WebSocket connections accept the token either as a header or query parameter. |
| Stream Thumbnail Generation | On exec_publish, Nginx spawns a shell script that captures a frame via ffmpeg, obtains a presigned S3 URL from the Rails API, and uploads the thumbnail directly. The API then confirms the upload and persists the URL. |
| Authorization with Pundit | Every API action is protected by a Pundit policy. Policies are namespaced under UserApp and enforce ownership, confirmation status, and ban status before any resource is touched. |
| Background Job Processing | Sidekiq processes three queues: RTMP webhook events, chat message persistence and broadcast, and thumbnail URL updates. |
| Swagger API Docs | rswag generates interactive OpenAPI documentation mounted at /api-docs, built from RSpec request specs. |
- Docker and Docker Compose
- OBS Studio (or any RTMP encoder) for testing streams
git clone https://github.com/adeifv/live-api
cd live-apiCreate the Rails environment file:
cp rails/.env.example rails/.env # edit DB credentials, JWT secret, S3 configCreate the Nginx environment file:
cp nginx/.env.example nginx/.env # edit RTMP/HTTP ports, webhook token, HLS pathBuild and start all services:
docker compose up --buildDatabase setup (first run):
docker compose exec web bin/rails db:create db:migrate db:seedThe API is available at http://localhost:3000. HLS streams are served at http://localhost:5555/hls/<username>/index.m3u8. Sidekiq dashboard is at http://localhost:3000/sidekiq (HTTP Basic Auth via SIDEKIQ_USERNAME / SIDEKIQ_PASSWORD env vars).
In OBS, set the stream server to rtmp://localhost:1935/live and the stream key to <username>?key=<your_stream_key>. Retrieve your stream key from GET /api/user_app/v1/stream_dashboard after authenticating.
The project uses RSpec with FactoryBot, Faker, Shoulda Matchers, and DatabaseCleaner.
# Run all tests
docker compose exec web bundle exec rspec spec/tests
# Run a specific file
docker compose exec web bundle exec rspec spec/tests/to/path/test.rbTest coverage spans:
- Model specs — validations, associations, scopes, lifecycle hooks.
- Policy specs — Pundit authorization rules.
- Request specs — full HTTP request/response cycles.
- Job specs — Sidekiq job behaviour.
- Service specs
- Shared examples — reduce duplication across specs
docker compose exec web bundle exec rubocopThe .rubocop.yml enforces team style. Cops are configured to match the project's conventions.
Two GitHub Actions workflows run on every pull request touching rails/**:
- RSpec Tests (
rspec-tests.yml) — spins up PostgreSQL and Redis as service containers, installs dependencies with Bundler cache, and runs the full test suite. - Rubocop Checks (
rubocop-checks.yml) — runs the linter and fails the build on any offences.
Redis for chat persistence instead of PostgreSQL — Chat messages during a live stream are ephemeral and high-volume. Writing every message to the database would create significant write pressure and leave behind rows that are only useful while the stream is active. Redis sorted sets (scored by timestamp) give sub-millisecond writes, natural time-ordered retrieval, and trivial bulk deletion via ClearChatMessagesJob when the stream ends.
Sidekiq for ActionCable broadcasts — The RTMP webhook arrives at Nginx's worker process thread pool and needs to fan out a WebSocket event from a Sidekiq worker process. Rails' default async ActionCable adapter only works within a single process. Using the Redis adapter makes the broadcast work correctly across process boundaries with no extra infrastructure since Redis is already in the stack.
Presigned S3 uploads from Nginx shell script — Routing thumbnail binary data through Rails would consume a Rails process thread for the duration of the upload. Instead, the shell script uploads directly to MinIO using a presigned URL. Rails only handles two lightweight JSON requests (presign and confirm), keeping the API layer thin and the upload path scalable.
Chain of Responsibility for chat message handling — Chat::Handlers::BaseHandler defines a composable pipeline. New handlers (profanity filtering, rate limiting, spam detection) can be prepended or appended without modifying existing handler classes. The orchestrator simply assembles the chain.
Pundit for authorization — Pundit's policy-per-resource model maps cleanly onto the namespaced controller hierarchy (user_app/). Policies live in app/policies/user_app/ alongside the controllers they protect, making the authorization rules easy to audit. policy_scope is used on every index action so collection filtering and access control are co-located.
JWT with per-device token tracking — Stateless JWTs are portable and eliminate server-side session storage, but plain JWTs cannot be invalidated before expiry. Storing a token_issued_at timestamp per device in devices_tokens allows the API to reject tokens issued before the last logout on that device, giving effective session revocation without full server-side session state.
PostGIS as the PostgreSQL adapter — Including PostGIS from the start costs nothing and opens the door to location-aware features (finding streams near a viewer, geofencing, region-based content rules) without a schema migration later.
