Simpler peer-to-peer connections. Use this module to simplify webrtc data channel connections. This combines signaling events with webrtc events, because in practice, you only need to know a few things for peer connections — did we connect to a peer? which peers exist? and did we get a new message?
Warning
WIP status.
npm i -S @substrate-system/webrtc
You can use the example app here.
You need to provide a Clouflare TURN server.
# .env
NODE_ENV="development"
DEBUG="*"
CF_TURN_TOKEN_ID="123abc"
CF_TURN_API_TOKEN="123bc"
We use @substrate-system/debug for
logging. In the browser, you can set the key DEBUG
to webrtc
to enable
logging in the browser.
window.localStorage.setItem('DEBUG', 'webrtc')
This package dynamically imports @substrate-system/debug;
it is an optionalDependency
. If you want to use the debug logs for this module,
then your application should use an HTML importmap
to map @substrate-system/debug
to the debug module.
You need 2 things: a Partykit server and a TURN server. The good news is that both of these are easy & free to use.
For the TURN server, I recommend Cloudflare's service. It is easy to setup and free for demonstration purposes.
If you want to run the example app locally, you will need to create a .env file following the example in .env.example. Replace the variables with your own Cloudflare credentials.
In your Partykit project,
create a server that inherits from the /server
path here. This is a
signaling server.
See ./example_backend.
import type * as Party from 'partykit/server'
import Server, { defaultHeaders } from '@substrate-system/webtrc/server'
export default class PartyServer extends Server {
async onRequest (req:Party.Request):Promise<Response> {
const res = await super.onRequest(req)
// example: set the headers in response
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: defaultHeaders()
})
}
}
Server satisfies Party.Worker
This is browser-side code.
import { type Connection, connect } from '@substrate-system/webrtc'
import Debug from '@substrate-system/debug'
const debug = Debug(true)
const connection = await connect({
host: PARTYKIT_HOST,
room: 'example'
})
// now we are connected to the websocket server,
// because we awaited the connection
connection.on('message', ev => {
debug('message event', ev)
})
connection.on('peerlist', list => {
debug('peerlist event', list)
})
connection.on('peer', ([peerId, dc]) => {
// now we are connected to a peer
debug('peer', peerId)
debug('data channel', dc)
})
connection.on('message', ev => {
debug('got a message from peer', ev.peer)
debug('the message content', ev.data)
})
connection.on('peerlist', list => {
debug('the list of peers connected to the websocket:', list)
})
connection.on('peer', ([peerId, _dc]:[string, RTCDataChannel]) => {
// when a connection is established to a peer
debug('new peer connection', peerId)
})
export function connect ({ host, room }:{
host:string;
room:string;
}):Promise<Connection>
Create a websocket connection to the given host.
peerlist:(peers:string[]) => void
Emitted when you first connect to the websocket server with a list of peer IDs currently in the room.
Parameters: peers: string[]
socket:(ws:PartySocket)=>void
Emitted when the websocket connection is established.
Parameters: ws: PartySocket
message:(ev:{ data:string, peer:string })=>void|Promise<void>
Emitted when a message is received from a connected peer. Includes the message data and the sender's peer ID.
Parameters: { data: string, peer: string }
datachannel:(dc:RTCDataChannel)=>void
Emitted when a WebRTC data channel connection is established.
Parameters: dc: RTCDataChannel
peer:(arg:[string, RTCDataChannel])=>void
Emitted when a peer is successfully connected via WebRTC. Provides both the peer ID and the data channel.
Parameters: [peerId: string, dc: RTCDataChannel]
'peer-disconnect':(peerId:string)=>void
Emitted when a peer disconnects from the WebRTC connection.
Parameters: peerId: string
'webrtc-close':(dc:RTCDataChannel)=>void
Emitted when a WebRTC data channel is closed.
Parameters: dc: RTCDataChannel
Your messages between clients are encrypted end-to-end. WebRTC data channels use DTLS (Datagram Transport Layer Security) and SRTP (Secure Real-time Transport Protocol) by default. This means:
- All peer-to-peer communication is encrypted before it leaves your browser
- Only the sending and receiving peers can decrypt the messages
- Encryption keys are negotiated directly between peers using DTLS-SRTP key exchange
- Encryption is mandatory in WebRTC — there is no "unencrypted mode"
The Partykit (signaling) server facilitates the initial connection setup, but cannot read your peer-to-peer messages. The signaling server can see:
- Connection metadata (who is connecting to whom)
- Peer IDs and room names
- WebRTC session descriptions (SDP) — these contain network information like IP addresses and supported codecs
- ICE candidates — potential network paths for establishing connections
- Presence information (who joined/left the room)
What it CANNOT see:
- The actual content of messages sent through WebRTC data channels
- Any data transmitted peer-to-peer after the connection is established
- Encryption keys (these are negotiated directly between peers)
The signaling server's role ends once the peer-to-peer connection is established. After that, data flows directly between peers, bypassing the signaling server entirely.
The Cloudflare TURN server is used only when a direct peer-to-peer connection cannot be established (typically due to restrictive NATs or firewalls).
When TURN is used:
What it CAN see:
- That encrypted packets are being relayed between two peers
- The size and timing of packets
- Source and destination IP addresses
- The volume of traffic
What it CANNOT see:
- The content of your messages (packets are encrypted with DTLS)
- Encryption keys
- Decrypted data
The TURN server is a blind relay — it forwards encrypted packets without being able to decrypt them.
Component | Can See Metadata | Can Read Messages |
---|---|---|
Signaling Server (WebSocket) | ✅ Yes | ❌ No |
TURN Server (Cloudflare) | ✅ Yes | ❌ No |
Peers (You & Other Clients) | ✅ Yes | ✅ Yes |
The peer-to-peer messages are protected by end-to-end encryption. Neither the signaling server nor the TURN server can decrypt the messages.
import { webrtc } from '@substrate-system/webrtc'
This package exposes minified JS files too. Copy them to a location that is accessible to your web server, then link to them in HTML.
cp ./node_modules/@substrate-system/webrtc/dist/index.min.js ./public/webrtc.min.js
<script type="module" src="/webrtc.min.js"></script>
Start a local websocket server and a local vite
server for the front-end.
npm start
To run the example, you will need to create a cloudflare account
and generate credentials.
Paste the credentials into .dev.vars
.
Deploying the backend means deploying partykit.
To deploy with environment variables in a .env
file, run with the flag
--with-vars
.
npx partykit deploy --with-vars
This uses the Perfect Negotiation pattern to establish a connection.
Because WebRTC doesn't mandate a specific transport mechanism for signaling during the negotiation of a new peer connection, it's highly flexible.
Negotiation is an inherently asymmetric operation: one side needs to serve as the "caller" while the other peer is the "callee."
[...] your application doesn't need to care which end of the connection it is.
the same code is used for both the caller and the callee
Assign each of the two peers a role to play in the negotiation process:
- A polite peer, which uses ICE rollback to prevent collisions with incoming offers. May send out offers, but then responds if an offer arrives from the other peer with "Okay, never mind, drop my offer and I'll consider yours instead."
- An impolite peer, which always ignores incoming offers that collide with its own offers. Always ignores incoming offers that collide with its own offers.
Both peers know exactly what should happen if there are collisions between offers.
We assign the polite role to the first peer to connect to the signaling server.
The two peers can then work together to manage signaling in a way that doesn't deadlock.
- fippo/minimal-webrtc
- Establishing a connection: The WebRTC perfect negotiation pattern.
- A Study of WebRTC Security
- Why WebRTC encryption is a must for security
- webrtchacks.com
- WebRTC and Man in the Middle Attacks
- shinyoshiaki/werift-webrtc
- WebRTC Security: How Safe Is It?
- WebRTC For The Curious
WebRTC For The Curious is an open-source book created by WebRTC implementers to share their hard-earned knowledge with the world.