@trystero-p2p/torrent 0.25.4 β†’ 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,99 +1,33 @@
1
1
  # ✨🀝✨ Trystero
2
2
 
3
- **Build instant multiplayer web apps, no server required**
3
+ **Instant peer-to-peer multiplayer for the web β€” no server required.**
4
4
 
5
5
  πŸ‘‰ **[Try it live on trystero.dev](https://trystero.dev)** πŸ‘ˆ
6
6
 
7
- Trystero makes browsers discover each other and communicate directly. No
8
- accounts. No deploying infrastructure. Just import and connect.
7
+ Making your app work with multiple users usually means a detour into setting up
8
+ servers, auth, billing, databases, etc. β€” all so two browsers can say hello.
9
9
 
10
- Peers can connect via 🌊 BitTorrent, 🐦 Nostr, πŸ“‘ MQTT, ⚑️ Supabase, πŸ”₯Firebase,
11
- πŸͺ IPFS, or a πŸ”Œ self-hosted WebSocket relay – all using the same API.
12
-
13
- Besides making peer matching automatic, Trystero offers some nice abstractions
14
- on top of WebRTC:
15
-
16
- - πŸ‘‚πŸ“£ Rooms / broadcasting
17
- - πŸ”’πŸ“© Automatic serialization / deserialization of data
18
- - πŸŽ₯🏷 Attach metadata to binary data and media streams
19
- - βœ‚οΈβ³ Automatic chunking and throttling of large data
20
- - ⏱🀞 Progress events and promises for data transfers
21
- - πŸ”πŸ“ Session data encryption
22
- - 🏭⚑ Can run peers server-side on Node, Bun, and Deno
23
- - βš›οΈπŸͺ React hooks
24
-
25
- You can see what people are building with Trystero
26
- [here](https://github.com/jeremyckahn/awesome-trystero).
27
-
28
- ---
29
-
30
- ## Contents
31
-
32
- - [How it works](#how-it-works)
33
- - [Get started](#get-started)
34
- - [Listen for events](#listen-for-events)
35
- - [Broadcast events](#broadcast-events)
36
- - [Audio and video](#audio-and-video)
37
- - [Advanced](#advanced)
38
- - [Binary metadata](#binary-metadata)
39
- - [Action promises](#action-promises)
40
- - [Progress updates](#progress-updates)
41
- - [Encryption](#encryption)
42
- - [React hooks](#react-hooks)
43
- - [Troubleshooting connection issues](#troubleshooting-connection-issues)
44
- - [Running server-side (Node, Bun, Deno)](#running-server-side-node-bun-deno)
45
- - [Self-hosted WebSocket relay](#self-hosted-websocket-relay)
46
- - [Write your own strategy](#write-your-own-strategy)
47
- - [Supabase setup](#supabase-setup)
48
- - [Firebase setup](#firebase-setup)
49
- - [API](#api)
50
- - [`joinRoom(config, roomId, [callbacks])`](#joinroomconfig-roomid-callbacks)
51
- - [`selfId`](#selfid)
52
- - [`getRelaySockets()`](#getrelaysockets)
53
- - [`pauseRelayReconnection()`](#pauserelayreconnection)
54
- - [`resumeRelayReconnection()`](#resumerelayreconnection)
55
- - [Which strategy should I choose?](#which-strategy-should-i-choose)
10
+ **Trystero skips the backend entirely.** Import the library, join a room by
11
+ name, and browsers discover each other automatically and connect directly over
12
+ fast, end-to-end encrypted WebRTC. Out of the box, you get verified user
13
+ identities, synced user and room state, host permissions, RPC, chunked file
14
+ transfers, and live audio/video. The web was meant to be decentralized and
15
+ Trystero makes it easy to build apps where data flows without middlemen.
56
16
 
57
17
  ---
58
18
 
59
- ## How it works
60
-
61
- πŸ‘‰ **If you just want to try out Trystero, you can skip this explainer and
62
- [jump into using it](#get-started).**
63
-
64
- To establish a direct peer-to-peer connection with WebRTC, a signalling channel
65
- is needed to exchange peer information
66
- ([SDP](https://en.wikipedia.org/wiki/Session_Description_Protocol)). Typically
67
- this involves running your own matchmaking server but Trystero abstracts this
68
- away for you and offers multiple strategies for connecting peers (currently
69
- BitTorrent, Nostr, MQTT, Supabase, Firebase, IPFS, and self-hosted WebSocket
70
- relay).
19
+ ## Quickstart
71
20
 
72
- The important point to remember is this:
73
-
74
- > πŸ”’
75
- >
76
- > Beyond peer discovery, your app's data never touches the strategy medium and
77
- > is sent directly peer-to-peer and end-to-end encrypted between users.
78
- >
79
- > πŸ‘†
80
-
81
- You can [compare strategies here](#which-strategy-should-i-choose).
82
-
83
- ## Get started
84
-
85
- Install Trystero with your preferred package manager, then import it in your
86
- code:
21
+ Install Trystero from npm:
87
22
 
88
23
  ```sh
89
24
  npm i trystero
90
25
  ```
91
26
 
92
- ```js
93
- import {joinRoom} from 'trystero'
94
- ```
95
-
96
- No package manager? You can also use a CDN:
27
+ <details>
28
+ <summary>
29
+ (Or skip the build step and import from a script tag)
30
+ </summary>
97
31
 
98
32
  ```html
99
33
  <script type="module">
@@ -101,1023 +35,337 @@ No package manager? You can also use a CDN:
101
35
  </script>
102
36
  ```
103
37
 
104
- The default Trystero package runs on the Nostr network, but you can swap in any
105
- other stategy by changing which package you import:
106
-
107
- ```js
108
- import {joinRoom} from '@trystero-p2p/mqtt'
109
- // or
110
- import {joinRoom} from '@trystero-p2p/torrent'
111
- // or
112
- import {joinRoom} from '@trystero-p2p/supabase'
113
- // or
114
- import {joinRoom} from '@trystero-p2p/firebase'
115
- // or
116
- import {joinRoom} from '@trystero-p2p/ipfs'
117
- // or
118
- import {joinRoom} from '@trystero-p2p/ws-relay'
119
- ```
38
+ </details>
120
39
 
121
- Next, join the user to a room with an ID:
40
+ Join a room with a unique app ID and room name and start syncing state. Here's a
41
+ complete multiplayer cursor canvas in a dozen lines:
122
42
 
123
43
  ```js
124
- const config = {appId: 'san_narciso_3d'}
125
- const room = joinRoom(config, 'yoyodyne')
126
- ```
127
-
128
- The first argument is a configuration object that requires an `appId`. This
129
- should be a completely unique identifier for your appΒΉ. The second argument is
130
- the room ID.
131
-
132
- > Why rooms? Browsers can only handle a limited amount of WebRTC connections at
133
- > a time so it's recommended to design your app such that users are divided into
134
- > groups (or rooms, or namespaces, or channels... whatever you'd like to call
135
- > them).
44
+ import {joinRoom} from 'trystero'
136
45
 
137
- ΒΉ When using Firebase, `appId` should be your `databaseURL` and when using
138
- Supabase, it should be your project URL.
46
+ // No API keys, accounts, or backend setup. Just pick a globally unique appId
47
+ // (e.g. your app's domain or 'username/app-name') and a room name
48
+ const room = joinRoom({appId: 'pixel-party.example.com'}, 'studio-49')
139
49
 
140
- ## Listen for events
50
+ // Give every user their own signed state slot
51
+ const cursor = room.makeUserState('cursor', {x: 0, y: 0})
141
52
 
142
- Listen for peers joining the room:
53
+ window.addEventListener('pointermove', e =>
54
+ cursor.set({x: e.clientX, y: e.clientY})
55
+ )
143
56
 
144
- ```js
145
- room.onPeerJoin = peerId => console.log(`${peerId} joined`)
57
+ cursor.onChange((pos, {userId, self, removed}) => {
58
+ if (self) {
59
+ return
60
+ }
61
+ if (removed) {
62
+ removeCursor(userId)
63
+ } else {
64
+ drawCursor(userId, pos)
65
+ }
66
+ })
146
67
  ```
147
68
 
148
- Listen for peers leaving the room:
69
+ Open your page in two browsers anywhere in the world and watch the cursors move.
149
70
 
150
- ```js
151
- room.onPeerLeave = peerId => console.log(`${peerId} left`)
152
- ```
71
+ > **Local testing tip:** Tabs in the same browser profile share your `userId`
72
+ > automatically. Open a Private/Incognito window (or pass `identity: 'none'` in
73
+ > the room config) when simulating a second person on one machine.
153
74
 
154
- Listen for peers sending their audio/video streams:
75
+ ---
155
76
 
156
- ```js
157
- room.onPeerStream = (stream, peerId) =>
158
- (peerElements[peerId].video.srcObject = stream)
159
- ```
77
+ ## What you can build with it
160
78
 
161
- To unsubscribe from events, leave the room:
79
+ ### 1. Synced user & room state
162
80
 
163
- ```js
164
- room.leave()
165
- ```
81
+ Most multiplayer apps need two kinds of live state: **per-user state** that each
82
+ participant owns (names, avatars, ready flags, cursors) and **shared room
83
+ state** for the group (a game board, round timer, or watch-party queue).
166
84
 
167
- You can access the local user's peer ID by importing `selfId` like so:
85
+ Trystero builds both right onto `room`. Updates apply immediately locally,
86
+ coalesce rapid writes on the wire, verify who wrote them, and sync automatically
87
+ to late joiners β€” even if the person who last updated shared state has already
88
+ closed their laptop.
168
89
 
169
90
  ```js
170
- import {selfId} from 'trystero'
171
-
172
- console.log(`my peer ID is ${selfId}`)
173
- ```
91
+ // Each player controls only their own signed slot β€” nobody can overwrite yours
92
+ const profile = room.makeUserState('profile', {name: 'Anon', ready: false})
93
+ profile.set({name: 'Oedipa', ready: true})
174
94
 
175
- ## Broadcast events
95
+ // Snapshot of everyone currently in the room: { [userId]: { name, ready } }
96
+ console.log(profile.getAll())
176
97
 
177
- Send peers your video stream:
98
+ // Shared state for the whole room, synced automatically to latecomers
99
+ const track = room.makeState('track', {url: 'chill-beat.mp3', paused: true})
178
100
 
179
- ```js
180
- const stream = await navigator.mediaDevices.getUserMedia({
181
- audio: true,
182
- video: true
101
+ track.onChange((next, {userId}) => {
102
+ syncAudioPlayer(next)
103
+ console.log(`Updated by ${userId}`)
183
104
  })
184
- room.addStream(stream)
185
- ```
186
-
187
- Send and subscribe to custom peer-to-peer actions:
188
105
 
189
- ```js
190
- const drink = room.makeAction('drink')
106
+ await track.set(prev => ({...prev, paused: false}))
107
+ ```
191
108
 
192
- // buy drink for a friend
193
- drink.send({drink: 'negroni', withIce: true}, {target: friendId})
109
+ Using React? Both `makeUserState` and `makeState` plug straight into
110
+ `useSyncExternalStore` β€” no `useEffect` glue or manual join/leave maps:
194
111
 
195
- // buy round for the house
196
- drink.send({drink: 'mezcal', withIce: false})
112
+ ```jsx
113
+ import {useSyncExternalStore} from 'react'
197
114
 
198
- // listen for drinks sent to you
199
- drink.onMessage = (data, {peerId}) =>
200
- console.log(
201
- `got a ${data.drink} with${data.withIce ? '' : 'out'} ice from ${peerId}`
202
- )
115
+ function Lobby() {
116
+ const players = useSyncExternalStore(profile.onChange, profile.getAll)
117
+ const nowPlaying = useSyncExternalStore(track.onChange, track.get)
118
+ // render directly from `players` and `nowPlaying`
119
+ }
203
120
  ```
204
121
 
205
- Actions can also use request/response semantics:
122
+ πŸ‘‰ **[Shared & Per-User State guide β†’](https://github.com/dmotz/trystero/blob/main/docs/state.md)**
206
123
 
207
- ```js
208
- const isEven = room.makeAction('is-even', {
209
- kind: 'request',
210
- onRequest: n => n % 2 === 0
211
- })
124
+ ---
212
125
 
213
- const result = await isEven.request(42, {
214
- target: friendId,
215
- timeoutMs: 1000
216
- })
217
- ```
126
+ ### 2. Unforgeable user identity, passwords & room caps
218
127
 
219
- To ask multiple peers at once, use `requestMany()`. It resolves with a
220
- peer-labeled result for every target, while `onResult` lets you react as each
221
- peer answers:
128
+ Every participant gets a persistent, cryptographically verified `userId` out of
129
+ the box β€” no login screens or auth servers required. If someone refreshes the
130
+ page or opens your app in two tabs at once, Trystero recognizes them as the same
131
+ user automatically. (Want strictly throwaway IDs or keys loaded from your own
132
+ backend? One config option switches modes.)
222
133
 
223
- ```js
224
- const availability = room.makeAction('availability', {
225
- kind: 'request',
226
- onRequest: ({date}) => calendar.isFree(date)
227
- })
134
+ Combine `password` and `maxUsers` to spin up private, seat-capped matches like a
135
+ 2-player chess table or a 4-player co-op lobby:
228
136
 
229
- const results = await availability.requestMany(
230
- {date: '2026-05-04'},
137
+ ```js
138
+ const room = joinRoom(
231
139
  {
232
- targets: teammateIds,
233
- timeoutMs: 1000,
234
- onResult: result => {
235
- if (result.status === 'fulfilled') {
236
- updateAvailabilityBadge(result.peerId, result.value)
237
- }
238
- }
239
- }
140
+ appId: 'chess.example.com',
141
+ password: 'knight-takes-f7-secret', // encrypts discovery so outsiders stay out
142
+ maxUsers: 2 // cap this match at 2 unique players (you + 1 opponent)
143
+ },
144
+ 'table-7'
240
145
  )
241
146
 
242
- const freePeers = results
243
- .filter(result => result.status === 'fulfilled' && result.value)
244
- .map(result => result.peerId)
245
- ```
246
-
247
- If you're using TypeScript, you can add a type hint to the action:
147
+ const {userId} = await room.getSelf()
248
148
 
249
- ```typescript
250
- type CursorMove = {x: number; y: number}
149
+ room.onUserJoin(({userId}) => console.log(`Opponent ${userId} sat down`))
150
+ room.onUserLeave(({userId}) => console.log(`Opponent ${userId} left`))
251
151
 
252
- const cursor = room.makeAction<CursorMove>('cursor-move')
152
+ room.onError(err => {
153
+ if (err.code === 'room-full' && Object.keys(room.getPeers()).length === 0) {
154
+ showBanner('Sorry, both seats at this table are taken!')
155
+ }
156
+ })
253
157
  ```
254
158
 
255
- You can also use actions to send binary data, like images:
159
+ πŸ‘‰ **[Identity, Presence & Room Access guide β†’](https://github.com/dmotz/trystero/blob/main/docs/identity.md)**
256
160
 
257
- ```js
258
- const pic = room.makeAction('pic')
161
+ ---
259
162
 
260
- // blobs are automatically handled, as are any form of TypedArray
261
- canvas.toBlob(blob => pic.send(blob))
163
+ ### 3. Serverless hosts, custom roles & moderation
262
164
 
263
- // binary data is received as raw ArrayBuffers so your handling code should
264
- // interpret it in a way that makes sense
265
- pic.onMessage = (data, {peerId}) =>
266
- (imgs[peerId].src = URL.createObjectURL(new Blob([data])))
267
- ```
165
+ In a typical P2P app, everyone is equal β€” which gets awkward the moment someone
166
+ opens DevTools in your game and awards themselves a million points.
268
167
 
269
- Let's say we want users to be able to name themselves:
168
+ Import the tree-shakeable `roles()` plugin to create rooms with mathematically
169
+ verified admins, custom roles (like `'dj'` or `'moderator'`), authoritative
170
+ state that only authorized roles can edit, and peer banning β€” still with zero
171
+ backend:
270
172
 
271
173
  ```js
272
- const idsToNames = {}
273
- const name = room.makeAction('name')
174
+ import {joinRoom, roles} from 'trystero'
274
175
 
275
- // tell new peers your name when they connect
276
- room.onPeerJoin = peerId => name.send('Oedipa', {target: peerId})
176
+ // Host creates a room where their userId has verified admin authority
177
+ const room = joinRoom(
178
+ {appId: 'oedipa/friday-trivia', use: [roles({selfAsAdmin: true})]},
179
+ 'quiz-night'
180
+ )
277
181
 
278
- // listen for peers naming themselves
279
- name.onMessage = (value, {peerId}) => (idsToNames[peerId] = value)
182
+ // Share `roomId` (e.g. "quiz-night~9fX2kLm...") in an invite link;
183
+ // guests join with `use: [roles()]` and automatically verify the host
184
+ const {roomId} = await room.getSelf()
280
185
 
281
- // tell all peers at once when your name changes
282
- nameInput.addEventListener('change', e => name.send(e.target.value))
186
+ // With roles() enabled, makeState defaults to admin-only writes β€”
187
+ // everyone can read `game`, and peers reject forged edits automatically
188
+ const game = room.makeState('game', {round: 1, prompt: 'Ready?'})
189
+ await game.set({round: 2, prompt: 'Name the moons of Mars'})
283
190
 
284
- room.onPeerLeave = peerId =>
285
- console.log(`${idsToNames[peerId] || 'a weird stranger'} left`)
191
+ // Promote helpers or boot troublemakers
192
+ await room.grantRole(friendId, 'cohost')
193
+ await room.ban(grieferId, {reason: 'spoiling answers'})
286
194
  ```
287
195
 
288
- > Actions are smart and handle serialization and chunking for you behind the
289
- > scenes. This means you can send very large files and whatever data you send
290
- > will be received on the other side as the same type (a number as a number, a
291
- > string as a string, an object as an object, binary as binary, etc.).
292
-
293
- ## Audio and video
294
-
295
- Here's a simple example of how you could create an audio chatroom:
296
-
297
- ```js
298
- // this object can store audio instances for later
299
- const peerAudios = {}
300
-
301
- // get a local audio stream from the microphone
302
- const selfStream = await navigator.mediaDevices.getUserMedia({
303
- audio: true,
304
- video: false
305
- })
306
-
307
- // send stream to peers currently in the room
308
- room.addStream(selfStream)
196
+ πŸ‘‰ **[Admins, Roles & Moderation guide β†’](https://github.com/dmotz/trystero/blob/main/docs/roles.md)**
309
197
 
310
- // send stream to peers who join later
311
- room.onPeerJoin = peerId => room.addStream(selfStream, {target: peerId})
312
-
313
- // handle streams from other peers
314
- room.onPeerStream = (stream, peerId) => {
315
- // create an audio instance and set the incoming stream
316
- const audio = new Audio()
317
- audio.srcObject = stream
318
- audio.autoplay = true
198
+ ---
319
199
 
320
- // add the audio to peerAudios object if you want to address it for something
321
- // later (volume, etc.)
322
- peerAudios[peerId] = audio
323
- }
324
- ```
200
+ ### 4. Events, RPC & direct P2P file drop
325
201
 
326
- Doing the same with video is similar, just be sure to add incoming streams to
327
- video elements in the DOM:
202
+ For ephemeral events, request/response calls, and binary transfers, create typed
203
+ channels with `room.makeAction()`. Pass strings, JSON objects, `Blob`s, `File`s,
204
+ or `TypedArray`s β€” Trystero handles serialization, chunking, backpressure,
205
+ metadata headers, and transfer progress automatically, with zero cloud storage
206
+ bills:
328
207
 
329
208
  ```js
330
- const peerVideos = {}
331
- const videoContainer = document.getElementById('videos')
209
+ const drop = room.makeAction('file-drop')
332
210
 
333
- room.onPeerStream = (stream, peerId) => {
334
- let video = peerVideos[peerId]
211
+ // Optionally add a predicate too accept based on max byteLength
212
+ drop.onReceive(({byteLength}) => byteLength <= 100 * 1024 ** 2)
335
213
 
336
- // if this peer hasn't sent a stream before, create a video element
337
- if (!video) {
338
- video = document.createElement('video')
339
- video.autoplay = true
214
+ // Listen for progress events to show a loading bar
215
+ drop.onReceiveProgress((pct, {metadata}) => showDownloadBar(metadata.name, pct))
340
216
 
341
- // add video element to the DOM
342
- videoContainer.appendChild(video)
343
- }
344
-
345
- video.srcObject = stream
346
- peerVideos[peerId] = video
347
- }
348
- ```
217
+ // When the file is done being received, save it
218
+ drop.onMessage((bytes, {userId, metadata}) => saveFile(bytes, metadata.name))
349
219
 
350
- ## Advanced
351
-
352
- ### Binary metadata
353
-
354
- Let's say your app supports sending various types of files and you want to
355
- annotate the raw bytes being sent with metadata about how they should be
356
- interpreted. Instead of manually adding metadata bytes to the buffer you can
357
- simply pass a metadata argument in the sender action for your binary payload:
220
+ // Send a file to the room with optional metadata
221
+ await drop.send(file, {
222
+ metadata: {name: file.name},
223
+ onProgress: pct => showUploadBar(pct)
224
+ })
358
225
 
359
- ```js
360
- const file = room.makeAction('file')
226
+ // Create a request action where you ask a peer for something and they provide
227
+ // an answer
228
+ const roll = room.makeAction('roll-check', {kind: 'request'})
361
229
 
362
- file.onMessage = (data, {peerId, metadata}) =>
363
- console.log(
364
- `got a file (${metadata.name}) from ${peerId} with type ${metadata.type}`,
365
- data
366
- )
230
+ // Respond to roll requests from other peers
231
+ roll.onRequest(() => Math.ceil(Math.random() * 20))
367
232
 
368
- file.send(buffer, {
369
- metadata: {name: 'The CourierΚΌs Tragedy', type: 'application/pdf'}
370
- })
233
+ // Ask a specific friendId for their roll number
234
+ const result = await roll.request(null, {target: friendId, timeoutMs: 2000})
371
235
  ```
372
236
 
373
- ### Action promises
374
-
375
- Action sender functions return a promise that resolves when they're done
376
- sending. You can optionally use this to indicate to the user when a large
377
- transfer is done.
237
+ πŸ‘‰ **[Actions, RPC & File Transfers guide β†’](https://github.com/dmotz/trystero/blob/main/docs/actions.md)**
378
238
 
379
- ```js
380
- await file.send(amplePayload)
381
- console.log('done sending to all peers')
382
- ```
239
+ ---
383
240
 
384
- ### Progress updates
241
+ ### 5. Voice, video & screen sharing
385
242
 
386
- Action sender functions also take an optional callback function that will be
387
- continuously called as the transmission progresses. This can be used for showing
388
- a progress bar to the sender for large transfers. The callback is called with a
389
- percentage value between 0 and 1 and the receiving peer's ID:
243
+ Drop live audio, webcam video, screen captures, or Web Audio streams into any
244
+ room, and tag streams with `metadata` so peers can tell a facecam from a screen
245
+ share:
390
246
 
391
247
  ```js
392
- file.send(payload, {
393
- target: [peerIdA, peerIdB, peerIdC],
394
- metadata: {filename: 'paranoids.flac'},
395
- onProgress: (percent, {peerId}) => (loadingBars[peerId].value = percent)
248
+ const stream = await navigator.mediaDevices.getUserMedia({
249
+ audio: true,
250
+ video: true
396
251
  })
397
- ```
398
252
 
399
- Similarly you can listen for progress events as a receiver like this:
400
-
401
- ```js
402
- const file = room.makeAction('file')
253
+ // Send to everyone here now, plus anyone who joins later
254
+ await room.addStream(stream, {metadata: {type: 'webcam'}})
255
+ room.onUserJoin(({userId}) =>
256
+ room.addStream(stream, {target: userId, metadata: {type: 'webcam'}})
257
+ )
403
258
 
404
- file.onReceiveProgress = (percent, {peerId, metadata}) =>
405
- console.log(
406
- `${percent * 100}% done receiving ${metadata.filename} from ${peerId}`
407
- )
259
+ // Add a video when receiving a stream from someone else
260
+ room.onPeerStream((remoteStream, {userId, metadata}) =>
261
+ attachVideo(userId, remoteStream, metadata.type)
262
+ )
408
263
  ```
409
264
 
410
- Notice that any metadata is sent with progress events so you can show the
411
- receiving user that there is a transfer in progress with perhaps the name of the
412
- incoming file.
265
+ πŸ‘‰ **[Audio, Video & Screen Sharing guide β†’](https://github.com/dmotz/trystero/blob/main/docs/media.md)**
413
266
 
414
- Since a peer can send multiple transmissions in parallel, you can also use
415
- metadata to differentiate between them, e.g. by sending a unique ID.
267
+ ---
416
268
 
417
- ### Encryption
269
+ ### 6. Scale to large numbers of peers
418
270
 
419
- Once peers are connected to each other all of their communications are
420
- end-to-end encrypted. During the initial connection / discovery process, peers'
421
- [SDPs](https://en.wikipedia.org/wiki/Session_Description_Protocol) are sent via
422
- the chosen peering strategy medium. By default the SDP is encrypted using a key
423
- derived from your app ID and room ID to prevent plaintext session data from
424
- appearing in logs. This is fine for most use cases, however a relay strategy
425
- operator can reverse engineer the key using the room and app IDs. A more secure
426
- option is to pass a `password` parameter in the app configuration object which
427
- will be used to derive the encryption key:
271
+ Standard WebRTC connects every browser directly to every other browser, which is
272
+ ideal for smaller hangouts and matches. Building a 100-person collaborative
273
+ canvas or a crowded party lobby? Opt into the `scale()` plugin:
428
274
 
429
275
  ```js
430
- joinRoom({appId: 'kinneret', password: 'MuchoMaa$'}, 'w_a_s_t_e__v_i_p')
431
- ```
432
-
433
- This is a shared secret that must be known ahead of time and the password must
434
- match for all peers in the room for them to be able to connect. An example use
435
- case might be a private chat room where users learn the password via external
436
- means.
437
-
438
- ### React hooks
439
-
440
- Trystero functions are idempotent so they already work out of the box as React
441
- hooks.
276
+ import {joinRoom, scale} from 'trystero'
442
277
 
443
- Here's a simple example component where each peer syncs their favorite color to
444
- everyone else:
445
-
446
- ```jsx
447
- import {joinRoom} from 'trystero'
448
- import {useState} from 'react'
449
-
450
- const trysteroConfig = {appId: 'thurn-und-taxis'}
451
-
452
- export default function App({roomId}) {
453
- const room = joinRoom(trysteroConfig, roomId)
454
- const colorAction = room.makeAction('color')
455
- const [myColor, setMyColor] = useState('#c0ffee')
456
- const [peerColors, setPeerColors] = useState({})
457
-
458
- // whenever new peers join the room, send my color to them:
459
- room.onPeerJoin = peer => colorAction.send(myColor, {target: peer})
460
-
461
- // listen for peers sending their colors and update the state accordingly:
462
- colorAction.onMessage = (color, {peerId}) =>
463
- setPeerColors(peerColors => ({...peerColors, [peerId]: color}))
464
-
465
- const updateColor = e => {
466
- const {value} = e.target
467
-
468
- // when updating my own color, broadcast it to all peers:
469
- colorAction.send(value)
470
- setMyColor(value)
471
- }
472
-
473
- return (
474
- <>
475
- <h1>Trystero + React</h1>
476
-
477
- <h2>My color:</h2>
478
- <input type="color" value={myColor} onChange={updateColor} />
479
-
480
- <h2>Peer colors:</h2>
481
- <ul>
482
- {Object.entries(peerColors).map(([peerId, color]) => (
483
- <li key={peerId} style={{backgroundColor: color}}>
484
- {peerId}: {color}
485
- </li>
486
- ))}
487
- </ul>
488
- </>
489
- )
490
- }
278
+ const room = joinRoom({appId: 'canvas.example.com', use: [scale()]}, 'plaza')
491
279
  ```
492
280
 
493
- Astute readers may notice the above example is simple and doesn't consider if we
494
- want to change the component's room ID or unmount it. For those scenarios you
495
- can use this simple `useRoom()` hook that unsubscribes from room events
496
- accordingly:
497
-
498
- ```js
499
- import {joinRoom} from 'trystero'
500
- import {useEffect, useRef} from 'react'
281
+ Small rooms stay in a fast direct mesh with zero extra overhead. As more people
282
+ pile in, Trystero automatically forms a multi-hop gossip network so no single
283
+ browser gets overwhelmed β€” while your state, action, and media code stays
284
+ exactly the same.
501
285
 
502
- export const useRoom = (roomConfig, roomId) => {
503
- const roomRef = useRef(joinRoom(roomConfig, roomId))
504
- const lastRoomIdRef = useRef(roomId)
286
+ πŸ‘‰ **[Scaling Large Rooms guide β†’](https://github.com/dmotz/trystero/blob/main/docs/scaling.md)**
505
287
 
506
- useEffect(() => {
507
- if (roomId !== lastRoomIdRef.current) {
508
- roomRef.current.leave()
509
- roomRef.current = joinRoom(roomConfig, roomId)
510
- lastRoomIdRef.current = roomId
511
- }
512
-
513
- return () => roomRef.current.leave()
514
- }, [roomConfig, roomId])
288
+ ---
515
289
 
516
- return roomRef.current
517
- }
518
- ```
290
+ ### 7. Run headless peers in Node, Bun & Deno
519
291
 
520
- ### Troubleshooting connection issues
521
-
522
- WebRTC is powerful but some networks simply don't allow direct P2P connections
523
- using it. If you find that certain user pairings aren't working in Trystero,
524
- you're likely encountering an issue at the network provider level. To solve this
525
- you can configure a TURN server which will act as a proxy layer for peers that
526
- aren't able to connect directly to one another.
527
-
528
- 1. If you can, confirm that the issue is specific to particular network
529
- conditions (e.g. user with ISP X cannot connect to a user with ISP Y). If
530
- other user pairings are working (like those between two browsers on the same
531
- machine), this likely confirms that Trystero is working correctly.
532
- 2. Sign up for a TURN service or host your own. There are various hosted TURN
533
- services you can find online like
534
- [Cloudflare](https://developers.cloudflare.com/calls/turn/) (which offers a
535
- free tier with 1,000 GB traffic per month) or
536
- [Open Relay](https://www.metered.ca/stun-turn). You can also host an open
537
- source TURN server like [coturn](https://github.com/coturn/coturn),
538
- [Pion TURN](https://github.com/pion/turn),
539
- [Violet](https://github.com/paullouisageneau/violet), or
540
- [eturnal](https://github.com/processone/eturnal). Keep in mind data will only
541
- go through the TURN server for peers that can't directly connect and will
542
- still be end-to-end encrypted.
543
- 3. Once you have a TURN server, configure Trystero with it like this:
544
- ```js
545
- const room = joinRoom(
546
- {
547
- // ...your app config
548
- turnConfig: [
549
- {
550
- // single string or list of strings of URLs to access TURN server
551
- urls: ['turn:your-turn-server.ok:1979'],
552
- username: 'username',
553
- credential: 'password'
554
- }
555
- ]
556
- },
557
- 'roomId'
558
- )
559
- ```
560
-
561
- ### Running server-side (Node, Bun, Deno)
562
-
563
- Trystero works outside browsers too, like in Node, Bun, or Deno. Why would you
564
- want to run something that helps you avoid servers on a server? One reason is if
565
- you want an always-on peer which can be useful for remembering the last state of
566
- data, broadcasting it to new users. Another reason might be to run peers that
567
- are lighter weight and don't need a full browser running, like an embedded
568
- device or Raspberry Pi.
569
-
570
- Running server-side uses the same syntax as in the browser, but you need to
571
- import a polyfill for WebRTC support:
292
+ Trystero runs outside the browser too. Drop a lightweight peer onto a Raspberry
293
+ Pi, build a game bot, or run a silent `passive: true` peer on a tiny box to keep
294
+ shared room state warm even when every browser tab closes:
572
295
 
573
296
  ```js
574
297
  import {joinRoom} from 'trystero'
575
298
  import {RTCPeerConnection} from 'werift'
576
299
 
577
300
  const room = joinRoom(
578
- {appId: 'your-app-id', rtcPolyfill: RTCPeerConnection},
579
- 'your-room-name'
301
+ {appId: 'oedipa/synth-jam', rtcPolyfill: RTCPeerConnection, passive: true},
302
+ 'room-1'
580
303
  )
581
- ```
582
-
583
- ### Self-hosted WebSocket relay
584
-
585
- If you want a tiny relay that you control, use the WebSocket relay package.
586
- Start the relay on Node, Bun, or Deno:
587
-
588
- ```js
589
- import {createWsRelayServer} from '@trystero-p2p/ws-relay/server'
590
304
 
591
- createWsRelayServer({port: 8080})
305
+ // Silently caches and syncs the latest pattern for newcomers
306
+ room.makeState('pattern', {bpm: 120, steps: []})
592
307
  ```
593
308
 
594
- Then in the browser:
595
-
596
- ```js
597
- import {joinRoom} from '@trystero-p2p/ws-relay'
309
+ πŸ‘‰ **[Running in Node, Bun & Deno guide β†’](https://github.com/dmotz/trystero/blob/main/docs/server-side.md)**
598
310
 
599
- const room = joinRoom(
600
- {
601
- appId: 'app-id',
602
- relayConfig: {
603
- urls: ['wss://localhost:8080']
604
- }
605
- },
606
- 'room-id'
607
- )
608
- ```
609
-
610
- The `relayConfig.urls` config option is required for this strategy because there
611
- are no public default servers. You can pass multiple.
612
-
613
- ### Write your own strategy
614
-
615
- If you want to provide your own signaling backend, you can build a custom
616
- strategy with `createTopicStrategy`. This is the recommended helper for simple
617
- pub/sub relays because Trystero handles room lifecycle details like passive
618
- mode, announce scheduling, and peer-specific signaling topics.
619
-
620
- The example below assumes a WebSocket relay that does simple pub/sub routing:
621
-
622
- - Client sends
623
- `{"type": "subscribe" | "unsubscribe" | "publish", "topic", "payload"}`
624
- - Server broadcasts `{"topic", "payload"}` to subscribers of each topic
625
-
626
- This is just to show you how it works; if you want a simple self-hosted
627
- solution, use the `ws-relay` package explained above.
628
-
629
- ```js
630
- import {createTopicStrategy, toJson} from '@trystero-p2p/core'
631
-
632
- export const joinRoom = createTopicStrategy({
633
- // Define init as a function that returns a promise of your signaling client.
634
- // Resolve the promise when your client is ready to send messages.
635
- // You can also return an array of client promises for redundancy.
636
- // In this case, the client is a single WebSocket.
637
- init: config =>
638
- new Promise((resolve, reject) => {
639
- const ws = new WebSocket(config.relayConfig.urls[0])
640
- ws.addEventListener('open', () => resolve(ws), {once: true})
641
- ws.addEventListener('error', reject, {once: true})
642
- }),
643
-
644
- // Subscribe to one topic. Trystero decides which topics are needed and when.
645
- subscribeTopic: (client, topic, onMessage) => {
646
- const onWsMessage = event => {
647
- const message = JSON.parse(String(event.data))
648
-
649
- if (message.topic !== topic) {
650
- return
651
- }
652
-
653
- onMessage(message.topic, message.payload)
654
- }
655
-
656
- client.addEventListener('message', onWsMessage)
657
- client.send(toJson({type: 'subscribe', topic}))
658
-
659
- return () => {
660
- client.send(toJson({type: 'unsubscribe', topic}))
661
- client.removeEventListener('message', onWsMessage)
662
- }
663
- },
664
-
665
- // Publish a payload to one topic.
666
- publishTopic: (client, topic, payload) =>
667
- client.send(
668
- toJson({
669
- type: 'publish',
670
- topic,
671
- payload
672
- })
673
- )
674
- })
675
-
676
- const room = joinRoom(
677
- {appId: 'my-app-id', relayConfig: {urls: ['wss://my-relay.example']}},
678
- 'my-room-id'
679
- )
680
- ```
681
-
682
- `createTopicStrategy` uses a short startup announcement burst followed by a
683
- 60-second steady interval and repeats the burst after a peer drops. Set
684
- `reannounceOnDisconnect: false` when the signaling backend stores peer presence
685
- rather than delivering announcements as transient events.
686
-
687
- For non-pub/sub signaling protocols, such as trackers that exchange offers in
688
- bulk, `createStrategy` is available as a lower-level advanced API.
689
-
690
- ### Supabase setup
691
-
692
- To use the Supabase strategy:
311
+ ---
693
312
 
694
- 1. Create a [Supabase](https://supabase.com) project or use an existing one
695
- 2. On the dashboard, go to Project Settings -> API
696
- 3. Copy the Project URL and set that as the `appId` in the Trystero config, copy
697
- the `anon public` API key and set it as `relayConfig.supabaseKey` in the
698
- Trystero config
313
+ ## How does Trystero connect users with no backend?
699
314
 
700
- ### Firebase setup
315
+ WebRTC lets browsers talk directly to each other, peer-to-peer, without routing
316
+ traffic through a central server. The one catch with WebRTC has always been
317
+ _signaling_: two browsers need a brief introduction to swap connection info
318
+ before they can link up.
701
319
 
702
- If you want to use the Firebase strategy and don't have an existing project:
320
+ Trystero handles that introduction automatically out of the box using
321
+ decentralized public relays β€” nothing to deploy or configure. Once peers
322
+ connect, **your app's data never touches a relay**: state, messages, files, and
323
+ media flow straight from browser to browser, end-to-end encrypted.
703
324
 
704
- 1. Create a [Firebase](https://firebase.google.com/) project
705
- 2. Create a new Realtime Database
706
- 3. Copy the `databaseURL` and use it as the `appId` in your Trystero config
325
+ Prefer to run your own signaling setup with Nostr, MQTT, BitTorrent, Supabase,
326
+ Firebase, or a tiny self-hosted WebSocket relay? You can swap strategies with a
327
+ one-line import change β€” see the
328
+ **[Signaling & Relays guide](https://github.com/dmotz/trystero/blob/main/docs/signaling.md)**.
707
329
 
708
330
  <details>
709
- <summary>
710
- Optional: configure the database with security rules to limit activity:
711
- </summary>
712
-
713
- ```json
714
- {
715
- "rules": {
716
- ".read": false,
717
- ".write": false,
718
- "__trystero__": {
719
- ".read": false,
720
- ".write": false,
721
- "$room_id": {
722
- ".read": true,
723
- ".write": true
724
- }
725
- }
726
- }
727
- }
728
- ```
729
-
730
- These rules ensure room peer presence is only readable if the room namespace is
731
- known ahead of time.
331
+ <summary>Peek under the hood: how serverless discovery and security work</summary>
332
+
333
+ - **Encrypted signaling**: Connection offers (SDPs) are encrypted with
334
+ AES-256-GCM before touching any relay. Adding a room `password` derives the
335
+ key via WebCrypto `PBKDF2-SHA256` (600,000 iterations) so outsiders and relay
336
+ operators can't read handshakes or join the room.
337
+ - **Self-certifying identities**: Every user (`userId`) and browser tab
338
+ (`peerId`) is derived from a WebCrypto ECDSA P-256 public keypair. During
339
+ connection setup, peers verify each other's keys against the WebRTC DTLS
340
+ certificate fingerprint
341
+ ([RFC 8827](https://www.rfc-editor.org/rfc/rfc8827.html)), preventing spoofing
342
+ and man-in-the-middle relays.
343
+ - **Signed state & roles**: Updates to `makeUserState`, `makeState`, and
344
+ `roles()` carry cryptographic signatures and Hybrid Logical Clock timestamps,
345
+ allowing peers (including late joiners) to verify who wrote what without a
346
+ central server.
732
347
 
733
348
  </details>
734
349
 
735
- ## API
736
-
737
- ### `joinRoom(config, roomId, [callbacks])`
738
-
739
- Adds local user to room whereby other peers in the same namespace will open
740
- communication channels and send events. Calling `joinRoom()` multiple times with
741
- the same namespace will return the same room instance.
742
-
743
- - `config` - Configuration object containing the following keys:
744
- - `appId` - **(required)** A unique string identifying your app. When using
745
- Supabase, this should be set to your project URL (see
746
- [Supabase setup instructions](#supabase-setup)). If using Firebase, this
747
- should be the `databaseURL` from your Firebase config (also see
748
- `relayConfig.firebaseApp` below for an alternative way of configuring the
749
- Firebase strategy).
750
-
751
- - `password` - **(optional)** A string to encrypt session descriptions via
752
- AES-GCM as they are passed through the peering medium. If not set, session
753
- descriptions will be encrypted with a key derived from the app ID and room
754
- name. A custom password must match between any peers in the room for them to
755
- connect. See [encryption](#encryption) for more details.
756
-
757
- - `passive` - **(optional)** Boolean for backup or relay peers that should
758
- listen for active peers without announcing themselves while a room is
759
- dormant. Passive peers activate only after hearing a non-passive peer and
760
- include a passive flag in their signaling so passive peers do not connect to
761
- each other. For BitTorrent, dormant passive rooms announce as seeders
762
- (`left: 0`) without offers, which avoids passive-to-passive discovery while
763
- keeping tracker load low.
764
-
765
- - `relayConfig` - **(optional unless required by your chosen strategy)**
766
- Object containing strategy-specific relay settings:
767
- - `urls` - **(optional for 🌊 BitTorrent, 🐦 Nostr, πŸ“‘ MQTT; required for πŸ”Œ
768
- WebSocket relay)** Custom list of URLs for the strategy to use to
769
- bootstrap P2P connections. These would be BitTorrent trackers, Nostr
770
- relays, MQTT brokers, and WebSocket relays, respectively. They must
771
- support secure WebSocket connections.
772
-
773
- - `redundancy` - **(optional, 🌊 BitTorrent, 🐦 Nostr, πŸ“‘ MQTT only)**
774
- Integer specifying how many default relay endpoints to connect to
775
- simultaneously. Passing a `urls` option will cause this option to be
776
- ignored as the entire list will be used.
777
-
778
- - `manualReconnection` - **(optional, 🐦 Nostr and 🌊 BitTorrent only)**
779
- Boolean (default: `false`) that when set to `true` disables automatically
780
- pausing and resuming reconnection attempts when the browser goes offline
781
- and comes back online. This is useful if you want to manage this behavior
782
- yourself.
783
-
784
- - `warnOnRelayFailure` - **(optional)** Boolean (default: `true`) that when
785
- set to `false` suppresses warnings caused by relay failures. This includes
786
- failed announce attempts and strategy-specific failure messages from
787
- relays or trackers.
788
-
789
- - `supabaseKey` - **(required, ⚑️ Supabase only)** Your Supabase project's
790
- `anon public` API key.
791
-
792
- - `firebaseApp` - **(optional, πŸ”₯ Firebase only)** You can pass an already
793
- initialized Firebase app instance instead of an `appId`. Normally Trystero
794
- will initialize a Firebase app based on the `appId` but this will fail if
795
- you've already initialized it for use elsewhere.
796
-
797
- - `firebasePath` - **(optional, πŸ”₯ Firebase only)** String specifying path
798
- where Trystero writes its matchmaking data in your database
799
- (`'__trystero__'` by default). Changing this is useful if you want to run
800
- multiple apps using the same database and don't want to worry about
801
- namespace collisions.
802
-
803
- - `rtcConfig` - **(optional)** Specifies a custom
804
- [`RTCConfiguration`](https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration)
805
- for all peer connections.
806
-
807
- - `trickleIce` - **(optional)** Boolean controlling whether ICE candidates are
808
- sent incrementally (`true`) or bundled with SDP (`false`). Default is
809
- strategy-dependent: `true` for most strategies, `false` for BitTorrent and
810
- IPFS unless explicitly set.
811
-
812
- - `turnConfig` - **(optional)** Specifies a custom list of TURN servers to use
813
- (see
814
- [Troubleshooting connection issues](#troubleshooting-connection-issues)).
815
- Each item in the list should correspond to an
816
- [ICE server config object](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection#iceservers).
817
- When passing a TURN config like this, Trystero's default STUN servers will
818
- also be used. To override this and use both custom STUN and TURN servers,
819
- instead pass the config via the above `rtcConfig.iceServers` option as a
820
- list of both STUN/TURN servers β€” this won't inherit Trystero's defaults.
821
-
822
- - `rtcPolyfill` - **(optional)** Use this to pass a custom
823
- [`RTCPeerConnection`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection)-compatible
824
- constructor. This is useful for running outside of a browser, such as in
825
- Node (still experimental).
826
-
827
- - `roomId` - A string to namespace peers and events within a room.
828
-
829
- - `callbacks` - **(optional)** Callback config object containing:
830
- - `onJoinError(details)` - Called when room join fails due to an incorrect
831
- password, when handshake admission fails (including timeout), or when peers
832
- exchange SDP but WebRTC still cannot establish a direct connection. This
833
- last case usually means TURN servers are needed or misconfigured (see
834
- [Troubleshooting connection issues](#troubleshooting-connection-issues)).
835
- `details` is an object containing `appId`, `roomId`, `peerId`, and `error`
836
- describing the failure.
837
-
838
- - `onPeerHandshake(peerId, send, receive, isInitiator)` - Async predicate that
839
- runs after the transport connects but before the peer becomes active.
840
- Return/resolve to accept the peer, throw/reject to deny the peer.
841
- - `peerId` - ID of the pending peer.
842
- - `send(data, [metadata])` - Sends handshake payloads to the pending peer.
843
- - `receive()` - Resolves to the next handshake message from the peer as
844
- `{data, metadata}`.
845
- - `isInitiator` - Deterministic role flag for avoiding protocol deadlocks.
846
-
847
- - `handshakeTimeoutMs` - Timeout for pending handshakes in milliseconds
848
- (`10000` by default). If exceeded, the peer is denied and `onJoinError` is
849
- called.
850
-
851
- During handshake, the peer remains pending and is not included in `getPeers()`
852
- and does not trigger Trystero API events (`onPeerJoin`, action receivers,
853
- stream/track callbacks). Non-handshake data received while pending is dropped.
854
-
855
- Minimal handshake example:
856
-
857
- ```js
858
- import {joinRoom} from 'trystero'
859
-
860
- const room = joinRoom({appId: 'my-app'}, 'secure-room', {
861
- onPeerHandshake: async (_, send, receive, isInitiator) => {
862
- if (isInitiator) {
863
- await send({challenge: 'prove-you-know-the-secret'})
864
- const {data} = await receive()
865
-
866
- if (data?.response !== 'shared-secret') {
867
- throw new Error('handshake rejected')
868
- }
869
- } else {
870
- const {data} = await receive()
871
-
872
- if (data?.challenge !== 'prove-you-know-the-secret') {
873
- throw new Error('handshake rejected')
874
- }
875
-
876
- await send({response: 'shared-secret'})
877
- }
878
- }
879
- })
880
- ```
881
-
882
- Returns an object with the following methods:
883
-
884
- - ### `leave()`
885
-
886
- Remove local user from room and unsubscribe from room events.
887
-
888
- - ### `getPeers()`
889
-
890
- Returns a map of
891
- [`RTCPeerConnection`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection)s
892
- for the peers present in room (not including the local user). The keys of this
893
- object are the respective peers' IDs.
894
-
895
- - ### `isPassive()`
896
-
897
- Returns whether the room was joined with `passive: true`.
898
-
899
- - ### `addStream(stream, [options])`
900
-
901
- Broadcasts media stream to other peers.
902
- - `stream` - A `MediaStream` with audio and/or video to send to peers in the
903
- room.
904
-
905
- - `options.target` - **(optional)** If specified, the stream is sent only to
906
- the target peer ID (string) or list of peer IDs (array). Passing `null` or
907
- omitting this option sends to all peers in the room.
908
-
909
- - `options.metadata` - **(optional)** Additional metadata (any serializable
910
- type) to be sent with the stream. This is useful when sending multiple
911
- streams so recipients know which is which (e.g. a webcam versus a screen
912
- capture).
913
-
914
- - ### `removeStream(stream, [options])`
915
-
916
- Stops sending previously sent media stream to other peers.
917
- - `stream` - A previously sent `MediaStream` to stop sending.
918
-
919
- - `options.target` - **(optional)** If specified, the stream is removed only
920
- from the target peer ID (string) or list of peer IDs (array).
921
-
922
- - ### `addTrack(track, stream, [options])`
923
-
924
- Adds a new media track to a stream.
925
- - `track` - A `MediaStreamTrack` to add to an existing stream.
926
-
927
- - `stream` - The target `MediaStream` to attach the new track to.
928
-
929
- - `options.target` - **(optional)** If specified, the track is sent only to
930
- the target peer ID (string) or list of peer IDs (array).
931
-
932
- - `options.metadata` - **(optional)** Additional metadata (any serializable
933
- type) to be sent with the track. See `metadata` notes for `addStream()`
934
- above for more details.
935
-
936
- - ### `removeTrack(track, [options])`
937
-
938
- Removes a media track.
939
- - `track` - The `MediaStreamTrack` to remove.
940
-
941
- - `options.target` - **(optional)** If specified, the track is removed only
942
- from the target peer ID (string) or list of peer IDs (array).
943
-
944
- - ### `replaceTrack(oldTrack, newTrack, [options])`
945
-
946
- Replaces a media track with a new one.
947
- - `oldTrack` - The `MediaStreamTrack` to remove.
948
-
949
- - `newTrack` - A `MediaStreamTrack` to attach.
950
-
951
- - `options.target` - **(optional)** If specified, the track is replaced only
952
- for the target peer ID (string) or list of peer IDs (array).
953
-
954
- - `options.metadata` - **(optional)** Additional metadata (any serializable
955
- type) to be sent with the replacement track.
956
-
957
- - ### `onPeerJoin`
958
-
959
- A callback property that will be called when a peer joins the room. Assigning
960
- a new function replaces the previous handler; assigning `null` clears it.
961
- Existing active peers are immediately replayed to a newly assigned handler.
962
- - `callback(peerId)` - Function to run whenever a peer joins, called with the
963
- peer's ID.
964
-
965
- Example:
966
-
967
- ```js
968
- room.onPeerJoin = peerId => console.log(`${peerId} joined`)
969
- ```
970
-
971
- - ### `onPeerLeave`
972
-
973
- A callback property that will be called when a peer leaves the room. Assigning
974
- a new function replaces the previous handler; assigning `null` clears it.
975
- - `callback(peerId)` - Function to run whenever a peer leaves, called with the
976
- peer's ID.
977
-
978
- Example:
979
-
980
- ```js
981
- room.onPeerLeave = peerId => console.log(`${peerId} left`)
982
- ```
983
-
984
- - ### `onPeerStream`
985
-
986
- A callback property that will be called when a peer sends a media stream.
987
- Assigning a new function replaces the previous handler; assigning `null`
988
- clears it.
989
- - `callback(stream, peerId, metadata)` - Function to run whenever a peer sends
990
- a media stream, called with the the peer's stream, ID, and optional metadata
991
- (see `addStream()` above for details).
992
-
993
- Example:
994
-
995
- ```js
996
- room.onPeerStream = (stream, peerId) =>
997
- console.log(`got stream from ${peerId}`, stream)
998
- ```
999
-
1000
- - ### `onPeerTrack`
1001
-
1002
- A callback property that will be called when a peer sends a media track.
1003
- Assigning a new function replaces the previous handler; assigning `null`
1004
- clears it.
1005
- - `callback(track, stream, peerId, metadata)` - Function to run whenever a
1006
- peer sends a media track, called with the the peer's track, attached stream,
1007
- ID, and optional metadata (see `addTrack()` above for details).
1008
-
1009
- Example:
1010
-
1011
- ```js
1012
- room.onPeerTrack = (track, stream, peerId) =>
1013
- console.log(`got track from ${peerId}`, track)
1014
- ```
1015
-
1016
- - ### `makeAction(actionId, [config])`
1017
-
1018
- Listen for and send custom data actions.
1019
- - `actionId` - A string to register this action consistently among all peers.
1020
-
1021
- If `config.kind` is omitted, the action is a one-way message action. Passing
1022
- `kind: 'request'` creates a request/response action.
1023
-
1024
- Message actions expose:
1025
- - `send(data, [options])` - Sends data to peers and resolves when local
1026
- sending is complete.
1027
- - `onMessage` - Nullable callback property for received messages.
1028
- - `onReceiveProgress` - Nullable callback property for inbound progress.
1029
-
1030
- Request actions expose:
1031
- - `request(data, options)` - Sends to one peer and resolves with its response.
1032
- - `requestMany(data, options)` - Sends to many peers and resolves with
1033
- peer-labeled settled results.
1034
- - `onRequest` - Nullable callback property that returns the response.
1035
- - `onReceiveProgress` - Nullable callback property for inbound progress.
1036
-
1037
- Send options use `target`, `metadata`, `onProgress`, and `signal`. Request
1038
- options use `target`, `metadata`, `timeoutMs`, `onProgress`, and `signal`.
1039
- `requestMany()` uses `targets` plus optional `onResult` for each peer result
1040
- as it arrives.
1041
-
1042
- Example:
1043
-
1044
- ```js
1045
- const cursor = room.makeAction('cursormove')
1046
-
1047
- window.addEventListener('mousemove', e => cursor.send([e.clientX, e.clientY]))
1048
-
1049
- cursor.onMessage = ([x, y], {peerId}) => {
1050
- const peerCursor = cursorMap[peerId]
1051
- peerCursor.style.left = x + 'px'
1052
- peerCursor.style.top = y + 'px'
1053
- }
1054
- ```
1055
-
1056
- - ### `ping(peerId)`
1057
-
1058
- Takes a peer ID and returns a promise that resolves to the milliseconds the
1059
- round-trip to that peer took. Use this for measuring latency.
1060
- - `peerId` - Peer ID string of the target peer.
1061
-
1062
- Example:
1063
-
1064
- ```js
1065
- // log round-trip time every 2 seconds
1066
- room.onPeerJoin = peerId =>
1067
- setInterval(
1068
- async () => console.log(`took ${await room.ping(peerId)}ms`),
1069
- 2000
1070
- )
1071
- ```
1072
-
1073
- ### `selfId`
1074
-
1075
- A unique ID string other peers will know the local user as globally across
1076
- rooms.
1077
-
1078
- ### `getRelaySockets()`
1079
-
1080
- **(🌊 BitTorrent, 🐦 Nostr, πŸ“‘ MQTT, πŸ”Œ WebSocket relay only)** Returns an
1081
- object of relay URL keys mapped to their WebSocket connections. This can be
1082
- useful for determining the state of the user's connection to the relays and
1083
- handling any connection failures.
1084
-
1085
- Example:
1086
-
1087
- ```js
1088
- import {getRelaySockets} from '@trystero-p2p/torrent'
1089
-
1090
- console.log(getRelaySockets())
1091
- // => Object {
1092
- // "wss://tracker.webtorrent.dev": WebSocket,
1093
- // "wss://tracker.openwebtorrent.com": WebSocket
1094
- // }
1095
- ```
1096
-
1097
- ### `pauseRelayReconnection()`
1098
-
1099
- **(🐦 Nostr, 🌊 BitTorrent only)** Normally Trystero will try to automatically
1100
- reconnect to relay sockets unless `relayConfig.manualReconnection: true` is set
1101
- in the room config. Calling this function stops relay reconnection attempts
1102
- until `resumeRelayReconnection()` is called.
1103
-
1104
- ### `resumeRelayReconnection()`
1105
-
1106
- **(🐦 Nostr, 🌊 BitTorrent only)** Allows relay reconnection attempts to resume.
1107
- (See `pauseRelayReconnection()` above).
1108
-
1109
- ## Which strategy should I choose?
1110
-
1111
- By default Trystero uses the Nostr network which is highly decentralized with
1112
- hundreds of active relays running. This is a good choice if you're interested in
1113
- decentralization and high redundancy. The other decentralized strategies are
1114
- recommended in the order of MQTT, BitTorrent, and IPFS, based on robustness.
1115
- These networks have far less relay redundancy than Nostr, but you might prefer
1116
- them for other reasons. You can of course host your own relay server for any of
1117
- these strategies.
350
+ ---
1118
351
 
1119
- For a middleground between using public relays and self-hosting, the built-in
1120
- Supabase and Firebase strategies are a good option.
352
+ ## Documentation
353
+
354
+ Dive deeper into any feature or browse the full API:
355
+
356
+ | Guide | What's inside |
357
+ | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
358
+ | **[Shared & Per-User State](https://github.com/dmotz/trystero/blob/main/docs/state.md)** | `makeUserState`, `makeState`, validation rules, late-joiner sync, and React (`useSyncExternalStore`) |
359
+ | **[Identity, Presence & Room Access](https://github.com/dmotz/trystero/blob/main/docs/identity.md)** | `userId` vs `peerId`, multi-tab presence, custom identity keys, room `password`s, `maxUsers`, and `onError` |
360
+ | **[Admins, Roles & Moderation](https://github.com/dmotz/trystero/blob/main/docs/roles.md)** | `roles()`, self-certifying rooms, fixed admins, custom roles, authoritative state, and banning |
361
+ | **[Actions, RPC & File Transfers](https://github.com/dmotz/trystero/blob/main/docs/actions.md)** | `makeAction`, `send`, `request` & `requestMany`, TypeScript types, binary metadata, progress bars, and `onReceive` |
362
+ | **[Audio, Video & Screen Sharing](https://github.com/dmotz/trystero/blob/main/docs/media.md)** | `addStream`, `onPeerStream`, `removeStream`, camera/screen switching (`replaceTrack`), and stream metadata |
363
+ | **[Scaling Large Rooms](https://github.com/dmotz/trystero/blob/main/docs/scaling.md)** | Multi-hop routing with `scale()`, direct media/file promotion, and `getTopology()` |
364
+ | **[Signaling & Relays](https://github.com/dmotz/trystero/blob/main/docs/signaling.md)** | Default zero-config relays, Supabase, Firebase, MQTT, BitTorrent, self-hosted `ws-relay`, custom strategies, and bundle sizes |
365
+ | **[Running in Node, Bun & Deno](https://github.com/dmotz/trystero/blob/main/docs/server-side.md)** | Headless peers, Raspberry Pi / IoT setups, and `passive` state backup peers |
366
+ | **[NAT Traversal & Troubleshooting](https://github.com/dmotz/trystero/blob/main/docs/troubleshooting.md)** | Configuring TURN servers, diagnosing strict firewalls, and `ping()` |
367
+ | **[Full API Reference](https://github.com/dmotz/trystero/blob/main/docs/api.md)** | Complete function signatures, TypeScript types, config options, and error codes |
368
+ | **[Upgrading to 0.27.0](https://github.com/dmotz/trystero/blob/main/MIGRATION.md)** | Architectural overview, breaking changes, and before/after migration examples |
1121
369
 
1122
370
  ---
1123
371