Getting started
Configuration
One options object controls everything: where the client connects, which signaling transport it uses, how it authenticates, and how aggressively it falls back when networks misbehave.
Passing options
The same options object is accepted by new Dendri(options),
new Dendri(id, options), and createDendriStore(options). The simplest
form is a single url:
import { Dendri, createDendriStore } from "@afterrealism/dendri-client";
// Store (recommended for UI apps)
const store = createDendriStore({ url: "wss://signal.example.com" });
// Low-level peer, with an explicit peer ID
const peer = new Dendri("alice-laptop", { url: "wss://signal.example.com" });
url is shorthand for host / port / secure /
path. If you pass any of those explicitly, the explicit value wins over the
url-derived one.
Option reference
| Option | Type | Default | Description |
|---|---|---|---|
url | string | — | Full server URL, e.g. wss://signal.example.com or http://127.0.0.1:9876. Shorthand for host/port/secure/path. |
host | string | — | Server hostname, e.g. signal.example.com. |
port | number | — | Server port, e.g. 443 or 9876. |
path | string | "/" | URL path prefix; must match the server's --path. |
secure | boolean | — | true for wss/https, false for ws/http. |
apiKey | string | — | API key for hosted / multi-tenant deployments. Sent with every signaling and REST request. |
key | string | "dendri" | Connection key namespace; must match the server's --key. |
jwt | string | — | JWT for authenticated connections (server started with --jwt_secret). |
token | string | random | Session token used to authenticate reconnects for the same peer ID. |
signalingTransport | "websocket" | "sse" | "polling" | "auto" | "websocket" | Signaling transport. "auto" tries WebSocket → SSE → long-poll. |
fetchTurnCredentials | boolean | false | Auto-fetch ephemeral TURN credentials from the server's /turn-credentials endpoint. |
enableRelay | boolean | false | Enable the tier-4 encrypted WebSocket relay when WebRTC is unavailable. |
config | RTCConfiguration | — | Custom ICE configuration passed to every RTCPeerConnection (your own STUN/TURN servers). |
validateMetadata | (meta: unknown) => boolean | — | Validate incoming connection metadata before accepting a connection. |
referrerPolicy | ReferrerPolicy | — | Referrer policy for the SDK's HTTP requests. |
debug | number | 0 | Log verbosity, 0 (silent) to 3 (verbose). |
Configuring from environment variables
Apps should never hardcode a server URL. The pattern used by every official example reads a single env var and derives the parts, so the same build runs against local, staging, and production servers:
// .env.local → VITE_DENDRI_URL=https://signal.example.com
// VITE_DENDRI_API_KEY=dk_your_key (hosted tier only)
const serverUrl = new URL(import.meta.env.VITE_DENDRI_URL ?? "http://127.0.0.1:9876");
export const DENDRI_OPTIONS = {
host: serverUrl.hostname,
port: Number(serverUrl.port) || (serverUrl.protocol === "https:" ? 443 : 80),
secure: serverUrl.protocol === "https:",
path: serverUrl.pathname,
apiKey: import.meta.env.VITE_DENDRI_API_KEY,
fetchTurnCredentials: true,
enableRelay: true,
debug: 0,
};
Signaling transports
Signaling (the handshake channel) is separate from the data path. Dendri supports three
transports against the same server, selected with signalingTransport:
| Value | Mechanism | Use when |
|---|---|---|
"websocket" | Persistent WebSocket (default) | Normal networks. Lowest latency. |
"sse" | Server-Sent Events + HTTP POST | Proxies that terminate WebSockets. |
"polling" | HTTP long-poll | The most restrictive corporate networks. |
"auto" | WS → SSE → polling | You don't control your users' networks. Recommended for public apps. |
Connectivity & fallback
TURN credentials
For peers behind symmetric NAT, WebRTC needs a TURN relay. If your server is configured with
--turn_secret, set fetchTurnCredentials: true and the client
automatically fetches short-lived credentials before connecting. Alternatively, pass your own
ICE servers:
const store = createDendriStore({
url: "wss://signal.example.com",
config: {
iceServers: [
{ urls: "stun:stun.example.com:3478" },
{
urls: "turn:turn.example.com:3478",
username: "user",
credential: "pass",
},
],
},
});
Encrypted TLS relay
With enableRelay: true, room broadcasts also flow through the signaling server as a
last-resort delivery path (tier 4), necessary on networks where ICE reports success but packets
silently vanish (VPNs, UDP-blocked networks). Relayed payloads are end-to-end encrypted with
ECDH-derived AES-256-GCM keys, so the server relays ciphertext it cannot read. Duplicate
deliveries are de-duplicated on the receiving side.
Connection lifecycle
The client runs a connection state machine, exposed as peer.connectionState /
store.connectionState and via the connectionStateChanged event:
| State | Meaning |
|---|---|
initialized | Created, not yet connected. |
connecting | Attempting to reach the signaling server. |
connected | Connected and ready. |
disconnected | Temporarily offline; auto-reconnect with exponential backoff and jitter. |
suspended | Extended offline (5+ consecutive failures); slower retry schedule. |
closed | Explicitly closed by your code. |
failed | Unrecoverable error. |
After a successful reconnect the SDK re-sends room membership and presence automatically, so a server restart or a laptop lid-close doesn't leave peers in a ghost state.
Room and store options
createDendriStore() also accepts an advanced shape when you need room-level
settings or want to inject a custom client class in tests:
const store = createDendriStore({
dendriOptions: { url: "wss://signal.example.com", enableRelay: true },
roomOptions: {
// How long to wait for a new host to become reachable
// during host migration (default 10 000 ms).
migrationTimeout: 5000,
},
});
Multi-tab behaviour
When two tabs use the same peer_id against the same server:
- Same peer ID, same token: treated as a reconnection. The server adopts the new socket; room memberships and queued messages survive.
- Same peer ID, different token: rejected with
ID_TAKEN, which prevents accidental or malicious ID hijacking.
For multi-tab apps, either coordinate a single connection with
BroadcastChannel, or let each tab be its own peer and communicate through a room.
Debugging
Set debug: 3 for verbose logs of signaling, ICE negotiation, and transport
fallbacks. peer.diagnostics() returns a snapshot of connection internals suitable
for bug reports.