browser A
createDendriStore(…)
Self-hosted by default
Dendri pairs a framework-neutral TypeScript SDK with a Rust signaling server so you can ship chat, presence, multiplayer state, Yjs documents, and relay-backed WebRTC without renting somebody else’s realtime cloud.
// Install the framework-neutral client
// $ npm i @afterrealism/dendri-client
import { createDendriStore } from "@afterrealism/dendri-client";
const store = createDendriStore({
host: "127.0.0.1",
port: 9876,
secure: false,
path: "/",
});
store.join("project-room");
store.setPresence({ name: "Ari" });
store.broadcast({ type: "chat", text: "hello team" });
Why this exists
Every collaboration product eventually rents the same five things from the same five vendors: a signaling endpoint, a presence service, a CRDT relay, a TURN pool, and a webhook fanout. Each one is a separate bill, a separate dashboard, and a separate point of failure that lives outside your repo and outside your incident response.
Dendri is the opposite shape. It is a small TypeScript SDK and a single Rust binary you deploy next to the rest of your stack. It speaks WebRTC where it can, falls back to a relay it controls, and exposes the same room snapshot to React, Vue, Svelte, and vanilla JS without a framework adapter on the critical path.
The point is not that hosted realtime is bad. The point is that you should be able to choose — and that the moment you outgrow the pricing tier or the compliance posture, the migration path should be a config change, not a rewrite.
What you get
Dendri ships the building blocks you would otherwise stitch together from signaling vendors, presence services, CRDT relays, and TURN providers.
Six load‑bearing ideas
Most of the project is a direct consequence of these six. If a tradeoff conflicts with one of them, the tradeoff loses.
Every code sample in the docs points at a signaling host you operate. The SDK has no hidden hosted endpoint, no telemetry beacon, no “works without credentials” mode that quietly billed somebody. If your laptop is offline, the only thing that breaks is your laptop.
createDendriStore() returns a snapshot‑shaped object that React,
Vue, Svelte, and vanilla code can read identically. Bindings are a thin layer of
idiomatic glue, not a rewrite. You can swap frontends without touching the room
protocol.
The library tries direct WebRTC first, falls through STUN and TURN, and lands
on a TLS relay through your own server when nothing else holds. Your code never
learns which tier is active — broadcast() just works on the
hostile coffee‑shop network.
When the fallback drops to your relay, payloads remain sealed against the server. The signaling host sees envelope metadata, never plaintext. Operate through hostile transit without changing your threat model.
Typed presence maps, ACK‑backed RPC, broadcast topics, binary chunks, and the Yjs CRDT bridge all share a single room and a single auth context. There is no separate “multiplayer service” to wire up next to the chat one.
The server is a single Axum/Tokio binary. JWT‑scoped room tokens, signed webhook fanout, host migration, and discovery are flags, not SaaS endpoints. It runs in a container, a VM, a Quadlet unit, or whatever your platform team already loves.
Architecture
Calls degrade transparently along this path. You only configure where to start; the SDK negotiates the rest.
browser A
createDendriStore(…)
browser B
createDendriStore(…)
Direct WebRTC peer connection
NAT traversal via STUN
stun:…:3478
TURN relay (coturn or hosted)
turn:…:3478
TLS relay through dendri‑server
dendri‑server
Axum · Tokio · single binary
Configuration as code
No dashboards, no consoles. Versioned with your repo, reviewed in PRs, deployed with the rest of your app.
import { createDendriStore } from "@afterrealism/dendri-client";
import { DendriYjsProvider } from "@afterrealism/dendri-client-y";
import * as Y from "yjs";
export const store = createDendriStore({
// Point at any signaling server you operate
host: "signaling.your-domain.dev",
port: 443,
secure: true,
path: "/",
// Issue scoped JWT tokens from your backend
auth: { token: await fetchRoomToken() },
// 4-tier fallback: P2P → STUN → TURN → TLS relay
transport: {
iceServers: [{ urls: "stun:stun.your-domain.dev:3478" }],
relay: { e2e: true },
},
// Optional: enable msgpack for binary throughput
codec: "msgpack",
});
// Bridge a Yjs document over the same room
const doc = new Y.Doc();
export const provider = new DendriYjsProvider({ room: store, doc });
store.join("workspace-42");
Localhost in dev, your VM in staging, a Quadlet behind Traefik in prod. Same client, different host.
Mint short-lived JWTs from your existing backend. Dendri verifies scoped room access, never owns your user table.
Swap STUN servers, force relay, opt into msgpack for tight binary loops. Defaults stay sane out of the box.
The same store backs chat, presence, RPC, and Yjs documents.
No separate services to wire up.
Framework integrations
The same createDendriStore() snapshot model, four idiomatic
bindings.
Vanilla JS integration
Examples
Every demo lives in dendri-examples/ and uses the framework-neutral
packages directly.
Topic-routed messages with presence-aware sender identity.
PresenceSnapshot-based presence list with typed metadata per peer.
CursorsSmooth cursor sync over high-frequency presence updates.
WhiteboardShared canvas backed by Yjs CRDT with persistence hooks.
DocsRich text editing using the DendriYjsProvider bridge.
Geospatial layers synced live across all peers in a room.
GameAuthoritative-host game loop with binary state diffs.
FilesChunked binary transport with backpressure-aware delivery.
VideoMedia tracks negotiated over Dendri-managed peer connections.
Local setup
cd dendri-server
cargo run -- \
--host 127.0.0.1 \
--port 9876 \
--enable_relay \
--allow_discovery
npm install @afterrealism/dendri-client @afterrealism/dendri-client-y
import { createDendriStore }
from "@afterrealism/dendri-client";
const store = createDendriStore({
host: "127.0.0.1",
port: 9876,
secure: false,
path: "/",
});
store.join("project-room");
Task recipes
Publish and subscribe by topic for clean message routing.
const unsubChat = store.subscribe("chat", (data, peerId) => {
console.log("chat", peerId, data);
});
store.broadcast(
{ type: "chat", text: "hello team" },
{ topic: "chat" },
);
Sync user metadata and read all peers from snapshot state.
store.setPresence({
name: "Ari",
cursor: [314, 128],
color: "#67e8f9",
});
store.subscribe(() => {
const { presences } = store.getSnapshot();
console.log("presence map", presences);
});
Bridge Yjs updates over Dendri topics using
@afterrealism/dendri-client-y.
import { DendriYjsProvider } from "@afterrealism/dendri-client-y";
import * as Y from "yjs";
const doc = new Y.Doc();
const provider = new DendriYjsProvider({ room: store, doc });
store.join("shared-doc");
// cleanup: provider.destroy(); doc.destroy(); store.destroy();
Send chunks for assets or CRDT updates without JSON overhead.
const bytes = new Uint8Array([1, 2, 3, 4]);
store.broadcastBinary(bytes, { topic: "asset-chunk" });
store.subscribe("asset-chunk", (payload) => {
if (payload instanceof Uint8Array) {
console.log("binary bytes:", payload.byteLength);
}
});
Validation and deployment
cd e2e
npm install
npm run test:examples-local
Drives Playwright against Svelte, React, and Vue example apps plus the local signaling server.
cd dendri-infrastructure/deploy
# Quadlet units + bootstrap.sh
# for VM rollout
Use the deployment folder when you need a packaged service topology.
cd dendri-infrastructure/terraform
# Deploy on any cloud provider
# Terraform modules
Start from Terraform modules if you need managed infrastructure provisioning on any cloud provider.
Comparison
An honest read of the tradeoff space. Other tools are excellent; the question is which constraints they optimise for.
| Dendri | Hosted realtime cloud | y‑websocket | Roll your own | |
|---|---|---|---|---|
| Self‑host as default | yes | paid tier | yes | yes |
| Single client API across frameworks | yes | yes | Yjs only | DIY |
| WebRTC P2P with TURN+TLS fallback | built‑in | built‑in | none | DIY |
| End‑to‑end encrypted relay | yes | vendor‑visible | plaintext | DIY |
| Yjs CRDT bridge | first‑class | via adapter | native | DIY |
| Typed presence + RPC + ACK | all three | all three | awareness only | DIY |
| Per‑message billing | none | yes | none | none |
| Vendor‑side telemetry | none | always | none | your call |
| Operability surface | single Rust binary | SaaS dashboard | Node process | whatever you build |
Hosted realtime clouds are great when their pricing matches your scale and their compliance posture matches yours. Dendri is the answer when one of those stops being true.
References & lineage
Most of this project is the result of standing on prior work and gluing it together with opinions. Credit where it is due.
The IETF transport stack that makes peer‑to‑peer over the open internet
practical. Dendri's tier‑1 through tier‑3 fallback is a thin policy
layer over standard RTCPeerConnection + ICE candidate negotiation.
The collaborative editing surface in @afterrealism/dendri-client‑y
is a Yjs provider, not a reimplementation. Yjs — and the broader CRDT
research it descends from — is what makes shared documents tractable.
The Rust signaling server is an Axum HTTP/WebSocket app on top of the Tokio runtime. Optional binary framing uses msgpack for tight, schemaless wire payloads when JSON overhead matters.
Liveblocks, PartyKit, Pusher, Ably, Supabase Realtime, and Cloudflare Durable Objects all set the bar for the hosted experience. Dendri does not try to replace them — it tries to be the one you reach for when you need to own the wire.
The cultural reference points are projects like Ory, Hocuspocus, Matrix, Jitsi, and coturn — tools that ship the operability story alongside the feature surface.
This site reaches for the README‑as‑landing‑page tradition: numbered ideas, a comparison table, a lineage block, a status list. Dense documentation pages tend to outlive the visual fashion of the year.
Roadmap & status
@afterrealism/dendri‑client — framework‑neutral SDK@afterrealism/dendri‑client‑y — Yjs providerOpen source
Every package in the Dendri stack is MIT-licensed and lives under one GitHub organization. Read it, fork it, contribute back.
Framework-neutral SDK and Yjs provider. The two npm packages every Dendri app installs.
The signaling, relay, and webhook server. Single binary, container, or Quadlet unit.
Chat, presence, cursors, video, whiteboard, collaborative editor, and more — all runnable.
Infrastructure modules, deployment recipes, and the broader realtime tooling family.
Pair a tiny SDK with a Rust server and ship collaboration features without betting your roadmap on someone else’s pricing page.