Use WebRTC Data Channels
Problem
You need ultra-low-latency peer-to-peer communication for real-time applications like collaborative drawing, live audio visualization, or multiplayer games where WebSocket round-trips through the server add too much delay.
Solution
Configure WebRTC options on the TypeScript client and use connectRTC() to establish peer-to-peer data channels. Messages sent over RTC bypass the server entirely.
INFO
WebRTC data channels are currently available in the TypeScript SDK only. Python and Swift clients can still communicate with WebRTC-enabled clients over WebSocket — messages fall back automatically when the fallback option is enabled (default).
Code
import { StarfishClient } from "@starfish/sdk";
const client = new StarfishClient({
server: "ws://localhost:4000",
rtc: {
factory: RTCPeerConnection, // browser built-in
iceServers: [
{ urls: "stun:stun.l.google.com:19302" },
],
},
});
await client.connect();
await client.join("my-session");
// Connect to a peer via WebRTC
const peerId = "client-abc";
await client.connectRTC(peerId, ["control", "stream"]);
// Send via RTC using delivery options
client.send(peerId, { cursor: { x: 100, y: 200 } }, {
delivery: { reliability: "unreliable" },
});
// Use preferTransport to explicitly route via RTC
client.publish("cursors", { x: 100, y: 200 }, {
delivery: {
preferTransport: "rtc",
reliability: "unreliable",
fallback: true, // fall back to WebSocket if RTC unavailable
},
});
// Monitor RTC peer connections
client.rtcPeers$.subscribe((peers) => {
for (const peer of peers) {
console.log(`Peer ${peer.id}: ${peer.state}`);
}
});
// Disconnect from a peer
client.disconnectRTC(peerId);Explanation
Default channels
WebRTC connections include three default channels:
| Channel | Max payload | Use case |
|---|---|---|
control | 64 KB | Commands, state updates |
stream | 16 KB | High-frequency data (cursors, audio levels) |
state | — | State synchronization |
Transport selection
Use preferTransport in delivery options to control routing:
"ws"— always use WebSocket (default)"rtc"— prefer WebRTC data channels"auto"— let the client choose based on connection availability and message characteristics
Fallback behavior
When fallback: true (default), messages intended for RTC are sent over WebSocket if no RTC connection exists to the target peer. This lets you use preferTransport: "rtc" without worrying about connection state.
Production TURN
The STUN-only iceServers config in the example above works on open networks, but restrictive NATs and firewalls — common on venue, corporate, museum, and mobile carrier networks — block direct peer-to-peer connectivity. Production apps should add a TURN relay (with credentials) so RTC can still establish:
const client = new StarfishClient({
server: "ws://localhost:4000",
rtc: {
factory: RTCPeerConnection,
iceServers: [
{ urls: "stun:stun.l.google.com:19302" },
{
urls: ["turn:turn.example.com:3478", "turns:turn.example.com:5349"],
username: "1783440000:client-abc",
credential: "9f2a1b3c4d5e6f70",
},
],
},
});Servers can also advertise iceServers (including short-lived TURN credentials) in the connection welcome. Without a reachable relay, RTC won't establish and messages fall back to WebSocket as described above — correct, but without the low-latency path. See §13.2 "Production ICE/TURN" in protocol/spec/starfish-v0.1.md.
Mesh peer-count limits
The RTC data mesh is per-peer fan-out: sending to K peers is K separate sends, so a session of N peers costs O(N²) traffic — most of it from high-frequency lanes like cursors and pose. The mesh suits small groups; for broadcast or high-fan-out topics beyond ~16 peers, route that traffic over WebSocket (preferTransport: "ws"), which the server fans out cheaply:
// Large session: broadcast over WS, keep only targeted low-latency data on RTC
client.publish("room-state", state, {
delivery: { preferTransport: "ws", reliability: "reliable" },
});The 16-peer figure is an advisory warning threshold, not an enforced limit — the SDK keeps working past it. See §13.3 "Data Mesh Scaling Boundary" in protocol/spec/starfish-v0.1.md.
Variations
Auto-connect to all peers
// Automatically establish RTC with every new peer
client.peers$.subscribe((peers) => {
for (const peer of peers) {
client.connectRTC(peer.id).catch(() => {
// RTC not supported by this peer — WebSocket fallback will handle it
});
}
});Custom channel configuration
// Connect with custom channels
await client.connectRTC(peerId, ["audio", "video", "control"]);
// Send on custom channels via delivery options
client.send(peerId, audioLevelData, {
delivery: { preferTransport: "rtc", reliability: "unreliable" },
});
client.send(peerId, frameData, {
delivery: { preferTransport: "rtc", reliability: "unreliable" },
});Mixed transport messaging
// High-frequency data over RTC
client.publish("cursor-updates", cursorData, {
delivery: { preferTransport: "rtc", reliability: "unreliable" },
});
// Important commands over WebSocket
client.publish("game-events", { event: "round-start" }, {
delivery: { preferTransport: "ws", reliability: "reliable" },
priority: "high",
});Related
To send live video/audio (camera, mic, screen, canvas) over these same peer connections, see Share Realtime Media — it uses the WebRTC mesh to carry MediaStreams with broadcast/direct/topic addressing.