Baike.dev
All toolsAI codingTrendingOpen sourceNewsSubmit
Log in
< Back to tools
T

trystero

> 编程语言
Open source

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

2.7K stars0 likes0 views
WebsiteGitHub

About

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

✨✨ 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, Bun, and Deno
  • ⚛️ React hooks

You can see what people are building with Trystero here.


Contents

  • How it works
  • Get started
  • Listen for events
  • Broadcast events
  • Audio and video
  • Advanced
    • Binary metadata
    • Action promises
    • Progress updates
    • Encryption
    • React hooks
    • Troubleshooting connection issues
    • Running server-side (Node, Bun, Deno)
    • Self-hosted WebSocket relay
    • Write your own strategy
    • Supabase setup
    • Firebase setup
  • API
    • joinRoom(config, roomId, [callbacks])
    • selfId
    • getRelaySockets()
    • pauseRelayReconnection()
    • resumeRelayReconnection()
  • Which strategy should I choose?

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:

npm i trystero
import {joinRoom} from 'trystero'

No package manager? You can also use a CDN:

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

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:

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:

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

Listen for peers leaving the room:

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

Listen for peers sending their audio/video streams:

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

To unsubscribe from events, leave the room:

room.leave()

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

import {selfId} from 'trystero'

console.log(`my peer ID is ${selfId}`)

Broadcast events

Send peers your video stream:

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

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

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:

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:

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:

type CursorMove = {x: number; y: number}

const cursor = room.makeAction<CursorMove>('cursor-move')

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

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:

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:

…

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

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:

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.

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:

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:

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:

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:

…

Astute readers may notice the above example is simple and doesn't

GitHub Issues· 0 open

View all on GitHub

No open issues yet, or sync has not completed.

Highlights

  • •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, Bun, and Deno
  • •⚛️ React hooks
  • •How it works
  • •Get started

> Tags

TypeScriptbittorrentchatdecentralizedfirebase

No comments yet. Be the first to share.

> Details

PublishedAug 1, 2026
UpdatedSep 17, 2026
Category编程语言
PricingOpen source

> Related tools

T
TypeScript
JavaScript 的超集,为前端与全栈提供静态类型
P
Python
通用编程语言,广泛用于 Web、数据与 AI
G
Go
Google 推出的简洁高效系统语言