Stream independently named application, microphone, caller, or generated-audio buses to native and browser receivers over WebRTC.
authenticated publisher
│
├─ application AudioBus ── selected receivers
├─ microphone AudioBus ─── selected receivers
└─ assistant AudioBus ──── selected receivers
Relay forwards live media. It does not capture desktop audio, run models, write recordings, or store durable application state. PocketStation Core captures and routes audio. The Control Plane creates RelaySessions and receiver invitations when a deployment uses control-plane mode.
An AudioBus is a name such as application, microphone, or assistant.
The name stays stable when a publisher reconnects; source_generation
identifies the new attachment. A receiver is authorized for one bus or for a
declared mix output.
You need Go 1.26 or newer and UDP access on the host.
Generate development credentials and start standalone mode:
export POCKETSTATION_JWT_SECRET="$(openssl rand -hex 32)"
export RELAY_INVITATION_SECRET="$(openssl rand -hex 32)"
export RELAY_AUTHORITY_MODE="standalone"
go run ./cmd/relay-serverRelay listens on http://127.0.0.1:4800 by default. In another terminal:
curl --fail http://127.0.0.1:4800/healthzThe response is ok. This confirms that the HTTP server is accepting work; it
does not confirm ICE connectivity or media delivery.
Publish three seconds of synthetic Opus with the repository test source:
go run ./cmd/relay-test-source -- \
--relay http://127.0.0.1:4800 \
--duration 3sThe command creates a temporary RelaySession and prints the session, bus, and credentials. The fixture checks transport behavior. It is not desktop capture or physical-device evidence.
Stop Relay with Ctrl-C. The default process shutdown deadline is 30 seconds;
HTTP work receives a five-second drain interval inside that deadline.
Continue with the local setup guide when a receiver or Control Plane will join the test.
Relay supports two explicit operating modes.
Use this mode when an application service owns RelaySession creation, required-bus readiness, receiver invitations, and scoped credentials.
export RELAY_AUTHORITY_MODE="control-plane"
export RELAY_API_SERVER_URL="https://control.example.com"
export POCKETSTATION_JWT_SECRET="<shared capability secret>"
export POCKETSTATION_INTERNAL_SECRET="<state synchronization secret>"
go run ./cmd/relay-serverThe Control Plane signs source and receiver capabilities. Relay verifies their issuer, audience, token type, role, expiry, RelaySession ID, and AudioBus scope. Relay sends complete attachment-state snapshots back to the Control Plane after changes and at the configured reconciliation interval.
Relay rejects local RelaySession and invitation creation endpoints in this mode.
Use standalone mode for a self-contained deployment or protocol development.
Relay creates temporary RelaySessions and single-use invitations itself. It
uses RELAY_INVITATION_SECRET to sign receiver capabilities.
Credentials from one mode are not accepted in the other. Choose one mode for a deployment; do not configure fallback verification with the other issuer or secret.
A source capability lists the buses a publisher may attach. One signaling connection can publish several WebRTC tracks:
{
"type": "PUBLISH",
"token": "<source capability>",
"publish_buses": [
{"stream_id":"application","bus_id":"application"},
{"stream_id":"microphone","bus_id":"microphone"}
],
"sdp_offer": "..."
}Every bus must be included in the capability. Stream IDs and bus IDs must be unique. Relay rejects missing credentials, ambiguous declarations, oversized messages, malformed SDP, and buses outside the token scope before attaching media.
Rust applications can use the
pocketstation-relay Connector
instead of implementing signaling, Opus, RTP, and WebRTC publication.
A receiver capability identifies exactly one RelaySession and AudioBus. Use it with WebSocket signaling or WHEP:
POST /v1/sessions/{session_id}/whep?bus=application
Authorization: Bearer <receiver capability>
Content-Type: application/sdpThe bus in the URL must match the capability. mix is an explicitly declared
output, not permission to subscribe to every source.
For all HTTP endpoints and authentication requirements, read HTTP and WebRTC. For every signaling message, field limit, and error code, read WebSocket signaling.
Relay sends one complete state document after an accepted attachment change:
{
"contract_version": 1,
"session_id": "16d2491c-86ef-4a86-9ba7-af1d2d246244",
"relay_epoch": "f78124e8-...",
"revision": 7,
"observed_at": "2026-08-21T17:45:00Z",
"buses": [
{"bus_id":"application","role":"application","source_active":true,"source_generation":2}
],
"subscriptions": [
{"subscriber_id":"a64a0c10-...","bus_id":"application"}
]
}A later complete snapshot replaces earlier Relay-owned state. Duplicate or
reordered revisions are safe to acknowledge without replaying mutations. A new
relay_epoch identifies a Relay process restart.
State-change notification enters a fixed-capacity mailbox without a lock, allocation, network request, or log call in RTP forwarding. If a notification is missed, periodic reconciliation sends the latest complete state again.
The server defaults are development values, not sizing recommendations:
| Resource | Environment variable | Default |
|---|---|---|
| active RelaySessions | RELAY_MAX_ROOMS |
100 |
| subscribers per RelaySession | RELAY_MAX_SUBSCRIBERS_PER_SESSION |
50 |
| AudioBuses per RelaySession | RELAY_MAX_BUSES_PER_SESSION |
16 |
| concurrent signaling and WHIP/WHEP handshakes | RELAY_MAX_CONCURRENT_HANDSHAKES |
128 |
| standalone RelaySession creation per client IP per minute | MAX_ROOMS_PER_IP_PER_MINUTE |
10 |
| inactive RelaySession lifetime | ROOM_EXPIRY_MINUTES |
30 minutes |
| publisher reconnect window | SOURCE_RECONNECT_WINDOW_SEC |
60 seconds |
Each receiver pacer holds 32 RTP packets and discards packets older than 120 milliseconds. Relay counts queue-full and age-related drops. When an admission limit is reached, it rejects new work with an HTTP or signaling error instead of growing resource use without a limit.
Set these values from the expected audience, host memory, CPU, bandwidth, and TURN allocation budget. Read capacity and recovery for metrics, reconnect behavior, shutdown, and the measurements required for a network claim.
- Store JWT, internal synchronization, TURN, and webhook secrets in the deployment secret manager.
- Serve public HTTP and signaling endpoints over TLS.
- Set
ALLOWED_ORIGINSto the exact HTTPS origins hosting browser receivers. - Restrict
/metricsand RelaySession diagnostic endpoints with a private network, proxy, or ingress policy; Relay does not add administrator auth to them. - Treat single-use invitation codes as credentials until redeemed or expired.
- Plan coordinated secret rotation around the remaining capability lifetime.
WebRTC uses DTLS-SRTP. Optional SFrame messages provide separate end-to-end media encryption only when both clients implement the same key exchange.
Read security before exposing Relay publicly.
Set ICE_UDP_PORT and expose that UDP port for a public Relay. Use
RELAY_PUBLIC_IPS when the server must advertise a public address. Optional
ICE-TCP uses ICE_TCP_PORT.
Set TURN_PUBLIC_IP and a 32-byte-or-longer TURN_SHARED_SECRET to enable the
embedded TURN server. Expose the selected TURN listeners and the complete UDP
allocation port range. Authentication can succeed while media still fails if
firewalls block allocation ports.
The complete environment-variable reference is in Relay configuration.
GET /metrics exposes Prometheus metrics for active RelaySessions,
subscribers, forwarded and dropped RTP packets, handshakes, ICE restarts,
callbacks, webhooks, and key exchange.
Inspect one affected RelaySession with:
GET /v1/sessions/{id}/health
GET /v1/sessions/{id}/latency
GET /v1/sessions/{id}/media-debug
GET /v1/sessions/{id}/packet-log?bus={bus_id}&limit=100
The packet-log endpoint returns at most 1,000 records. Restrict these endpoints before public deployment.
These observations describe Relay and WebRTC behavior. They do not prove the exact sample a loudspeaker played or what a person heard.
Repository and same-host tests establish component and local integration behavior. They do not establish every NAT topology, cross-network latency, multi-region behavior, or production load.
PocketStation's Fly deployment is a small, rate-limited environment used by the installed Python example. It is not Relay's default configuration, a hosted product, or an SLA. Operate Relay and the Control Plane under endpoints and limits you control for an application deployment.
| Task | Guide |
|---|---|
| Start a local Relay | Run Relay locally |
| Configure every environment variable | Configuration |
| Secure credentials and browser access | Security |
| Set capacity and recover from failures | Capacity and recovery |
| Implement a client | WebSocket signaling |
| Inspect HTTP, WHIP, WHEP, and diagnostics | HTTP and WebRTC |
scripts/check-code-protocol.sh
go test -race -short ./...
go test -race ./internal/server ./test/integrationCI must pass before deployment. The deploy workflow checks out the successful revision and records it in the OCI image. Local or same-host tests do not establish WAN/TURN or multi-region performance.
PocketStation Relay is available under the MIT license.