npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@substrate-system/webrtc

v0.0.7

Published

WebRTC, simplified.

Readme

webrtc

tests module semantic versioning Common Changelog install size GZip size license

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.

Screenshot of the example app

Install

npm i -S @substrate-system/webrtc

Get Started

You can use the example app here.

.env file

You need to provide a Clouflare TURN server.

# .env
NODE_ENV="development"
DEBUG="*"
CF_TURN_TOKEN_ID="123abc"
CF_TURN_API_TOKEN="123bc"

Logs

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.

Servers

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.

Websocket Server

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

Client Example

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)
})

API

Client-Side

connect

export function connect ({ host, room }:{
  host:string;
  room:string;
}):Promise<Connection>

Create a websocket connection to the given host.

Events

peerlist
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
socket:(ws:PartySocket)=>void

Emitted when the websocket connection is established.

Parameters: ws: PartySocket

message
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
datachannel:(dc:RTCDataChannel)=>void

Emitted when a WebRTC data channel connection is established.

Parameters: dc: RTCDataChannel

peer
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
'peer-disconnect':(peerId:string)=>void

Emitted when a peer disconnects from the WebRTC connection.

Parameters: peerId: string

webrtc-close
'webrtc-close':(dc:RTCDataChannel)=>void

Emitted when a WebRTC data channel is closed.

Parameters: dc: RTCDataChannel


Security

End-to-End Encryption

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"

What the Signaling Server Can See

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.

What the TURN Server Can See

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.

Summary

| 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.


Modules

ESM

import { webrtc } from '@substrate-system/webrtc'

pre-built JS

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.

copy

cp ./node_modules/@substrate-system/webrtc/dist/index.min.js ./public/webrtc.min.js

HTML

<script type="module" src="/webrtc.min.js"></script>

Develop

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.

Deploy

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

Perfect Negotiation

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.

See Also

WebRTC For The Curious is an open-source book created by WebRTC implementers to share their hard-earned knowledge with the world.