OpenVibe.Live/Docs
Source on GitHub

System Architecture#

Overview#

OpenVibe.Live is a self-hosted live streaming platform, one of the OpenVibe services:

Authentication#

Frontend loading#

The SPA is plain JavaScript with no build step. What loads is decided per route:

public/index.html        shell: navbar, home page, empty <section id="page-*"> shells, core scripts
public/features.json     registry: feature → fragment, stylesheets, scripts, dependencies, stubs, idle prefetch
                         routes: URL pattern → features
public/fragments/*.html  page markup for channel, VOD/clip players, dashboard, broadcast, chat, documentation, content, moments
public/css/features/*.css rules used only by one feature (split out of style.css / broadcast.css / i18n-star.css)
public/js/ov-loader.js   configures /shared/web-runtime.js (openvibe-shared): ov.load(feature), ov.route(path), ov.prefetch(),
                         route generations, scopes, stubs, transactional asset groups, ov.diagnostics()/ov.leaks()
public/js/app.js         core: router, auth, API helper, shared renderers, connection pill
public/js/app-*.js       route code split out of app.js: home, channel, media, chatpage, docs

Streaming Protocols#

WebRTC (browser broadcast)#

Browser → mediasoup Router → Consumers → Viewers     (viewer sockets only for live streams)

WHIP#

OBS / browser → POST /whip/:slot (slot key) → werift → mediasoup → Viewers

RTMP#

OBS → Node-Media-Server :1935 → HTTP-FLV (127.0.0.1 only) → Viewers via /api/streams/rtmp-proxy

JSMPEG#

FFmpeg → WebSocket relay → Canvas viewers

Recording, VODs and clips are produced by OpenVibe.Media from the live session (server/streaming/recorder.js starts and stops Media recordings).

Chat#

Client WS → OpenVibe.Chat (127.0.0.1:4400; nginx routes /ws/chat, /api/chat/, /api/dm/, /api/tts/, /api/sounds there)

Live runs no chat WebSocket server or chat REST routes: it delivers to Chat's typed ingress through server/chat/chat-delivery.js (producer pushes, moderation, cache hints) and reads Chat's snapshot for presence. The room is derived from the stream the client joined; offline channel chat applies the channel's bans and chat rules. Reconnects are jittered; a server_restart notice shows "OpenVibe is updating" and refills history on reconnect. See chat-system.md.

Server runtime#

ModulePathPurpose
Entryserver/index.jsExpress, security headers + report-only CSP, static mounts, WS upgrade, boot, shutdown
Assetsserver/web/assets.jscontent hashes, HTML rewriting, route assets, fragments, previous-release serving
Serializersserver/web/serializers.jspublic shapes for streams, slots, channels, profiles (secrets off by default)
Egressserver/net/egress.jsfetching user-chosen URLs: address policy, redirects, loopback proxy for yt-dlp/ffmpeg
Jobsserver/utils/jobs.jssingle-flight, jittered background loops; stopped on shutdown; stats for diagnostics
Limitsserver/utils/limit.jssemaphores for CPU-heavy work (offline encodes, stream-memory captures)
Diagnosticsserver/diagnostics.jsevent-loop delay, memory, sockets, jobs, queues, migrations → GET /api/admin/diagnostics
Log redactionserver/utils/redact.jsstream keys and credentials in log lines
Databaseserver/db/database.jsevery query, async (openvibe-sdk/db)
Migrationsmigrations/NNNN_*.sqlthe schema, applied at boot by the owner role (openvibe-sdk/db migrate: ledger, advisory lock, expand/migrate/contract phases)
Chat deliveryserver/chat/chat-delivery.jsLive's one seam to OpenVibe.Chat (pushes, moderation, cache hints, presence)
Streamsserver/streaming/routes.jsstream and slot CRUD, channel pages
Media proxyserver/media-proxy/*.jsVODs, clips, pastes, thumbnails via OpenVibe.Media
Authserver/auth/auth.js, server/auth/permissions.jsJWT/API tokens, scopes, role ranks
Analyticsserver/analytics/store.jsrequest analytics in the analytics_* tables within ADR-021 (see Analytics)

Migrations#

The schema is migrations/NNNN_*.sql, applied at boot on the owner's direct connection (DATABASE_DIRECT_URL) before the service serves on the pooled runtime role (DATABASE_URL, PgBouncer in transaction mode). Each file names its phase (expand, migrate, contract); a contract migration names the expand it completes and waits out the N-1 window. A file that has run is never edited: the next change is a new file. 0002_live.sql was generated from production's SQLite schema at the switch to PostgreSQL (2026-10-08) and carries the compatibility functions the queries use (datetime(), julianday(), ov_now()).

Analytics (ADR-021)#

openvibe-shared/analytics (the one ADR-021 module for Live, Tools and Network, wired in server/index.js) records one row per finished request in the analytics_* tables of Live's PostgreSQL database and rolls rows up hourly and daily for the Network admin dashboards. What a raw row may carry is bound by ADR-021 (OpenVibe.Contracts docs/adr/ADR-021-analytics.md):

OpenVibe Integration#

SSO and wallet come from OpenVibe.Network; media storage and processing from OpenVibe.Media. Binding inter-service contracts are in ../CONTRACTS.md (OpenVibers workspace). Deployment: deploy.md. Security posture: ../SECURITY_AUDIT.md. Measurements: performance-audit.md.

Durable events (OpenVibe.Events)#

Live publishes through one transactional outbox (event_outbox, server/events/stream-events.js; off unless EVENTS_URL and OV_OAUTH_CLIENT_SECRET are set). Every event is queued in the same PostgreSQL transaction as the change it describes.

eventwhensubject
live.stream.started / live.stream.endeda streams row goes live / endsstream <id>
live.release.deployedthe first boot that runs new commits (server/events/release-events.js), queued with the deploy_last_announced update; payload: service, release, commit, previous, commit_count, commits[] (≤ 40), deployed_at, notes_urlrelease <head sha>

The chat deploy notice is Chat's: Live queues live.release.deployed and OpenVibe.Chat folds and stores the rolling card (C-84). Live keeps no chat table of its own.

Live consumes, each on its own endpoint with its own subscription secret, signature v2 only, applied once through the SDK inbox (idempotency_receipts):

topicsendpointsecretsubscribe with
openre.session.*/internal/openre-eventsOPENRE_EVENTS_SECRETOpenRestream's scripts/subscribe-live-events.js
media.vod.*, media.clip.*, media.storage.*/internal/media-eventsMEDIA_EVENTS_SECRETscripts/subscribe-media-events.js

Media outcomes over Events#

Media reports vod.ready|failed, clip.ready|failed and storage.alert|recovered as media.* events. It writes each event in the state change's transaction, and Events delivers it to POST /internal/media-events (signature v2, MEDIA_EVENTS_SECRET). server/media-proxy/outcomes.js applies each outcome once (receipt media:<object type>:<object id>:<event id>, consumer live-media-outcomes), so a redelivery is a no-op. Only Live's tenant (MEDIA_APP_ID) counts, and media.object.uploaded is ignored.

This is the only path. The direct webhook (POST /internal/media-webhook, MEDIA_WEBHOOK_SECRET) carried the same outcomes during the transition. It was retired on 2026-10-10, after Events had delivered every one of Live's 123 media outcomes with no dead letters: Live's webhook_url was cleared in Media's apps row and seed, and the route, server/media-proxy/webhook.js, MEDIA_WEBHOOK_SECRET and MEDIA_EVENTS_AUTHORITY were deleted.

This page is rendered from docs/architecture.md in the OpenVibe.Live repository. Found a mistake? Edit it there.