pul.se is an open-source, self-hosted live streaming platform. Broadcasters push a video stream via RTMP (from OBS, ffmpeg, or any compatible encoder), and viewers watch it live or on-demand through the web UI. All recordings are stored automatically and available as VOD after the broadcast ends.
- Docker and Docker Compose (v2)
- An NVIDIA GPU with the NVIDIA Container Toolkit (recommended for hardware-accelerated encoding; CPU fallback available via
TRANSCODER_TYPE=CPU) - A Google Cloud project with OAuth 2.0 credentials (Client ID + Secret)
- An RSA key pair for JWT signing (see below)
Generate an RSA key pair:
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out private.pem
openssl rsa -pubout -in private.pem -out public.pem
# Base64-encode them for the environment variables:
base64 -w 0 private.pem # → JWT_PRIVATE_KEY
base64 -w 0 public.pem # → JWT_PUBLIC_KEYCopy .env.develop as a starting point and fill in every value:
| Variable | Description |
|---|---|
PULSE_HOSTNAME |
Public base URL of the deployment (e.g. https://example.com) |
PULSE_CDN |
Base URL for HLS files (e.g. https://example.com/buckets/streams) |
POSTGRES_USER |
PostgreSQL username |
POSTGRES_PASSWORD |
PostgreSQL password |
POSTGRES_DB |
PostgreSQL database name |
STORAGE_ACCESS_KEY |
S3 access key for RustFS |
STORAGE_SECRET_KEY |
S3 secret key for RustFS |
JWT_PRIVATE_KEY |
Base64-encoded RSA private key (for signing JWTs) |
JWT_PUBLIC_KEY |
Base64-encoded RSA public key (for verifying JWTs) |
GOOGLE_OAUTH_CLIENTID |
Google OAuth 2.0 client ID |
GOOGLE_OAUTH_CLIENTSECRET |
Google OAuth 2.0 client secret |
TRANSCODER_TYPE |
Set to GPU (default) for NVIDIA hardware-accelerated encoding, or CPU for software-based encoding |
In Google Cloud Console, add <PULSE_HOSTNAME>/auth/callback as an authorised redirect URI for your OAuth client.
# 1. Copy and edit the environment file
cp .env.develop .env.local
# edit .env.local with your values
# 2. Source the environment and start all services with hot reload
source .env.local
npm run developThe web UI is served on http://localhost:8080. The RustFS storage console is available at http://localhost:9001.
# 1. Export environment variables
export PULSE_HOSTNAME=https://example.com
# ... set all variables
# 2. Build and start
docker compose up --build -dRun migrations after first boot and after any schema changes:
npm run migrate| Service | Language / Runtime | Role |
|---|---|---|
| ingress | Caddy 2.11 | Reverse proxy, TLS termination, and path-based routing. |
| web | Node.js 24 + React 19 | SSR frontend and management dashboard. |
| auth | Node.js 24 + Express | Google OAuth 2.0 and JWT issuance (RS256). |
| graphql | Node.js 24 + Express | API for stream data, sessions, and user settings. |
| buckets | Node.js 24 + Express | Proxies HLS files from S3-compatible storage to the browser. |
| broadcast | Node.js 24 + Socket.io | Real-time event broadcasting to the web UI. |
| remuxer | Rust + GStreamer | RTMP server, HLS transcoder, and S3 uploader. |
| storage | RustFS | S3-compatible object store for HLS segments and playlists. |
| postgres | PostgreSQL 16 | Relational database for users, streams, and session events. |
Internet
│
┌────────────────────┴─────────────────────────────────────┐
│ │
(HTTP :80 / :443) (RTMP :1935)
┌──────────▼──────────┐ ┌──────────▼──────────┐
│ ingress │ │ remuxer │
│ (Caddy 2.11) │ │ (Rust / GStreamer) │
└─┬───┬───┬───┬───┬───┘ └─────┬─────────┬─────┘
│ │ │ │ │ │ │
│ │ │ │ └─────────────────────────────┐ │ │
│ │ │ └───────────────────┐ │ │ │
│ │ └─────────┐ │ │ │ │
┌─▼─┐┌▼───┐ ┌───▼───┐ ┌───▼─────┐ ┌───▼───┐ │ │
│web││auth│ │graphql│ │broadcast│ │buckets│────────────┼──┐ │
└───┘└─┬──┘ └───┬───┘ └─────┬───┘ └───────┘ │ │ │
│ │ │ │ │ │
────────┼────────────┼───────────────┼────────────────────────────┼──┼──────┼──────────── backend network
│ │ │ │ │ │
└────────────┴───────────────┴───┐ │ │ │
│ │ │ │
┌───▼────────────────────────▼┐ │ │
│ postgres │ │ │
└─────────────────────────────┘ │ │
│ │
┌─▼──────▼─┐
│ storage │
└──────────┘
A high-performance RTMP server built in Rust. It uses GStreamer to transcode incoming RTMP bitstreams into HLS segments (.ts) and playlists (.m3u8). It handles:
- RTMP Ingest: Listening on port 1935.
- Transcoding: Supports both hardware-accelerated (NVIDIA
nvh264enc/nvh264dec) and software-based (x264enc/avdec_h264) encoding, selectable via theTRANSCODER_TYPEenvironment variable. GPU vendor is auto-detected at startup. - Dynamic Framerate Detection: Reads the incoming stream's framerate from RTMP
on_metadataand dynamically adjusts the GOP (Group of Pictures) size to match. - Infinite HLS Playlists: Generates playlists with no segment rotation limit, allowing indefinite streaming sessions.
- S3 Uploads: Writing segments directly to the
storageservice. - State Management: Recording
PLAYandSTOPevents in PostgreSQL, with per-session metadata and stream settings snapshots.
The main frontend application built with React 19 and served with SSR. It includes:
- Landing Page: Public-facing homepage with hero section, feature showcase, and hosting information.
- Dashboard: A real-time management dashboard for streams with paginated, sortable, and searchable tables. Includes a dedicated button for creating new streams.
- Theater: A custom HLS player (using
hls.js) for watching live and archived content. Features automatic error recovery on manifest parsing failures and a minimum 2-second buffer before playback. - Live & VOD: Separate components for live stream viewing and on-demand playback of archived recordings.
- Stream Settings: Per-stream configuration page with title editing, RTMP endpoint display, stream key regeneration, broadcast settings (storage and keyframe interval), and stream deletion.
- Real-time Updates: Instant UI updates via WebSocket connection to the
broadcastservice — no page refresh required.
The data API layer. It provides a GraphQL schema for:
- Live Streams: Querying currently active broadcasts (
lives,live). - VODs: Paginated archived recordings with metadata (
vods,vod). - User Streams: Paginated stream configurations with search, sorting, and filtering.
- Management: Mutations for creating, renaming, and deleting streams, regenerating stream keys, and updating per-stream broadcast settings (storage configuration, keyframe interval).
- Pagination: Built-in support for limit/offset pagination, column sorting, and text search across all list queries.
Handles authentication via Google OAuth 2.0. It issues RS256-signed JWTs stored in httpOnly cookies, which are then verified by other services using the public key.
A proxy service that fetches HLS files from the private storage service and streams them to the browser. This prevents direct exposure of the storage service and allows for future access control.
A WebSocket service powered by socket.io. It leverages PostgreSQL LISTEN/NOTIFY to broadcast real-time database changes (e.g., stream status updates, new recordings) directly to connected clients.
A shared internal package that manages the PostgreSQL connection pool using pg.
A shared HTTP client wrapper that handles fetch requests, JSON parsing, and GraphQL error normalization.
- Encoder (OBS) connects to
remuxeron port 1935. remuxervalidates the stream key againstpostgres.- GStreamer pipeline starts, uploading segments to
s3://streams/<session_uid>/. - A
PLAYevent is inserted into theeventstable. - The stream appears as "Live" in the web dashboard.
- Browser fetches active sessions via GraphQL.
- Theater page loads the HLS playlist through the
/buckets/proxy. HLS.jsmanages segment fetching and playback.
- Any database change (e.g., a new stream starting or stopping) triggers a PostgreSQL
NOTIFY. - The
broadcastservice receives the notification and identifies the relevant user. - The update is pushed via WebSocket to the user's browser.
- The Dashboard or Theater UI updates instantly without requiring a page refresh.
uid(UUID): Primary key.email(TEXT): Unique user email.
app(UUID): Unique stream identifier used in the RTMP path.key(UUID): Private stream key for encoder authentication.name(TEXT): Display name for the stream.owner(UUID): FK tousers.uid.deleted(BOOL): Soft-delete flag.settings(JSONB): Per-stream broadcast configuration (storage credentials, keyframe interval).
uid(UUID): Session identifier for a specific broadcast.app(UUID): FK tostreams.app.name(TEXT): Title of the broadcast session.event(TEXT): Event type (PLAY|STOP).meta(JSONB): Session metadata (snapshot of stream settings at broadcast time).timestamp(TIMESTAMP): Time of the event.
A convenience view that aggregates events by app and uid, joining with streams to provide a unified broadcast record including stream state (PLAY/STOP).
A PostgreSQL on_change() trigger function fires on INSERT, UPDATE, and DELETE on the streams and events tables. It sends a pg_notify('on_change', ...) payload containing the operation type, target room (stream owner), and row data — powering real-time updates via the broadcast service.
We welcome contributions! Please follow these guidelines to ensure a smooth process.
- All code changes must be linked to an issue.
- Before starting work, please open an issue to discuss your proposed changes or report a bug.
- Pull requests that do not reference an open issue will be closed.
- Declarative Functions: Use
functiondeclarations and expressions. No arrow functions (const x = () => ...). - Explicit JSX Control Flow: Use
ifguard clauses. No ternary operators (a ? b : c) or short-circuits (a && b) inside JSX. - Strict Comparisons: Always use explicit comparisons (
=== true,!== null,=== undefined). No truthy/falsy coercion. - Hooks: Always list all dependencies in
useCallbackanduseMemo. - Data Fetching: Use the
@pul.se/clientwrapper or the provideduseGraphql/useAuthhooks.
- Adhere to standard
rustfmtandclippyconventions. - Maintain strict modularity: keep S3, DB, and GStreamer logic in their respective modules.
- Fork the repository and create a feature branch.
- Ensure your changes are focused and well-documented.
- Link to an issue in your commit messages (e.g.,
Fixes #123). - Build and Test:
- Web services:
npm run build --workspace=@pul.se/<service> - Remuxer:
cd remuxer && cargo build - Run the full stack with
npm run developto verify changes end-to-end.
- Web services:
- Open a Pull Request against
master.