Self-hosting
Signaling servers
Run your own PeerJS server so rooms don't depend on the public one, and point rooms at it with ?signal= or a server list.
A signaling server only introduces a room’s devices to each other. By default rooms use the public PeerJS cloud (0.peerjs.com), which is free and shared, so it can be slow, down or blocked. Run your own for reliable rooms, and to keep room codes and IP addresses off a third-party server.
Shortest path: run npx peer --port 9000 --path / behind TLS, then open rooms with ?signal=signal.example.com.
What signaling does
Section titled “What signaling does”The host registers the room code; a joiner asks for it, and the two swap WebRTC offers through the server. After that the links are direct (or through a TURN relay). Prompts, answers and the model never touch it. A running room keeps going if signaling drops, but new devices can’t join and a reloading host can’t reclaim its room until it’s back.
Run a PeerServer
Section titled “Run a PeerServer”Any PeerJS server 1.x works. The repo already has one as a dev dependency (peer).
npx peer --port 9000 --path /# listening on ws://<this machine>:9000docker run -p 9000:9000 -d peerjs/peerjs-serverThe image listens on port 9000 at path /. It is published for linux/amd64 only, so on an ARM machine use npx.
Put it behind TLS
Section titled “Put it behind TLS”An https page can only open wss://, so the server needs TLS. Give it a certificate:
npx peer --port 9000 --path / --sslkey key.pem --sslcert cert.pemOr run it behind a reverse proxy and pass --proxied. With Caddy:
signal.example.com { reverse_proxy 127.0.0.1:9000}npx peer --port 9000 --path / --proxiedCheck it
Section titled “Check it”curl https://signal.example.com/peerjs/idIt should print a fresh id (a UUID). With a non-default --key, the path is /<key>/id.
Point rooms at your server
Section titled “Point rooms at your server”Add ?signal= to the room URL. The host and every joiner must use the same server, because a room registered on one server doesn’t exist on another. The host’s invite link and QR code carry the parameter for you.
https://pooled.run/room?signal=signal.example.com wss, port 443, path /https://pooled.run/room?signal=wss://signal.example.com:8443/pooledhttps://pooled.run/room?signal=signal.example.com,cloud yours first, then the cloudhttp://127.0.0.1:8080/room?signal=127.0.0.1:9000 local dev: ws, since the page is http?signal= replaces every other setting: only the servers it names are tried, in order.
If you deploy the site yourself, define an ordered list before room.js loads, for example in p2p.html:
<script>window.POOLED_SIGNAL_SERVERS = ["wss://signal.example.com", "cloud"];</script>Entries can also be objects: { host, port, path, secure, key }. Use key if your server runs with a non-default --key. Without the list, rooms use ["cloud"].
Accepted forms
Section titled “Accepted forms”| Form | Example | Meaning |
|---|---|---|
cloud |
cloud |
The public PeerJS server, 0.peerjs.com |
host |
signal.example.com |
wss when the page is https (ws when http), port 443, path / |
host:port |
127.0.0.1:9000 |
Same, on that port |
host:port/path |
signal.example.com:443/pooled |
Same, under that path |
| URL | wss://signal.example.com:8443/pooled |
wss://, ws://, https:// or http:// |
Without a port, a bare host uses 443; a URL uses 443 for wss:///https:// and 80 for ws:///http://.
How the fallback works
Section titled “How the fallback works”You can list several servers (room/signal.js):
- The host uses the first server that registers the room code. A server counts as down when it refuses, drops, fails TLS or doesn’t answer within 8 s (20 s for the last one). “That code is already hosting a room” is an answer, not an outage, so it never falls back.
- A joiner walks the same list, and tries the next server when one has no such room. An invite link goes straight to the host’s server: it carries
?signal=(anddev=0) whenever the host isn’t on the first server. - A host that reloads tries its room’s server first, because its guests are there.
- All servers down: the join screen says so, running rooms are not affected, and you can retry.
- Drops during a room: every device shows a note under the header and re-registers with backoff (2, 4, 8, 16 s, then every 30 s). Running rooms keep working; new devices can’t join until it’s back.
Signaling is not part of the room protocol, so devices on different Pooled versions still meet on the same server.
Test it
Section titled “Test it”No GPU needed:
deno test --allow-read tests/unit/signal_test.js # spec parsing, list order, fallback rulesnode tests/e2e/signal_fallback.mjs # the fallback end to end in headless Chromium