WebRTC Signaling

bext handles WebRTC signaling only — the exchange of SDP offers/answers and ICE candidates that peers need to establish a direct connection. bext does not relay media or act as a STUN/TURN server.

WebRTC signaling is part of bext-realtime. No additional feature flag is required beyond the base realtime module.

How It Works

  1. Peers connect to bext via WebSocket (or SSE).
  2. They join a signaling room by name.
  3. bext relays SDP and ICE messages between peers in the room.
  4. Once peers exchange enough information, they connect directly (P2P).
  5. bext is no longer in the data path.
   Peer A ──ws──▶ bext ◀──ws── Peer B
      │                          │
      └──── direct P2P ─────────┘

Signaling Rooms

Rooms isolate signaling traffic. A room is created when the first peer joins and destroyed when the last peer leaves.

const ws = new WebSocket("wss://example.com/__bext/rtc/room-42");

ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  switch (msg.type) {
    case "PeerJoined":
      startOffer(msg.peerId);
      break;
    case "Offer":
      handleOffer(msg.peerId, msg.sdp);
      break;
    case "Answer":
      handleAnswer(msg.peerId, msg.sdp);
      break;
    case "IceCandidate":
      addCandidate(msg.peerId, msg.candidate);
      break;
    case "PeerLeft":
      removePeer(msg.peerId);
      break;
  }
};

Room names are arbitrary strings. Use them to scope signaling to a call, a game lobby, or a file transfer session.

Message Types

All messages are JSON with a type field:

PeerJoined

Sent to existing peers when a new peer enters the room.

{
  "type": "PeerJoined",
  "peerId": "abc-123",
  "metadata": { "name": "Alice" }
}

Offer

An SDP offer forwarded from one peer to another.

{
  "type": "Offer",
  "peerId": "abc-123",
  "targetPeerId": "def-456",
  "sdp": "v=0\r\no=- ..."
}

Answer

An SDP answer in response to an offer.

{
  "type": "Answer",
  "peerId": "def-456",
  "targetPeerId": "abc-123",
  "sdp": "v=0\r\no=- ..."
}

IceCandidate

A trickle ICE candidate relayed between peers.

{
  "type": "IceCandidate",
  "peerId": "abc-123",
  "targetPeerId": "def-456",
  "candidate": {
    "candidate": "candidate:1 1 UDP ...",
    "sdpMLineIndex": 0,
    "sdpMid": "0"
  }
}

PeerLeft

Sent to remaining peers when someone disconnects.

{
  "type": "PeerLeft",
  "peerId": "abc-123"
}

Configuration

[realtime.signaling]
enabled = true
max_room_size = 50
room_idle_timeout_secs = 300
Field Default Description
max_room_size 50 Max peers per room.
room_idle_timeout_secs 300 Destroy room after this idle duration.

No STUN/TURN

bext only routes signaling messages. It does not implement STUN or TURN servers. For peers behind symmetric NATs, you need an external TURN server. Point clients at it in the RTCPeerConnection config:

const pc = new RTCPeerConnection({
  iceServers: [
    { urls: "stun:stun.example.com:3478" },
    { urls: "turn:turn.example.com:3478", username: "u", credential: "p" },
  ],
});

Use Cases

  • Voice and video calls — exchange SDP for media tracks, then stream directly between browsers.
  • File transfer — negotiate a DataChannel, transfer files P2P with no server bandwidth cost.
  • Multiplayer games — DataChannels for low-latency game state sync between players.
  • Screen sharing — add a screen capture track to an existing peer connection.

Authentication

Signaling WebSocket connections go through bext's standard auth middleware. Require authentication on the signaling endpoint to prevent unauthorized room access:

[[routes]]
path = "/__bext/rtc/*"
auth = "required"

Peer metadata (like display name) is attached on join and forwarded in PeerJoined messages so other peers can identify who is in the room.

Metrics

Metric Description
rtc_signaling_rooms_active Currently active rooms.
rtc_signaling_peers_connected Total connected peers.
rtc_signaling_messages_relayed Messages relayed between peers.
Note

bext handles signaling only — SDP and ICE exchange. It does not relay media. Peers behind symmetric NATs that cannot form a direct P2P path need an external TURN server; without one, those calls will silently fail to connect after the signaling phase completes successfully.

Tip

Protect signaling rooms with authentication (auth = "required" on the /__bext/rtc/* route). Unauthenticated signaling endpoints let any client join any room by guessing its name.

Related

Links