Guides
Migrating from PeerJS
Dendri's low-level API is deliberately PeerJS-shaped: connect(),
call(), the same event names. Most apps migrate in under an hour, and gain
reconnection that keeps your ID, transport fallback, rooms, presence, and a maintained server.
Why teams migrate
- Reconnection that works. PeerJS's long-standing "ID is taken" issue on reconnect does not exist here:
reconnect()re-establishes signaling with your previous ID. - A connection that survives hostile networks. PeerJS gives up when WebRTC is blocked. Dendri falls back through STUN, TURN, and an end-to-end encrypted TLS relay; signaling itself falls back WebSocket → SSE → long-poll.
- Rooms, presence, RPC, and ACK delivery as first-class APIs instead of hand-rolled connection bookkeeping.
- A maintained, self-hostable server. The Rust signaling server is a single binary you run yourself (or use the hosted tier). No best-effort community cloud.
Constructor and options
// PeerJS
import Peer from "peerjs";
const peer = new Peer("alice", {
host: "peer.example.com", port: 443, path: "/", secure: true,
key: "peerjs", debug: 2,
});
// Dendri
import Dendri from "@afterrealism/dendri-client";
const peer = new Dendri("alice", {
url: "wss://signal.example.com", // replaces host/port/path/secure
key: "dendri",
debug: 2,
});
| PeerJS option | Dendri equivalent | Notes |
|---|---|---|
host, port, path, secure | url (or the same four options) | One URL replaces four fields; the explicit fields still work. |
key | key | Defaults to "dendri"; must match the server's --key. |
config | config | Same RTCConfiguration shape for custom ICE servers. |
debug | debug | Same 0 to 3 scale. |
| (none) | fetchTurnCredentials | Ask the server to mint short-lived TURN credentials. |
| (none) | enableRelay | Opt in to the tier-4 encrypted relay fallback. |
| (none) | apiKey, jwt | Hosted-tier key or JWT auth for locked-down servers. |
Data connections
The shape you already know. connect() returns a connection that emits open, data, close, and error:
const conn = peer.connect("bob", { reliable: true });
conn.on("open", () => conn.send({ hello: "world" }));
conn.on("data", (data) => console.log(data));
peer.on("connection", (conn) => {
conn.on("data", (data) => console.log("got", data));
});
Upgrades you can adopt incrementally: peer.connectHybrid() adds automatic
WebRTC ⇄ relay fallback with upgrade retries, and MsgPackDendri is a drop-in
subclass that serializes with MsgPack for object-heavy traffic.
Media calls
// caller
const call = peer.call("bob", localStream);
call.on("stream", (remote) => (video.srcObject = remote));
// callee
peer.on("call", (call) => {
call.answer(localStream);
call.on("stream", (remote) => (video.srcObject = remote));
});
Peer events
| Event | PeerJS | Dendri |
|---|---|---|
open, connection, call, close, disconnected, error | Yes | Yes, same payloads |
reconnected | No | Signaling re-established; room state re-sent automatically. |
connectionStateChanged | No | Observe the full connection state machine. |
roomPeers, presenceUpdate | No | Room membership and presence, no manual bookkeeping. |
The server
Replace peerjs-server with the Dendri signaling server, a single Rust binary with Redis state:
# before: npx peerjs --port 9000
# after (Redis + server)
podman run -d --name dendri-redis -p 6379:6379 docker.io/redis:7
podman run -d --name dendri --network host \
ghcr.io/afterrealism/dendri-server:latest \
--host 127.0.0.1 --port 9876 \
--redis_url redis://127.0.0.1:6379 \
--enable_relay --allow_discovery
Full flags, TLS, and TURN are covered in Self-hosting.
Beyond parity: what to adopt next
Once the port compiles, the payoff is the layer PeerJS never had. Replace hand-rolled connection fan-out with a Room, or skip the class entirely and use createDendriStore():
import { createDendriStore } from "@afterrealism/dendri-client";
const store = createDendriStore({ url: "wss://signal.example.com" });
store.join("my-room"); // discovery, host election, reconnects
store.broadcast({ cursor: { x, y } }); // fan-out to every peer
store.subscribe("chat", (msg, from) => …); // topics
store.setPresence({ name: "Alice" }); // typed presence
dendri_get_example tool gives it verified patterns to copy.
Hit a migration snag? Open an issue on GitHub.