Skip to content

Relay and rooms

Who this is for: hosters (sysadmins) and technical readers.

This page is the technical reference for the private caption relay: its rooms, tokens, connection protocol and message flow. It describes the relay in captionninja/relay, an independent implementation of the browser message protocol. The relay ships as part of both supported deployments — see Choose a deployment.

The relay forwards caption text only. It performs no speech recognition and has no GPU, model or cloud dependency, so it runs separately from inference.

Rooms

Rooms are named channels on the relay. Clients join exactly one room with exactly one role per WebSocket, and rooms are isolated from one another. The configuration generator creates two rooms:

Room Purpose Typical producers Typical readers
source Raw, uncorrected captions from the capture page Capture page (write) Editor (read)
output Corrected captions for the audience Editor (write) Overlay, OBS, viewers (read)

The editor's output room must exist in the relay configuration and must differ from its source room. Rooms must be provisioned ahead of time (at most 1,000), so provisioning another event means adding rooms and restarting the relay.

Room tokens

Each room has separate read and write credentials. A read token subscribes, a write token publishes. A viewer cannot publish with a read token, and credentials for one room cannot join another room.

Role Permission Who needs it
write on source Publish raw captions Capture page
read on source Subscribe to raw captions Editor
write on output Publish corrected captions Editor
read on output Subscribe to corrected captions Overlay, OBS, viewers

These are relay credentials and are separate from the speech service's service access token (CAPTION_API_KEY). Only authenticated readers in the same room receive captions.

Connecting

Local testing uses the loopback address; internet hosting uses a TLS reverse proxy and the wss scheme:

Deployment WebSocket URL
Loopback ws://127.0.0.1:8787
Internet wss://relay.example.org/

A reliable client joins by sending a JSON join frame. The join value is the room name and role is read or write:

{"protocol":2,"join":"ROOM","role":"read","token":"TOKEN"}

The join response includes the process epoch, the next sequence and any gap reason. Returning readers resume by adding a cursor from their last position:

{"protocol":2,"join":"ROOM","role":"read","token":"TOKEN","cursor":{"epoch":"PREVIOUS_EPOCH","sequence":123}}

A legacy private join without protocol is still supported, but it has no reliable client acknowledgements and no resume cursor. One socket has one immutable room and role; viewers cannot publish.

Message flow

Raw captions travel to source, where the editor reads them. After a human reviews the text, the editor publishes the corrected captions to output, which the overlay and audience read.

capture page --write--> source --read--> editor --write--> output --read--> overlay / OBS / viewers

Writers send captions in this shape:

{"msg":true,"final":"text","id":1,"delivery":{"client":"UNIQUE_PRODUCER_ID","sequence":1}}
Field Meaning
msg Marks a caption message
final A completed caption; interm can replace it for an interim result
id Original caption identifier, kept distinct from the delivery identity
delivery.client Unique producer identifier for the retry/acknowledgement scheme
delivery.sequence Per-producer sequence number
label, ln Optional; preserved unchanged by the relay

The relay acknowledges the delivery identity and adds an epoch/sequence cursor to forwarded captions. Duplicate receipts are suppressed within the retained history.

Origin check

The relay rejects any WebSocket upgrade whose Origin is not listed in the private configuration's origins. Origins are the exact scheme plus host, written with no trailing slash and no path. Because the capture page and the editor/overlay usually run on different hosts, every page origin must be listed separately.

Origin checks supplement tokens; they are not authentication. Native clients that send no Origin header still need valid room credentials. Forwarded-IP headers are deliberately ignored, so clients behind one reverse proxy share the per-IP limit.

Bounds and defaults

The relay bounds its own resources and rejects or disconnects rather than growing buffers. Implementation defaults:

Limit Default
Total WebSockets 512
WebSockets per direct peer IP 128
WebSockets per room 256
Messages per connection per second (including join) 40
Message size 8 KiB
Caption length 4,000 characters
Pending transport output per connection 64 KiB
Join deadline 10 seconds
Heartbeat 30 seconds
Backpressure grace for slow reliable viewers 10 seconds
Unacknowledged captions retained by a publisher 100
Rooms 1,000

An optional limits object in the private configuration can raise individual bounds, for example "limits": {"perIp": 512} for large audiences behind one proxy. This raises admission capacity only; it does not enlarge caption buffers. Unknown limit names and nonpositive or noninteger values are rejected.

Replay and recovery

The relay keeps a short replay history in RAM with delivery receipts: at most 120 seconds and 256 messages per room, bounded globally by 8,192 messages and a 16 MiB accounting budget. Text never goes to disk or logs, and no audio is stored. Each room's viewing (read) token authorizes both its live captions and its retained history.

This is bounded recovery, not durable or exactly-once delivery. If the buffer expires or is evicted, the viewer shows a gap warning. Restarting the relay clears history and receipts, and an unacknowledged retry can repeat. A private relay failure never falls back to the public relay.

The editor can build a view-only OBS link that uses the output room's read token in the URL fragment:

#relayReadToken=YOUR_OUTPUT_READ_TOKEN

The page reads the fragment into memory and removes it from the address bar. Fragments are not sent in HTTP requests, but the original link is still a credential that OBS settings, the clipboard and browser history may retain. Never put a write token in an audience link.