>
)
}
```
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](https://developers.cloudflare.com/calls/turn/) (which offers a
free tier with 1,000 GB traffic per month) or
[Open Relay](https://www.metered.ca/stun-turn). You can also host an open
source TURN server like [coturn](https://github.com/coturn/coturn),
[Pion TURN](https://github.com/pion/turn),
[Violet](https://github.com/paullouisageneau/violet), or
[eturnal](https://github.com/processone/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, Deno)
Trystero works outside browsers too, like in Node, Bun, or Deno. 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, Bun, or Deno:
```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'
)
```
`createTopicStrategy` uses a short startup announcement burst followed by a
60-second steady interval and repeats the burst after a peer drops. Set
`reannounceOnDisconnect: false` when the signaling backend stores peer presence
rather than delivering announcements as transient events.
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](https://supabase.com) 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](https://firebase.google.com/) project
2. Create a new Realtime Database
3. Copy the `databaseURL` and use it as the `appId` in your Trystero config
Optional: configure the database with security rules to limit activity:
```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.
## 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](#supabase-setup)). 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](#encryption) for more details.
- `maxReceiveBytes` - **(optional)** Maximum payload size and budget for
concurrent large incoming transfers per room, in bytes. Defaults to
`256 * 1024 ** 2` (256 MiB). Must be a positive integer. Raise it for larger
files or lower it to reduce memory use.
- `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.
- `warnOnRelayFailure` - **(optional)** Boolean (default: `true`) that when
set to `false` suppresses warnings caused by relay failures. This includes
failed announce attempts and strategy-specific failure messages from
relays or trackers.
- `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`](https://developer.mozilla.org/en-US/docs/Web/API/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](#troubleshooting-connection-issues)).
Each item in the list should correspond to an
[ICE server config object](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection#iceservers).
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`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/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](#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
[`RTCPeerConnection`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection)s
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. To cancel an in-progress send, pass an
`AbortController`'s `signal` in the options (e.g.
`send(data, {signal: controller.signal})`) and call `controller.abort()`.
- `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.
Both kinds expose `onReceive`, a nullable callback accepting
`{byteLength, peerId, metadata, kind, signal}` and returning a boolean or a
promise of a boolean. It can also be provided in `config`. `kind` is
`'message'`, `'request'`, or `'response'`; request actions apply it to both
requests and responses. Responses have no metadata. See
[receiving files](#receiving-files).
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. Request timeouts cover approval, sending, and the response.
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](https://oxism.com)