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

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 optionDendri equivalentNotes
host, port, path, secureurl (or the same four options)One URL replaces four fields; the explicit fields still work.
keykeyDefaults to "dendri"; must match the server's --key.
configconfigSame RTCConfiguration shape for custom ICE servers.
debugdebugSame 0 to 3 scale.
(none)fetchTurnCredentialsAsk the server to mint short-lived TURN credentials.
(none)enableRelayOpt in to the tier-4 encrypted relay fallback.
(none)apiKey, jwtHosted-tier key or JWT auth for locked-down servers.
There is no default cloud. PeerJS silently connects to its community server when you omit options. Dendri never does: you always pass your own server URL (or your hosted endpoint). If the URL is missing you get a clear error, not a surprise dependency.

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

EventPeerJSDendri
open, connection, call, close, disconnected, errorYesYes, same payloads
reconnectedNoSignaling re-established; room state re-sent automatically.
connectionStateChangedNoObserve the full connection state machine.
roomPeers, presenceUpdateNoRoom 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
Migrating with an AI agent? Point it at the Dendri MCP server and ask it to port your PeerJS code; the dendri_get_example tool gives it verified patterns to copy.

Hit a migration snag? Open an issue on GitHub.