{"id":13691666,"url":"https://github.com/dmotz/trystero","last_synced_at":"2025-04-10T13:56:34.769Z","repository":{"id":44451460,"uuid":"307219355","full_name":"dmotz/trystero","owner":"dmotz","description":"✨🤝✨ Build instant multiplayer webapps, no server required — Magic WebRTC matchmaking over BitTorrent, Nostr, MQTT, IPFS, Supabase, and Firebase","archived":false,"fork":false,"pushed_at":"2025-04-02T01:51:56.000Z","size":1305,"stargazers_count":1476,"open_issues_count":24,"forks_count":105,"subscribers_count":20,"default_branch":"main","last_synced_at":"2025-04-03T09:03:52.334Z","etag":null,"topics":["bittorrent","chat","dapp","decentralized","firebase","ipfs","javascript","matchmaking","mqtt","nostr","p2p","peer-to-peer","realtime","serverless","signaling","signalling","supabase","web3","webrtc","webtorrent"],"latest_commit_sha":null,"homepage":"https://oxism.com/trystero","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dmotz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-10-26T00:11:07.000Z","updated_at":"2025-04-02T01:51:59.000Z","dependencies_parsed_at":"2023-01-25T22:45:46.118Z","dependency_job_id":"a653cf95-3152-42e2-bf9a-afb69bdaf7a2","html_url":"https://github.com/dmotz/trystero","commit_stats":{"total_commits":831,"total_committers":5,"mean_commits":166.2,"dds":"0.028880866425992746","last_synced_commit":"8eaaafb71cbb5272a6c597f0212cbfb4bed0110c"},"previous_names":[],"tags_count":51,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmotz%2Ftrystero","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmotz%2Ftrystero/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmotz%2Ftrystero/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmotz%2Ftrystero/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dmotz","download_url":"https://codeload.github.com/dmotz/trystero/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248230004,"owners_count":21069031,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bittorrent","chat","dapp","decentralized","firebase","ipfs","javascript","matchmaking","mqtt","nostr","p2p","peer-to-peer","realtime","serverless","signaling","signalling","supabase","web3","webrtc","webtorrent"],"created_at":"2024-08-02T17:00:48.606Z","updated_at":"2025-04-10T13:56:34.744Z","avatar_url":"https://github.com/dmotz.png","language":"JavaScript","funding_links":[],"categories":["JavaScript","webrtc","Libraries","Official links","TypeScript"],"sub_categories":["JavaScript"],"readme":"# ✨🤝✨ Trystero\n\n**Build instant multiplayer web apps, no server required**\n\n👉 **[TRY THE DEMO](https://oxism.com/trystero)** 👈\n\nTrystero manages a clandestine courier network that lets your application's\nusers talk directly with one another, encrypted and without a server middleman.\n\nThe net is full of open, decentralized communication channels: torrent trackers,\nIoT device brokers, boutique file protocols, and niche social networks.\n\nTrystero piggybacks on these networks to automatically establish secure,\nprivate, P2P connections between your app's users with no effort on your part.\n\nPeers can connect via\n[🌊 BitTorrent, 🐦 Nostr, 📡 MQTT, ⚡️ Supabase, 🔥 Firebase, or 🪐 IPFS](#strategy-comparison)\n– all using the same API.\n\nBesides making peer matching automatic, Trystero offers some nice abstractions\non top of WebRTC:\n\n- 👂📣 Rooms / broadcasting\n- 🔢📩 Automatic serialization / deserialization of data\n- 🎥🏷 Attach metadata to binary data and media streams\n- ✂️⏳ Automatic chunking and throttling of large data\n- ⏱🤞 Progress events and promises for data transfers\n- 🔐📝 Session data encryption\n- ⚛️🪝 React hooks\n\nYou can see what people are building with Trystero [here](https://github.com/jeremyckahn/awesome-trystero).\n\n---\n\n## Contents\n\n- [How it works](#how-it-works)\n- [Get started](#get-started)\n- [Listen for events](#listen-for-events)\n- [Broadcast events](#broadcast-events)\n- [Audio and video](#audio-and-video)\n- [Advanced](#advanced)\n  - [Binary metadata](#binary-metadata)\n  - [Action promises](#action-promises)\n  - [Progress updates](#progress-updates)\n  - [Encryption](#encryption)\n  - [React hooks](#react-hooks)\n  - [Connection issues](#connection-issues)\n  - [Supabase setup](#supabase-setup)\n  - [Firebase setup](#firebase-setup)\n- [API](#api)\n- [Strategy comparison](#strategy-comparison)\n  - [How to choose](#how-to-choose)\n\n---\n\n## How it works\n\n👉 **If you just want to try out Trystero, you can skip this explainer and\n[jump into using it](#get-started).**\n\nTo establish a direct peer-to-peer connection with WebRTC, a signalling channel\nis needed to exchange peer information\n([SDP](https://en.wikipedia.org/wiki/Session_Description_Protocol)). Typically\nthis involves running your own matchmaking server but Trystero abstracts this\naway for you and offers multiple \"serverless\" strategies for connecting peers\n(currently BitTorrent, Nostr, MQTT, Supabase, Firebase, and IPFS).\n\nThe important point to remember is this:\n\n\u003e 🔒\n\u003e\n\u003e Beyond peer discovery, your app's data never touches the strategy medium and\n\u003e is sent directly peer-to-peer and end-to-end encrypted between users.\n\u003e\n\u003e 👆\n\nYou can [compare strategies here](#strategy-comparison).\n\n## Get started\n\nYou can install with npm (`npm i trystero`) and import like so:\n\n```js\nimport {joinRoom} from 'trystero'\n```\n\nOr maybe you prefer a simple script tag? Download a pre-built JS file from the\n[latest release](https://github.com/dmotz/trystero/releases/latest) and import\nit locally:\n\n```html\n\u003cscript type=\"module\"\u003e\n  import {joinRoom} from './trystero-torrent.min.js'\n\u003c/script\u003e\n```\n\nBy default, the [Nostr strategy](#strategy-comparison) is used. To use a\ndifferent one just deep import like so (your bundler should handle including\nonly relevant code):\n\n```js\nimport {joinRoom} from 'trystero/mqtt' // (trystero-mqtt.min.js with a local file)\n// or\nimport {joinRoom} from 'trystero/torrent' // (trystero-torrent.min.js)\n// or\nimport {joinRoom} from 'trystero/supabase' // (trystero-supabase.min.js)\n// or\nimport {joinRoom} from 'trystero/firebase' // (trystero-firebase.min.js)\n// or\nimport {joinRoom} from 'trystero/ipfs' // (trystero-ipfs.min.js)\n```\n\nNext, join the user to a room with an ID:\n\n```js\nconst config = {appId: 'san_narciso_3d'}\nconst room = joinRoom(config, 'yoyodyne')\n```\n\nThe first argument is a configuration object that requires an `appId`. This\nshould be a completely unique identifier for your app¹. The second argument\nis the room ID.\n\n\u003e Why rooms? Browsers can only handle a limited amount of WebRTC connections at\n\u003e a time so it's recommended to design your app such that users are divided into\n\u003e groups (or rooms, or namespaces, or channels... whatever you'd like to call\n\u003e them).\n\n¹ When using Firebase, `appId` should be your `databaseURL` and when using\nSupabase, it should be your project URL.\n\n## Listen for events\n\nListen for peers joining the room:\n\n```js\nroom.onPeerJoin(peerId =\u003e console.log(`${peerId} joined`))\n```\n\nListen for peers leaving the room:\n\n```js\nroom.onPeerLeave(peerId =\u003e console.log(`${peerId} left`))\n```\n\nListen for peers sending their audio/video streams:\n\n```js\nroom.onPeerStream(\n  (stream, peerId) =\u003e (peerElements[peerId].video.srcObject = stream)\n)\n```\n\nTo unsubscribe from events, leave the room:\n\n```js\nroom.leave()\n```\n\nYou can access the local user's peer ID by importing `selfId` like so:\n\n```js\nimport {selfId} from 'trystero'\n\nconsole.log(`my peer ID is ${selfId}`)\n```\n\n## Broadcast events\n\nSend peers your video stream:\n\n```js\nroom.addStream(\n  await navigator.mediaDevices.getUserMedia({audio: true, video: true})\n)\n```\n\nSend and subscribe to custom P2P actions:\n\n```js\nconst [sendDrink, getDrink] = room.makeAction('drink')\n\n// buy drink for a friend\nsendDrink({drink: 'negroni', withIce: true}, friendId)\n\n// buy round for the house (second argument omitted)\nsendDrink({drink: 'mezcal', withIce: false})\n\n// listen for drinks sent to you\ngetDrink((data, peerId) =\u003e\n  console.log(\n    `got a ${data.drink} with${data.withIce ? '' : 'out'} ice from ${peerId}`\n  )\n)\n```\n\nYou can also use actions to send binary data, like images:\n\n```js\nconst [sendPic, getPic] = room.makeAction('pic')\n\n// blobs are automatically handled, as are any form of TypedArray\ncanvas.toBlob(blob =\u003e sendPic(blob))\n\n// binary data is received as raw ArrayBuffers so your handling code should\n// interpret it in a way that makes sense\ngetPic(\n  (data, peerId) =\u003e (imgs[peerId].src = URL.createObjectURL(new Blob([data])))\n)\n```\n\nLet's say we want users to be able to name themselves:\n\n```js\nconst idsToNames = {}\nconst [sendName, getName] = room.makeAction('name')\n\n// tell other peers currently in the room our name\nsendName('Oedipa')\n\n// tell newcomers\nroom.onPeerJoin(peerId =\u003e sendName('Oedipa', peerId))\n\n// listen for peers naming themselves\ngetName((name, peerId) =\u003e (idsToNames[peerId] = name))\n\nroom.onPeerLeave(peerId =\u003e\n  console.log(`${idsToNames[peerId] || 'a weird stranger'} left`)\n)\n```\n\n\u003e Actions are smart and handle serialization and chunking for you behind the\n\u003e scenes. This means you can send very large files and whatever data you send\n\u003e will be received on the other side as the same type (a number as a number,\n\u003e a string as a string, an object as an object, binary as binary, etc.).\n\n## Audio and video\n\nHere's a simple example of how you could create an audio chatroom:\n\n```js\n// this object can store audio instances for later\nconst peerAudios = {}\n\n// get a local audio stream from the microphone\nconst selfStream = await navigator.mediaDevices.getUserMedia({\n  audio: true,\n  video: false\n})\n\n// send stream to peers currently in the room\nroom.addStream(selfStream)\n\n// send stream to peers who join later\nroom.onPeerJoin(peerId =\u003e room.addStream(selfStream, peerId))\n\n// handle streams from other peers\nroom.onPeerStream((stream, peerId) =\u003e {\n  // create an audio instance and set the incoming stream\n  const audio = new Audio()\n  audio.srcObject = stream\n  audio.autoplay = true\n\n  // add the audio to peerAudio object if you want to address it for something\n  // later (volume, etc.)\n  peerAudios[peerId] = audio\n})\n```\n\nDoing the same with video is similar, just be sure to add incoming streams to\nvideo elements in the DOM:\n\n```js\nconst peerVideos = {}\nconst videoContainer = document.getElementById('videos')\n\nroom.onPeerStream((stream, peerId) =\u003e {\n  let video = peerVideos[peerId]\n\n  // if this peer hasn't sent a stream before, create a video element\n  if (!video) {\n    video = document.createElement('video')\n    video.autoplay = true\n\n    // add video element to the DOM\n    videoContainer.appendChild(video)\n  }\n\n  video.srcObject = stream\n  peerVideos[peerId] = video\n})\n```\n\n## Advanced\n\n### Binary metadata\n\nLet's say your app supports sending various types of files and you want to\nannotate the raw bytes being sent with metadata about how they should be\ninterpreted. Instead of manually adding metadata bytes to the buffer you can\nsimply pass a metadata argument in the sender action for your binary payload:\n\n```js\nconst [sendFile, getFile] = makeAction('file')\n\ngetFile((data, peerId, metadata) =\u003e\n  console.log(\n    `got a file (${metadata.name}) from ${peerId} with type ${metadata.type}`,\n    data\n  )\n)\n\n// to send metadata, pass a third argument\n// to broadcast to the whole room, set the second peer ID argument to null\nsendFile(buffer, null, {name: 'The Courierʼs Tragedy', type: 'application/pdf'})\n```\n\n### Action promises\n\nAction sender functions return a promise that resolves when they're done\nsending. You can optionally use this to indicate to the user when a large\ntransfer is done.\n\n```js\nawait sendFile(amplePayload)\nconsole.log('done sending to all peers')\n```\n\n### Progress updates\n\nAction sender functions also take an optional callback function that will be\ncontinuously called as the transmission progresses. This can be used for showing\na progress bar to the sender for large tranfers. The callback is called with a\npercentage value between 0 and 1 and the receiving peer's ID:\n\n```js\nsendFile(\n  payload,\n  // notice the peer target argument for any action sender can be a single peer\n  // ID, an array of IDs, or null (meaning send to all peers in the room)\n  [peerIdA, peerIdB, peerIdC],\n  // metadata, which can also be null if you're only interested in the\n  // progress handler\n  {filename: 'paranoids.flac'},\n  // assuming each peer has a loading bar added to the DOM, its value is\n  // updated here\n  (percent, peerId) =\u003e (loadingBars[peerId].value = percent)\n)\n```\n\nSimilarly you can listen for progress events as a receiver like this:\n\n```js\nconst [sendFile, getFile, onFileProgress] = room.makeAction('file')\n\nonFileProgress((percent, peerId, metadata) =\u003e\n  console.log(\n    `${percent * 100}% done receiving ${metadata.filename} from ${peerId}`\n  )\n)\n```\n\nNotice that any metadata is sent with progress events so you can show the\nreceiving user that there is a transfer in progress with perhaps the name of the\nincoming file.\n\nSince a peer can send multiple transmissions in parallel, you can also use\nmetadata to differentiate between them, e.g. by sending a unique ID.\n\n### Encryption\n\nOnce peers are connected to each other all of their communications are\nend-to-end encrypted. During the initial connection / discovery process, peers'\n[SDPs](https://en.wikipedia.org/wiki/Session_Description_Protocol) are sent via\nthe chosen peering strategy medium. By default the SDP is encrypted using a key\nderived from your app ID and room ID to prevent plaintext session data from\nappearing in logs. This is fine for most use cases, however a relay strategy\noperator can reverse engineer the key using the room and app IDs. A more secure\noption is to pass a `password` parameter in the app configuration object which\nwill be used to derive the encryption key:\n\n```js\njoinRoom({appId: 'kinneret', password: 'MuchoMaa$'}, 'w_a_s_t_e__v_i_p')\n```\n\nThis is a shared secret that must be known ahead of time and the password must\nmatch for all peers in the room for them to be able to connect. An example use\ncase might be a private chat room where users learn the password via external\nmeans.\n\n### React hooks\n\nTrystero functions are idempotent so they already work out of the box as React\nhooks.\n\nHere's a simple example component where each peer syncs their favorite\ncolor to everyone else:\n\n```jsx\nimport {joinRoom} from 'trystero'\nimport {useState} from 'react'\n\nconst trysteroConfig = {appId: 'thurn-und-taxis'}\n\nexport default function App({roomId}) {\n  const room = joinRoom(trysteroConfig, roomId)\n  const [sendColor, getColor] = room.makeAction('color')\n  const [myColor, setMyColor] = useState('#c0ffee')\n  const [peerColors, setPeerColors] = useState({})\n\n  // whenever new peers join the room, send my color to them:\n  room.onPeerJoin(peer =\u003e sendColor(myColor, peer))\n\n  // listen for peers sending their colors and update the state accordingly:\n  getColor((color, peer) =\u003e\n    setPeerColors(peerColors =\u003e ({...peerColors, [peer]: color}))\n  )\n\n  const updateColor = e =\u003e {\n    const {value} = e.target\n\n    // when updating my own color, broadcast it to all peers:\n    sendColor(value)\n    setMyColor(value)\n  }\n\n  return (\n    \u003c\u003e\n      \u003ch1\u003eTrystero + React\u003c/h1\u003e\n\n      \u003ch2\u003eMy color:\u003c/h2\u003e\n      \u003cinput type=\"color\" value={myColor} onChange={updateColor} /\u003e\n\n      \u003ch2\u003ePeer colors:\u003c/h2\u003e\n      \u003cul\u003e\n        {Object.entries(peerColors).map(([peerId, color]) =\u003e (\n          \u003cli key={peerId} style={{backgroundColor: color}}\u003e\n            {peerId}: {color}\n          \u003c/li\u003e\n        ))}\n      \u003c/ul\u003e\n    \u003c/\u003e\n  )\n}\n```\n\nAstute readers may notice the above example is simple and doesn't consider if we\nwant to change the component's room ID or unmount it. For those scenarios you\ncan use this simple `useRoom()` hook that unsubscribes from room events\naccordingly:\n\n```js\nimport {joinRoom} from 'trystero'\nimport {useEffect, useRef} from 'react'\n\nexport const useRoom = (roomConfig, roomId) =\u003e {\n  const roomRef = useRef(joinRoom(roomConfig, roomId))\n  const lastRoomIdRef = useRef(roomId)\n\n  useEffect(() =\u003e {\n    if (roomId !== lastRoomIdRef.current) {\n      roomRef.current.leave()\n      roomRef.current = joinRoom(roomConfig, roomId)\n      lastRoomIdRef.current = roomId\n    }\n\n    return () =\u003e roomRef.current.leave()\n  }, [roomConfig, roomId])\n\n  return roomRef.current\n}\n```\n\n### Connection issues\n\nWebRTC is powerful but some networks simply don't allow direct P2P connections\nusing it. If you find that certain user pairings aren't working in Trystero,\nyou're likely encountering an issue at the network provider level. To solve this\nyou can configure a TURN server which will act as a proxy layer for peers\nthat aren't able to connect directly to one another.\n\n1. If you can, confirm that the issue is specific to particular network\n   conditions (e.g. user with ISP A cannot connect to a user with ISP B). If\n   other user pairings are working (like those between two browsers on the same\n   machine), this likely confirms that Trystero is working correctly.\n2. Sign up for a TURN service or host your own. There are various hosted TURN\n   services you can find online (like\n   [Open Relay](https://www.metered.ca/stun-turn) or\n   [Cloudflare](https://developers.cloudflare.com/calls/turn/)), some with free\n   tiers. You can also host an open source TURN server like\n   [coturn](https://github.com/coturn/coturn),\n   [Pion TURN](https://github.com/pion/turn),\n   [Violet](https://github.com/paullouisageneau/violet), or\n   [eturnal](https://github.com/processone/eturnal).\n3. Once you have a TURN server, configure Trystero with it like this:\n   ```js\n   const room = joinRoom(\n     {\n       // ...your app config\n       turnConfig: [\n         {\n           // single string or list of strings of urls to access TURN server\n           urls: ['turn:your-turn-server.ok:1979'],\n           username: 'username',\n           credential: 'password'\n         }\n       ]\n     },\n     'roomId'\n   )\n   ```\n\n### Supabase setup\n\nTo use the Supabase strategy:\n\n1. Create a [Supabase](https://supabase.com) project or use an existing one\n2. On the dashboard, go to Project Settings -\u003e API\n3. Copy the Project URL and set that as the `appId` in the Trystero config,\n   copy the `anon public` API key and set it as `supabaseKey` in the Trystero\n   config\n\n### Firebase setup\n\nIf you want to use the Firebase strategy and don't have an existing project:\n\n1. Create a [Firebase](https://firebase.google.com/) project\n2. Create a new Realtime Database\n3. Copy the `databaseURL` and use it as the `appId` in your Trystero config\n\n\u003cdetails\u003e\n  \u003csummary\u003e\n  [*Optional*] Configure the database with security rules to limit activity:\n  \u003c/summary\u003e\n\n```json\n{\n  \"rules\": {\n    \".read\": false,\n    \".write\": false,\n    \"__trystero__\": {\n      \".read\": false,\n      \".write\": false,\n      \"$room_id\": {\n        \".read\": true,\n        \".write\": true\n      }\n    }\n  }\n}\n```\n\nThese rules ensure room peer presence is only readable if the room namespace is\nknown ahead of time.\n\n\u003c/details\u003e\n\n## API\n\n### `joinRoom(config, roomId, [onError])`\n\nAdds local user to room whereby other peers in the same namespace will open\ncommunication channels and send events. Calling `joinRoom()` multiple times with\nthe same namespace will return the same room instance.\n\n- `config` - Configuration object containing the following keys:\n\n  - `appId` - **(required)** A unique string identifying your app. When using\n    Supabase, this should be set to your project URL (see\n    [Supabase setup instructions](#supabase-setup)). If using\n    Firebase, this should be the `databaseURL` from your Firebase config (also\n    see `firebaseApp` below for an alternative way of configuring the Firebase\n    strategy).\n\n  - `password` - **(optional)** A string to encrypt session descriptions via\n    AES-GCM as they are passed through the peering medium. If not set, session\n    descriptions will be encrypted with a key derived from the app ID and room\n    name. A custom password must match between any peers in the room for them to\n    connect. See [encryption](#encryption) for more details.\n\n  - `relayUrls` - **(optional, 🌊 BitTorrent, 🐦 Nostr, 📡 MQTT only)** Custom\n    list of URLs for the strategy to use to bootstrap P2P connections. These\n    would be BitTorrent trackers, Nostr relays, and MQTT brokers, respectively.\n    They must support secure WebSocket connections.\n\n  - `relayRedundancy` - **(optional, 🌊 BitTorrent, 🐦 Nostr, 📡 MQTT only)**\n    Integer specifying how many torrent trackers to connect to simultaneously in\n    case some fail. Passing a `relayUrls` option will cause this option to be\n    ignored as the entire list will be used.\n\n  - `rtcConfig` - **(optional)** Specifies a custom\n    [`RTCConfiguration`](https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration)\n    for all peer connections.\n\n  - `turnConfig` - **(optional)** Specifies a custom list of TURN servers to use\n    (see [Connection issues](#connection-issues) section). Each item in the list\n    should correspond to an\n    [ICE server config object](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection#iceservers).\n    When passing a TURN config like this, Trystero's default STUN servers will\n    also be used. To override this and use both custom STUN and TURN servers,\n    instead pass the config via the above `rtcConfig.iceServers` option as a\n    list of both STUN/TURN servers — this won't inherit Trystero's defaults.\n\n  - `rtcPolyfill` - **(optional)** Use this to pass a custom\n    [`RTCPeerConnection`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection)-compatible\n    constructor. This is useful for running outside of a browser, such as in\n    Node (still experimental).\n\n  - `supabaseKey` - **(required, ⚡️ Supabase only)** Your Supabase project's\n    `anon public` API key.\n\n  - `firebaseApp` - **(optional, 🔥 Firebase only)** You can pass an already\n    initialized Firebase app instance instead of an `appId`. Normally Trystero\n    will initialize a Firebase app based on the `appId` but this will fail if\n    youʼve already initialized it for use elsewhere.\n\n  - `rootPath` - **(optional, 🔥 Firebase only)** String specifying path where\n    Trystero writes its matchmaking data in your database (`'__trystero__'` by\n    default). Changing this is useful if you want to run multiple apps using the\n    same database and don't want to worry about namespace collisions.\n\n  - `libp2pConfig` - **(optional, 🪐 IPFS only)**\n    [`Libp2pOptions`](https://libp2p.github.io/js-libp2p/types/libp2p.index.Libp2pOptions.html)\n    where you can specify a list of static peers for bootstrapping.\n\n- `roomId` - A string to namespace peers and events within a room.\n\n- `onError(details)` - **(optional)** A callback function that will be called if\n  the room cannot be joined due to an incorrect password. `details` is an\n  object containing `appId`, `roomId`, `peerId`, and `error` describing the\n  error.\n\nReturns an object with the following methods:\n\n- ### `leave()`\n\n  Remove local user from room and unsubscribe from room events.\n\n- ### `getPeers()`\n\n  Returns a map of\n  [`RTCPeerConnection`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection)s\n  for the peers present in room (not including the local user). The keys of\n  this object are the respective peers' IDs.\n\n- ### `addStream(stream, [targetPeers], [metadata])`\n\n  Broadcasts media stream to other peers.\n\n  - `stream` - A `MediaStream` with audio and/or video to send to peers in the\n    room.\n\n  - `targetPeers` - **(optional)** If specified, the stream is sent only to the\n    target peer ID (string) or list of peer IDs (array).\n\n  - `metadata` - **(optional)** Additional metadata (any serializable type) to\n    be sent with the stream. This is useful when sending multiple streams so\n    recipients know which is which (e.g. a webcam versus a screen capture). If\n    you want to broadcast a stream to all peers in the room with a metadata\n    argument, pass `null` as the second argument.\n\n- ### `removeStream(stream, [targetPeers])`\n\n  Stops sending previously sent media stream to other peers.\n\n  - `stream` - A previously sent `MediaStream` to stop sending.\n\n  - `targetPeers` - **(optional)** If specified, the stream is removed only from\n    the target peer ID (string) or list of peer IDs (array).\n\n- ### `addTrack(track, stream, [targetPeers], [metadata])`\n\n  Adds a new media track to a stream.\n\n  - `track` - A `MediaStreamTrack` to add to an existing stream.\n\n  - `stream` - The target `MediaStream` to attach the new track to.\n\n  - `targetPeers` - **(optional)** If specified, the track is sent only to the\n    target peer ID (string) or list of peer IDs (array).\n\n  - `metadata` - **(optional)** Additional metadata (any serializable type) to\n    be sent with the track. See `metadata` notes for `addStream()` above for\n    more details.\n\n- ### `removeTrack(track, stream, [targetPeers])`\n\n  Removes a media track from a stream.\n\n  - `track` - The `MediaStreamTrack` to remove.\n\n  - `stream` - The `MediaStream` the track is attached to.\n\n  - `targetPeers` - **(optional)** If specified, the track is removed only from\n    the target peer ID (string) or list of peer IDs (array).\n\n- ### `replaceTrack(oldTrack, newTrack, stream, [targetPeers])`\n\n  Replaces a media track with a new one.\n\n  - `oldTrack` - The `MediaStreamTrack` to remove.\n\n  - `newTrack` - A `MediaStreamTrack` to attach.\n\n  - `stream` - The `MediaStream` the `oldTrack` is attached to.\n\n  - `targetPeers` - **(optional)** If specified, the track is replaced only for\n    the target peer ID (string) or list of peer IDs (array).\n\n- ### `onPeerJoin(callback)`\n\n  Registers a callback function that will be called when a peer joins the room.\n  If called more than once, only the latest callback registered is ever called.\n\n  - `callback(peerId)` - Function to run whenever a peer joins, called with the\n    peer's ID.\n\n  Example:\n\n  ```js\n  onPeerJoin(peerId =\u003e console.log(`${peerId} joined`))\n  ```\n\n- ### `onPeerLeave(callback)`\n\n  Registers a callback function that will be called when a peer leaves the room.\n  If called more than once, only the latest callback registered is ever called.\n\n  - `callback(peerId)` - Function to run whenever a peer leaves, called with the\n    peer's ID.\n\n  Example:\n\n  ```js\n  onPeerLeave(peerId =\u003e console.log(`${peerId} left`))\n  ```\n\n- ### `onPeerStream(callback)`\n\n  Registers a callback function that will be called when a peer sends a media\n  stream. If called more than once, only the latest callback registered is ever\n  called.\n\n  - `callback(stream, peerId, metadata)` - Function to run whenever a peer sends\n    a media stream, called with the the peer's stream, ID, and optional metadata\n    (see `addStream()` above for details).\n\n  Example:\n\n  ```js\n  onPeerStream((stream, peerId) =\u003e\n    console.log(`got stream from ${peerId}`, stream)\n  )\n  ```\n\n- ### `onPeerTrack(callback)`\n\n  Registers a callback function that will be called when a peer sends a media\n  track. If called more than once, only the latest callback registered is ever\n  called.\n\n  - `callback(track, stream, peerId, metadata)` - Function to run whenever a\n    peer sends a media track, called with the the peer's track, attached stream,\n    ID, and optional metadata (see `addTrack()` above for details).\n\n  Example:\n\n  ```js\n  onPeerTrack((track, stream, peerId) =\u003e\n    console.log(`got track from ${peerId}`, track)\n  )\n  ```\n\n- ### `makeAction(actionId)`\n\n  Listen for and send custom data actions.\n\n  - `actionId` - A string to register this action consistently among all peers.\n\n  Returns an array of three functions:\n\n  1. #### Sender\n\n     - Sends data to peers and returns a promise that resolves when all\n       target peers are finished receiving data.\n\n     - `(data, [targetPeers], [metadata], [onProgress])`\n\n       - `data` - Any value to send (primitive, object, binary). Serialization\n         and chunking is handled automatically. Binary data (e.g. `Blob`,\n         `TypedArray`) is received by other peer as an agnostic `ArrayBuffer`.\n\n       - `targetPeers` - **(optional)** Either a peer ID (string), an array of\n         peer IDs, or `null` (indicating to send to all peers in the room).\n\n       - `metadata` - **(optional)** If the data is binary, you can send an\n         optional metadata object describing it (see\n         [Binary metadata](#binary-metadata)).\n\n       - `onProgress` - **(optional)** A callback function that will be called\n         as every chunk for every peer is transmitted. The function will be\n         called with a value between 0 and 1 and a peer ID. See\n         [Progress updates](#progress-updates) for an example.\n\n  2. #### Receiver\n\n     - Registers a callback function that runs when data for this action is\n       received from other peers.\n\n     - `(data, peerId, metadata)`\n\n       - `data` - The value transmitted by the sending peer. Deserialization is\n         handled automatically, i.e. a number will be received as a number, an\n         object as an object, etc.\n\n       - `peerId` - The ID string of the sending peer.\n\n       - `metadata` - **(optional)** Optional metadata object supplied by the\n         sender if `data` is binary, e.g. a filename.\n\n  3. #### Progress handler\n\n     - Registers a callback function that runs when partial data is received\n       from peers. You can use this for tracking large binary transfers. See\n       [Progress updates](#progress-updates) for an example.\n\n     - `(percent, peerId, metadata)`\n\n       - `percent` - A number between 0 and 1 indicating the percentage complete\n         of the transfer.\n\n       - `peerId` - The ID string of the sending peer.\n\n       - `metadata` - **(optional)** Optional metadata object supplied by the\n         sender.\n\n  Example:\n\n  ```js\n  const [sendCursor, getCursor] = room.makeAction('cursormove')\n\n  window.addEventListener('mousemove', e =\u003e sendCursor([e.clientX, e.clientY]))\n\n  getCursor(([x, y], peerId) =\u003e {\n    const peerCursor = cursorMap[peerId]\n    peerCursor.style.left = x + 'px'\n    peerCursor.style.top = y + 'px'\n  })\n  ```\n\n- ### `ping(peerId)`\n\n  Takes a peer ID and returns a promise that resolves to the milliseconds the\n  round-trip to that peer took. Use this for measuring latency.\n\n  - `peerId` - Peer ID string of the target peer.\n\n  Example:\n\n  ```js\n  // log round-trip time every 2 seconds\n  room.onPeerJoin(peerId =\u003e\n    setInterval(\n      async () =\u003e console.log(`took ${await room.ping(peerId)}ms`),\n      2000\n    )\n  )\n  ```\n\n### `selfId`\n\nA unique ID string other peers will know the local user as globally across\nrooms.\n\n### `getRelaySockets()`\n\n**(🌊 BitTorrent, 🐦 Nostr, 📡 MQTT only)** Returns an object of relay URL keys\nmapped to their WebSocket connections. This can be useful for determining the\nstate of the user's connection to the relays and handling any connection\nfailures.\n\nExample:\n\n```js\nconsole.log(trystero.getRelaySockets())\n// =\u003e Object {\n//  \"wss://tracker.webtorrent.dev\": WebSocket,\n//  \"wss://tracker.openwebtorrent.com\": WebSocket\n//  }\n```\n\n### `getOccupants(config, roomId)`\n\n**(🔥 Firebase only)** Returns a promise that resolves to a list of user IDs\npresent in the given namespace. This is useful for checking how many users are\nin a room without joining it.\n\n- `config` - A configuration object\n- `roomId` - A namespace string that you'd pass to `joinRoom()`.\n\nExample:\n\n```js\nconsole.log((await trystero.getOccupants(config, 'the_scope')).length)\n// =\u003e 3\n```\n\n## Strategy comparison\n\n|                   | one-time setup¹ | bundle size² |\n| ----------------- | --------------- | ------------ |\n| 🐦 **Nostr**      | none 🏆         | 16K          |\n| 📡 **MQTT**       | none 🏆         | 75K          |\n| 🌊 **BitTorrent** | none 🏆         | 5K 🏆        |\n| ⚡️ **Supabase**  | ~5 mins         | 27K          |\n| 🔥 **Firebase**   | ~5 mins         | 43K          |\n| 🪐 **IPFS**       | none 🏆         | 143K         |\n\n**¹** All strategies except Supabase and Firebase require zero setup. Supabase\nand Firebase are managed strategies which require setting up an account.\n\n**²** Calculated via Terser minification + Brotli compression.\n\n### How to choose\n\nTrysteroʼs unique advantage is that it requires zero backend setup and uses\ndecentralized infrastructure in most cases. This allows for frictionless\nexperimentation and no single point of failure. One potential drawback is that\nitʼs difficult to guarantee that the public infrastructure it uses will always\nbe highly available, even with the redundancy techniques Trystero uses. While\nthe other strategies are decentralized, the Supabase and Firebase strategies are\na more managed approach with greater control and an SLA, which might be more\nappropriate for “production” apps.\n\nTrystero makes it trivial to switch between strategies — just change a single\nimport line and quickly experiment:\n\n```js\nimport {joinRoom} from 'trystero/[torrent|nostr|mqtt|supabase|firebase|ipfs]'\n```\n\n---\n\nTrystero by [Dan Motzenbecker](https://oxism.com)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmotz%2Ftrystero","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdmotz%2Ftrystero","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmotz%2Ftrystero/lists"}