GitPedia

Trystero

✨🀝✨ Build instant multiplayer webapps, no server required β€” Magic WebRTC matchmaking over BitTorrent, Nostr, MQTT, IPFS, Supabase, and Firebase

From dmotzΒ·Updated June 13, 2026Β·View on GitHubΒ·

**trystero** is a ✨🀝✨ Build instant multiplayer webapps, no server required β€” Magic WebRTC matchmaking over BitTorrent, Nostr, MQTT, IPFS, Supabase, and Firebase The project is written primarily in TypeScript, distributed under the MIT License license, first published in 2020. It has gained significant community traction with 2,598 stars and 155 forks on GitHub. Key topics include: bittorrent, chat, decentralized, firebase, ipfs.

Latest release: 0.25.2

✨🀝✨ Trystero

Build instant multiplayer web apps, no server required

πŸ‘‰ Try it live on trystero.dev πŸ‘ˆ

Trystero makes browsers discover each other and communicate directly. No
accounts. No deploying infrastructure. Just import and connect.

Peers can connect via 🌊 BitTorrent, 🐦 Nostr, πŸ“‘ MQTT, ⚑️ Supabase, πŸ”₯Firebase,
πŸͺ IPFS, or a πŸ”Œ self-hosted WebSocket relay – all using the same API.

Besides making peer matching automatic, Trystero offers some nice abstractions
on top of WebRTC:

  • πŸ‘‚πŸ“£ Rooms / broadcasting
  • πŸ”’πŸ“© Automatic serialization / deserialization of data
  • πŸŽ₯🏷 Attach metadata to binary data and media streams
  • βœ‚οΈβ³ Automatic chunking and throttling of large data
  • ⏱🀞 Progress events and promises for data transfers
  • πŸ”πŸ“ Session data encryption
  • 🏭⚑ Can run peers server-side on Node and Bun
  • βš›οΈπŸͺ React hooks

You can see what people are building with Trystero
here.


Contents


How it works

πŸ‘‰ If you just want to try out Trystero, you can skip this explainer and
jump into using it.

To establish a direct peer-to-peer connection with WebRTC, a signalling channel
is needed to exchange peer information
(SDP). Typically
this involves running your own matchmaking server but Trystero abstracts this
away for you and offers multiple strategies for connecting peers (currently
BitTorrent, Nostr, MQTT, Supabase, Firebase, IPFS, and self-hosted WebSocket
relay).

The important point to remember is this:

πŸ”’

Beyond peer discovery, your app's data never touches the strategy medium and
is sent directly peer-to-peer and end-to-end encrypted between users.

πŸ‘†

You can compare strategies here.

Get started

Install Trystero with your preferred package manager, then import it in your
code:

sh
npm i trystero
js
import {joinRoom} from 'trystero'

No package manager? You can also use a CDN:

html
<script type="module"> import {joinRoom} from 'https://esm.run/trystero' </script>

The default Trystero package runs on the Nostr network, but you can swap in any
other stategy by changing which package you import:

js
import {joinRoom} from '@trystero-p2p/mqtt' // or import {joinRoom} from '@trystero-p2p/torrent' // or import {joinRoom} from '@trystero-p2p/supabase' // or import {joinRoom} from '@trystero-p2p/firebase' // or import {joinRoom} from '@trystero-p2p/ipfs' // or import {joinRoom} from '@trystero-p2p/ws-relay'

Next, join the user to a room with an ID:

js
const config = {appId: 'san_narciso_3d'} const room = joinRoom(config, 'yoyodyne')

The first argument is a configuration object that requires an appId. This
should be a completely unique identifier for your appΒΉ. The second argument is
the room ID.

Why rooms? Browsers can only handle a limited amount of WebRTC connections at
a time so it's recommended to design your app such that users are divided into
groups (or rooms, or namespaces, or channels... whatever you'd like to call
them).

ΒΉ When using Firebase, appId should be your databaseURL and when using
Supabase, it should be your project URL.

Listen for events

Listen for peers joining the room:

js
room.onPeerJoin = peerId => console.log(`${peerId} joined`)

Listen for peers leaving the room:

js
room.onPeerLeave = peerId => console.log(`${peerId} left`)

Listen for peers sending their audio/video streams:

js
room.onPeerStream = (stream, peerId) => (peerElements[peerId].video.srcObject = stream)

To unsubscribe from events, leave the room:

js
room.leave()

You can access the local user's peer ID by importing selfId like so:

js
import {selfId} from 'trystero' console.log(`my peer ID is ${selfId}`)

Broadcast events

Send peers your video stream:

js
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true }) room.addStream(stream)

Send and subscribe to custom peer-to-peer actions:

js
const drink = room.makeAction('drink') // buy drink for a friend drink.send({drink: 'negroni', withIce: true}, {target: friendId}) // buy round for the house drink.send({drink: 'mezcal', withIce: false}) // listen for drinks sent to you drink.onMessage = (data, {peerId}) => console.log( `got a ${data.drink} with${data.withIce ? '' : 'out'} ice from ${peerId}` )

Actions can also use request/response semantics:

js
const isEven = room.makeAction('is-even', { kind: 'request', onRequest: n => n % 2 === 0 }) const result = await isEven.request(42, { target: friendId, timeoutMs: 1000 })

To ask multiple peers at once, use requestMany(). It resolves with a
peer-labeled result for every target, while onResult lets you react as each
peer answers:

js
const availability = room.makeAction('availability', { kind: 'request', onRequest: ({date}) => calendar.isFree(date) }) const results = await availability.requestMany( {date: '2026-05-04'}, { targets: teammateIds, timeoutMs: 1000, onResult: result => { if (result.status === 'fulfilled') { updateAvailabilityBadge(result.peerId, result.value) } } } ) const freePeers = results .filter(result => result.status === 'fulfilled' && result.value) .map(result => result.peerId)

If you're using TypeScript, you can add a type hint to the action:

typescript
type CursorMove = {x: number; y: number} const cursor = room.makeAction<CursorMove>('cursor-move')

You can also use actions to send binary data, like images:

js
const pic = room.makeAction('pic') // blobs are automatically handled, as are any form of TypedArray canvas.toBlob(blob => pic.send(blob)) // binary data is received as raw ArrayBuffers so your handling code should // interpret it in a way that makes sense pic.onMessage = (data, {peerId}) => (imgs[peerId].src = URL.createObjectURL(new Blob([data])))

Let's say we want users to be able to name themselves:

js
const idsToNames = {} const name = room.makeAction('name') // tell new peers your name when they connect room.onPeerJoin = peerId => name.send('Oedipa', {target: peerId}) // listen for peers naming themselves name.onMessage = (value, {peerId}) => (idsToNames[peerId] = value) // tell all peers at once when your name changes nameInput.addEventListener('change', e => name.send(e.target.value)) room.onPeerLeave = peerId => console.log(`${idsToNames[peerId] || 'a weird stranger'} left`)

Actions are smart and handle serialization and chunking for you behind the
scenes. This means you can send very large files and whatever data you send
will be received on the other side as the same type (a number as a number, a
string as a string, an object as an object, binary as binary, etc.).

Audio and video

Here's a simple example of how you could create an audio chatroom:

js
// this object can store audio instances for later const peerAudios = {} // get a local audio stream from the microphone const selfStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }) // send stream to peers currently in the room room.addStream(selfStream) // send stream to peers who join later room.onPeerJoin = peerId => room.addStream(selfStream, {target: peerId}) // handle streams from other peers room.onPeerStream = (stream, peerId) => { // create an audio instance and set the incoming stream const audio = new Audio() audio.srcObject = stream audio.autoplay = true // add the audio to peerAudios object if you want to address it for something // later (volume, etc.) peerAudios[peerId] = audio }

Doing the same with video is similar, just be sure to add incoming streams to
video elements in the DOM:

js
const peerVideos = {} const videoContainer = document.getElementById('videos') room.onPeerStream = (stream, peerId) => { let video = peerVideos[peerId] // if this peer hasn't sent a stream before, create a video element if (!video) { video = document.createElement('video') video.autoplay = true // add video element to the DOM videoContainer.appendChild(video) } video.srcObject = stream peerVideos[peerId] = video }

Advanced

Binary metadata

Let's say your app supports sending various types of files and you want to
annotate the raw bytes being sent with metadata about how they should be
interpreted. Instead of manually adding metadata bytes to the buffer you can
simply pass a metadata argument in the sender action for your binary payload:

js
const file = room.makeAction('file') file.onMessage = (data, {peerId, metadata}) => console.log( `got a file (${metadata.name}) from ${peerId} with type ${metadata.type}`, data ) file.send(buffer, { metadata: {name: 'The CourierΚΌs Tragedy', type: 'application/pdf'} })

Action promises

Action sender functions return a promise that resolves when they're done
sending. You can optionally use this to indicate to the user when a large
transfer is done.

js
await file.send(amplePayload) console.log('done sending to all peers')

Progress updates

Action sender functions also take an optional callback function that will be
continuously called as the transmission progresses. This can be used for showing
a progress bar to the sender for large transfers. The callback is called with a
percentage value between 0 and 1 and the receiving peer's ID:

js
file.send(payload, { target: [peerIdA, peerIdB, peerIdC], metadata: {filename: 'paranoids.flac'}, onProgress: (percent, {peerId}) => (loadingBars[peerId].value = percent) })

Similarly you can listen for progress events as a receiver like this:

js
const file = room.makeAction('file') file.onReceiveProgress = (percent, {peerId, metadata}) => console.log( `${percent * 100}% done receiving ${metadata.filename} from ${peerId}` )

Notice that any metadata is sent with progress events so you can show the
receiving user that there is a transfer in progress with perhaps the name of the
incoming file.

Since a peer can send multiple transmissions in parallel, you can also use
metadata to differentiate between them, e.g. by sending a unique ID.

Encryption

Once peers are connected to each other all of their communications are
end-to-end encrypted. During the initial connection / discovery process, peers'
SDPs are sent via
the chosen peering strategy medium. By default the SDP is encrypted using a key
derived from your app ID and room ID to prevent plaintext session data from
appearing in logs. This is fine for most use cases, however a relay strategy
operator can reverse engineer the key using the room and app IDs. A more secure
option is to pass a password parameter in the app configuration object which
will be used to derive the encryption key:

js
joinRoom({appId: 'kinneret', password: 'MuchoMaa$'}, 'w_a_s_t_e__v_i_p')

This is a shared secret that must be known ahead of time and the password must
match for all peers in the room for them to be able to connect. An example use
case might be a private chat room where users learn the password via external
means.

React hooks

Trystero functions are idempotent so they already work out of the box as React
hooks.

Here's a simple example component where each peer syncs their favorite color to
everyone else:

jsx
import {joinRoom} from 'trystero' import {useState} from 'react' const trysteroConfig = {appId: 'thurn-und-taxis'} export default function App({roomId}) { const room = joinRoom(trysteroConfig, roomId) const colorAction = room.makeAction('color') const [myColor, setMyColor] = useState('#c0ffee') const [peerColors, setPeerColors] = useState({}) // whenever new peers join the room, send my color to them: room.onPeerJoin = peer => colorAction.send(myColor, {target: peer}) // listen for peers sending their colors and update the state accordingly: colorAction.onMessage = (color, {peerId}) => setPeerColors(peerColors => ({...peerColors, [peerId]: color})) const updateColor = e => { const {value} = e.target // when updating my own color, broadcast it to all peers: colorAction.send(value) setMyColor(value) } return ( <> <h1>Trystero + React</h1> <h2>My color:</h2> <input type="color" value={myColor} onChange={updateColor} /> <h2>Peer colors:</h2> <ul> {Object.entries(peerColors).map(([peerId, color]) => ( <li key={peerId} style={{backgroundColor: color}}> {peerId}: {color} </li> ))} </ul> </> ) }

Astute readers may notice the above example is simple and doesn't consider if we
want to change the component's room ID or unmount it. For those scenarios you
can use this simple useRoom() hook that unsubscribes from room events
accordingly:

js
import {joinRoom} from 'trystero' import {useEffect, useRef} from 'react' export const useRoom = (roomConfig, roomId) => { const roomRef = useRef(joinRoom(roomConfig, roomId)) const lastRoomIdRef = useRef(roomId) useEffect(() => { if (roomId !== lastRoomIdRef.current) { roomRef.current.leave() roomRef.current = joinRoom(roomConfig, roomId) lastRoomIdRef.current = roomId } return () => roomRef.current.leave() }, [roomConfig, roomId]) return roomRef.current }

Troubleshooting connection issues

WebRTC is powerful but some networks simply don't allow direct P2P connections
using it. If you find that certain user pairings aren't working in Trystero,
you're likely encountering an issue at the network provider level. To solve this
you can configure a TURN server which will act as a proxy layer for peers that
aren't able to connect directly to one another.

  1. If you can, confirm that the issue is specific to particular network
    conditions (e.g. user with ISP X cannot connect to a user with ISP Y). If
    other user pairings are working (like those between two browsers on the same
    machine), this likely confirms that Trystero is working correctly.
  2. Sign up for a TURN service or host your own. There are various hosted TURN
    services you can find online like
    Cloudflare (which offers a
    free tier with 1,000 GB traffic per month) or
    Open Relay. You can also host an open
    source TURN server like coturn,
    Pion TURN,
    Violet, or
    eturnal. Keep in mind data will only
    go through the TURN server for peers that can't directly connect and will
    still be end-to-end encrypted.
  3. Once you have a TURN server, configure Trystero with it like this:
    js
    const room = joinRoom( { // ...your app config turnConfig: [ { // single string or list of strings of URLs to access TURN server urls: ['turn:your-turn-server.ok:1979'], username: 'username', credential: 'password' } ] }, 'roomId' )

Running server-side (Node, Bun)

Trystero works outside browsers too, like in Node or Bun. Why would you want to
run something that helps you avoid servers on a server? One reason is if you
want an always-on peer which can be useful for remembering the last state of
data, broadcasting it to new users. Another reason might be to run peers that
are lighter weight and don't need a full browser running, like an embedded
device or Raspberry Pi.

Running server-side uses the same syntax as in the browser, but you need to
import a polyfill for WebRTC support:

js
import {joinRoom} from 'trystero' import {RTCPeerConnection} from 'werift' const room = joinRoom( {appId: 'your-app-id', rtcPolyfill: RTCPeerConnection}, 'your-room-name' )

Self-hosted WebSocket relay

If you want a tiny relay that you control, use the WebSocket relay package.
Start the relay on Node or Bun:

js
import {createWsRelayServer} from '@trystero-p2p/ws-relay/server' createWsRelayServer({port: 8080})

Then in the browser:

js
import {joinRoom} from '@trystero-p2p/ws-relay' const room = joinRoom( { appId: 'app-id', relayConfig: { urls: ['wss://localhost:8080'] } }, 'room-id' )

The relayConfig.urls config option is required for this strategy because there
are no public default servers. You can pass multiple.

Write your own strategy

If you want to provide your own signaling backend, you can build a custom
strategy with createTopicStrategy. This is the recommended helper for simple
pub/sub relays because Trystero handles room lifecycle details like passive
mode, announce scheduling, and peer-specific signaling topics.

The example below assumes a WebSocket relay that does simple pub/sub routing:

  • Client sends
    {"type": "subscribe" | "unsubscribe" | "publish", "topic", "payload"}
  • Server broadcasts {"topic", "payload"} to subscribers of each topic

This is just to show you how it works; if you want a simple self-hosted
solution, use the ws-relay package explained above.

js
import {createTopicStrategy, toJson} from '@trystero-p2p/core' export const joinRoom = createTopicStrategy({ // Define init as a function that returns a promise of your signaling client. // Resolve the promise when your client is ready to send messages. // You can also return an array of client promises for redundancy. // In this case, the client is a single WebSocket. init: config => new Promise((resolve, reject) => { const ws = new WebSocket(config.relayConfig.urls[0]) ws.addEventListener('open', () => resolve(ws), {once: true}) ws.addEventListener('error', reject, {once: true}) }), // Subscribe to one topic. Trystero decides which topics are needed and when. subscribeTopic: (client, topic, onMessage) => { const onWsMessage = event => { const message = JSON.parse(String(event.data)) if (message.topic !== topic) { return } onMessage(message.topic, message.payload) } client.addEventListener('message', onWsMessage) client.send(toJson({type: 'subscribe', topic})) return () => { client.send(toJson({type: 'unsubscribe', topic})) client.removeEventListener('message', onWsMessage) } }, // Publish a payload to one topic. publishTopic: (client, topic, payload) => client.send( toJson({ type: 'publish', topic, payload }) ) }) const room = joinRoom( {appId: 'my-app-id', relayConfig: {urls: ['wss://my-relay.example']}}, 'my-room-id' )

For non-pub/sub signaling protocols, such as trackers that exchange offers in
bulk, createStrategy is available as a lower-level advanced API.

Supabase setup

To use the Supabase strategy:

  1. Create a Supabase project or use an existing one
  2. On the dashboard, go to Project Settings -> API
  3. Copy the Project URL and set that as the appId in the Trystero config, copy
    the anon public API key and set it as relayConfig.supabaseKey in the
    Trystero config

Firebase setup

If you want to use the Firebase strategy and don't have an existing project:

  1. Create a Firebase project
  2. Create a new Realtime Database
  3. Copy the databaseURL and use it as the appId in your Trystero config
<details> <summary> Optional: configure the database with security rules to limit activity: </summary>
json
{ "rules": { ".read": false, ".write": false, "__trystero__": { ".read": false, ".write": false, "$room_id": { ".read": true, ".write": true } } } }

These rules ensure room peer presence is only readable if the room namespace is
known ahead of time.

</details>

API

joinRoom(config, roomId, [callbacks])

Adds local user to room whereby other peers in the same namespace will open
communication channels and send events. Calling joinRoom() multiple times with
the same namespace will return the same room instance.

  • config - Configuration object containing the following keys:

    • appId - (required) A unique string identifying your app. When using
      Supabase, this should be set to your project URL (see
      Supabase setup instructions). If using Firebase, this
      should be the databaseURL from your Firebase config (also see
      relayConfig.firebaseApp below for an alternative way of configuring the
      Firebase strategy).

    • password - (optional) A string to encrypt session descriptions via
      AES-GCM as they are passed through the peering medium. If not set, session
      descriptions will be encrypted with a key derived from the app ID and room
      name. A custom password must match between any peers in the room for them to
      connect. See encryption for more details.

    • passive - (optional) Boolean for backup or relay peers that should
      listen for active peers without announcing themselves while a room is
      dormant. Passive peers activate only after hearing a non-passive peer and
      include a passive flag in their signaling so passive peers do not connect to
      each other. For BitTorrent, dormant passive rooms announce as seeders
      (left: 0) without offers, which avoids passive-to-passive discovery while
      keeping tracker load low.

    • relayConfig - (optional unless required by your chosen strategy)
      Object containing strategy-specific relay settings:

      • urls - (optional for 🌊 BitTorrent, 🐦 Nostr, πŸ“‘ MQTT; required for πŸ”Œ
        WebSocket relay)
        Custom list of URLs for the strategy to use to
        bootstrap P2P connections. These would be BitTorrent trackers, Nostr
        relays, MQTT brokers, and WebSocket relays, respectively. They must
        support secure WebSocket connections.

      • redundancy - (optional, 🌊 BitTorrent, 🐦 Nostr, πŸ“‘ MQTT only)
        Integer specifying how many default relay endpoints to connect to
        simultaneously. Passing a urls option will cause this option to be
        ignored as the entire list will be used.

      • manualReconnection - (optional, 🐦 Nostr and 🌊 BitTorrent only)
        Boolean (default: false) that when set to true disables automatically
        pausing and resuming reconnection attempts when the browser goes offline
        and comes back online. This is useful if you want to manage this behavior
        yourself.

      • supabaseKey - (required, ⚑️ Supabase only) Your Supabase project's
        anon public API key.

      • firebaseApp - (optional, πŸ”₯ Firebase only) You can pass an already
        initialized Firebase app instance instead of an appId. Normally Trystero
        will initialize a Firebase app based on the appId but this will fail if
        you've already initialized it for use elsewhere.

      • firebasePath - (optional, πŸ”₯ Firebase only) String specifying path
        where Trystero writes its matchmaking data in your database
        ('__trystero__' by default). Changing this is useful if you want to run
        multiple apps using the same database and don't want to worry about
        namespace collisions.

    • rtcConfig - (optional) Specifies a custom
      RTCConfiguration
      for all peer connections.

    • trickleIce - (optional) Boolean controlling whether ICE candidates are
      sent incrementally (true) or bundled with SDP (false). Default is
      strategy-dependent: true for most strategies, false for BitTorrent and
      IPFS unless explicitly set.

    • turnConfig - (optional) Specifies a custom list of TURN servers to use
      (see
      Troubleshooting connection issues).
      Each item in the list should correspond to an
      ICE server config object.
      When passing a TURN config like this, Trystero's default STUN servers will
      also be used. To override this and use both custom STUN and TURN servers,
      instead pass the config via the above rtcConfig.iceServers option as a
      list of both STUN/TURN servers β€” this won't inherit Trystero's defaults.

    • rtcPolyfill - (optional) Use this to pass a custom
      RTCPeerConnection-compatible
      constructor. This is useful for running outside of a browser, such as in
      Node (still experimental).

  • roomId - A string to namespace peers and events within a room.

  • callbacks - (optional) Callback config object containing:

    • onJoinError(details) - Called when room join fails due to an incorrect
      password, when handshake admission fails (including timeout), or when peers
      exchange SDP but WebRTC still cannot establish a direct connection. This
      last case usually means TURN servers are needed or misconfigured (see
      Troubleshooting connection issues).
      details is an object containing appId, roomId, peerId, and error
      describing the failure.

    • onPeerHandshake(peerId, send, receive, isInitiator) - Async predicate that
      runs after the transport connects but before the peer becomes active.
      Return/resolve to accept the peer, throw/reject to deny the peer.

      • peerId - ID of the pending peer.
      • send(data, [metadata]) - Sends handshake payloads to the pending peer.
      • receive() - Resolves to the next handshake message from the peer as
        {data, metadata}.
      • isInitiator - Deterministic role flag for avoiding protocol deadlocks.
    • handshakeTimeoutMs - Timeout for pending handshakes in milliseconds
      (10000 by default). If exceeded, the peer is denied and onJoinError is
      called.

    During handshake, the peer remains pending and is not included in getPeers()
    and does not trigger Trystero API events (onPeerJoin, action receivers,
    stream/track callbacks). Non-handshake data received while pending is dropped.

    Minimal handshake example:

    js
    import {joinRoom} from 'trystero' const room = joinRoom({appId: 'my-app'}, 'secure-room', { onPeerHandshake: async (_, send, receive, isInitiator) => { if (isInitiator) { await send({challenge: 'prove-you-know-the-secret'}) const {data} = await receive() if (data?.response !== 'shared-secret') { throw new Error('handshake rejected') } } else { const {data} = await receive() if (data?.challenge !== 'prove-you-know-the-secret') { throw new Error('handshake rejected') } await send({response: 'shared-secret'}) } } })

Returns an object with the following methods:

  • leave()

    Remove local user from room and unsubscribe from room events.

  • getPeers()

    Returns a map of
    RTCPeerConnections
    for the peers present in room (not including the local user). The keys of this
    object are the respective peers' IDs.

  • isPassive()

    Returns whether the room was joined with passive: true.

  • addStream(stream, [options])

    Broadcasts media stream to other peers.

    • stream - A MediaStream with audio and/or video to send to peers in the
      room.

    • options.target - (optional) If specified, the stream is sent only to
      the target peer ID (string) or list of peer IDs (array). Passing null or
      omitting this option sends to all peers in the room.

    • options.metadata - (optional) Additional metadata (any serializable
      type) to be sent with the stream. This is useful when sending multiple
      streams so recipients know which is which (e.g. a webcam versus a screen
      capture).

  • removeStream(stream, [options])

    Stops sending previously sent media stream to other peers.

    • stream - A previously sent MediaStream to stop sending.

    • options.target - (optional) If specified, the stream is removed only
      from the target peer ID (string) or list of peer IDs (array).

  • addTrack(track, stream, [options])

    Adds a new media track to a stream.

    • track - A MediaStreamTrack to add to an existing stream.

    • stream - The target MediaStream to attach the new track to.

    • options.target - (optional) If specified, the track is sent only to
      the target peer ID (string) or list of peer IDs (array).

    • options.metadata - (optional) Additional metadata (any serializable
      type) to be sent with the track. See metadata notes for addStream()
      above for more details.

  • removeTrack(track, [options])

    Removes a media track.

    • track - The MediaStreamTrack to remove.

    • options.target - (optional) If specified, the track is removed only
      from the target peer ID (string) or list of peer IDs (array).

  • replaceTrack(oldTrack, newTrack, [options])

    Replaces a media track with a new one.

    • oldTrack - The MediaStreamTrack to remove.

    • newTrack - A MediaStreamTrack to attach.

    • options.target - (optional) If specified, the track is replaced only
      for the target peer ID (string) or list of peer IDs (array).

    • options.metadata - (optional) Additional metadata (any serializable
      type) to be sent with the replacement track.

  • onPeerJoin

    A callback property that will be called when a peer joins the room. Assigning
    a new function replaces the previous handler; assigning null clears it.
    Existing active peers are immediately replayed to a newly assigned handler.

    • callback(peerId) - Function to run whenever a peer joins, called with the
      peer's ID.

    Example:

    js
    room.onPeerJoin = peerId => console.log(`${peerId} joined`)
  • onPeerLeave

    A callback property that will be called when a peer leaves the room. Assigning
    a new function replaces the previous handler; assigning null clears it.

    • callback(peerId) - Function to run whenever a peer leaves, called with the
      peer's ID.

    Example:

    js
    room.onPeerLeave = peerId => console.log(`${peerId} left`)
  • onPeerStream

    A callback property that will be called when a peer sends a media stream.
    Assigning a new function replaces the previous handler; assigning null
    clears it.

    • callback(stream, peerId, metadata) - Function to run whenever a peer sends
      a media stream, called with the the peer's stream, ID, and optional metadata
      (see addStream() above for details).

    Example:

    js
    room.onPeerStream = (stream, peerId) => console.log(`got stream from ${peerId}`, stream)
  • onPeerTrack

    A callback property that will be called when a peer sends a media track.
    Assigning a new function replaces the previous handler; assigning null
    clears it.

    • callback(track, stream, peerId, metadata) - Function to run whenever a
      peer sends a media track, called with the the peer's track, attached stream,
      ID, and optional metadata (see addTrack() above for details).

    Example:

    js
    room.onPeerTrack = (track, stream, peerId) => console.log(`got track from ${peerId}`, track)
  • makeAction(actionId, [config])

    Listen for and send custom data actions.

    • actionId - A string to register this action consistently among all peers.

    If config.kind is omitted, the action is a one-way message action. Passing
    kind: 'request' creates a request/response action.

    Message actions expose:

    • send(data, [options]) - Sends data to peers and resolves when local
      sending is complete.
    • onMessage - Nullable callback property for received messages.
    • onReceiveProgress - Nullable callback property for inbound progress.

    Request actions expose:

    • request(data, options) - Sends to one peer and resolves with its response.
    • requestMany(data, options) - Sends to many peers and resolves with
      peer-labeled settled results.
    • onRequest - Nullable callback property that returns the response.
    • onReceiveProgress - Nullable callback property for inbound progress.

    Send options use target, metadata, onProgress, and signal. Request
    options use target, metadata, timeoutMs, onProgress, and signal.
    requestMany() uses targets plus optional onResult for each peer result
    as it arrives.

    Example:

    js
    const cursor = room.makeAction('cursormove') window.addEventListener('mousemove', e => cursor.send([e.clientX, e.clientY])) cursor.onMessage = ([x, y], {peerId}) => { const peerCursor = cursorMap[peerId] peerCursor.style.left = x + 'px' peerCursor.style.top = y + 'px' }
  • ping(peerId)

    Takes a peer ID and returns a promise that resolves to the milliseconds the
    round-trip to that peer took. Use this for measuring latency.

    • peerId - Peer ID string of the target peer.

    Example:

    js
    // log round-trip time every 2 seconds room.onPeerJoin = peerId => setInterval( async () => console.log(`took ${await room.ping(peerId)}ms`), 2000 )

selfId

A unique ID string other peers will know the local user as globally across
rooms.

getRelaySockets()

(🌊 BitTorrent, 🐦 Nostr, πŸ“‘ MQTT, πŸ”Œ WebSocket relay only) Returns an
object of relay URL keys mapped to their WebSocket connections. This can be
useful for determining the state of the user's connection to the relays and
handling any connection failures.

Example:

js
import {getRelaySockets} from '@trystero-p2p/torrent' console.log(getRelaySockets()) // => Object { // "wss://tracker.webtorrent.dev": WebSocket, // "wss://tracker.openwebtorrent.com": WebSocket // }

pauseRelayReconnection()

(🐦 Nostr, 🌊 BitTorrent only) Normally Trystero will try to automatically
reconnect to relay sockets unless relayConfig.manualReconnection: true is set
in the room config. Calling this function stops relay reconnection attempts
until resumeRelayReconnection() is called.

resumeRelayReconnection()

(🐦 Nostr, 🌊 BitTorrent only) Allows relay reconnection attempts to resume.
(See pauseRelayReconnection() above).

Which strategy should I choose?

By default Trystero uses the Nostr network which is highly decentralized with
hundreds of active relays running. This is a good choice if you're interested in
decentralization and high redundancy. The other decentralized strategies are
recommended in the order of MQTT, BitTorrent, and IPFS, based on robustness.
These networks have far less relay redundancy than Nostr, but you might prefer
them for other reasons. You can of course host your own relay server for any of
these strategies.

For a middleground between using public relays and self-hosting, the built-in
Supabase and Firebase strategies are a good option.


Trystero by Dan Motzenbecker

Contributors

Showing top 10 contributors by commit count.

View all contributors on GitHub β†’

This article is auto-generated from dmotz/trystero via the GitHub API.Last fetched: 6/14/2026