alpha

Dendri is in active development and not yet production-ready. Feedback is very welcome — open an issue on GitHub.

Self-hosted by default

Realtime collaboration infrastructure you ship and own.

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.

licenseMIT clienttypescript serverrust sdkv2.3.7 frameworks4 examples12 self‑hostfirst‑class
// 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" });
4
fallback tiers P2P · STUN · TURN · TLS relay
2
npm packages client + Yjs provider
1
Rust binary signaling, relay, webhooks
12
example apps chat to whiteboard to game
0
vendor accounts own the wire end‑to‑end

Why this exists

Realtime should not be a rented dependency.

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

One stack, every realtime primitive

Dendri ships the building blocks you would otherwise stitch together from signaling vendors, presence services, CRDT relays, and TURN providers.

Messaging

  • Topic pub / sub
  • Multi-room chat
  • Notifications
  • Typed broadcast
  • RPC + ACK

State sync

  • Typed presence
  • Multiplayer state
  • Yjs CRDT bridge
  • Cursors
  • Live snapshots

Media

  • Video chat
  • Voice rooms
  • Screen share
  • Binary payloads
  • P2P file transfer

Transport

  • WebRTC P2P
  • STUN discovery
  • TURN relay
  • TLS server relay
  • msgpack codec

Operations

  • JWT room tokens
  • Signed webhooks
  • Host migration
  • End-to-end relay
  • Discovery service

Frameworks

  • Vanilla JS / TS
  • React
  • Vue
  • Svelte 5
  • Yjs ecosystem

Six load‑bearing ideas

The constraints Dendri actually optimises for.

Most of the project is a direct consequence of these six. If a tradeoff conflicts with one of them, the tradeoff loses.

  1. 01

    Self‑host is the default, not a tier

    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.

  2. 02

    One client surface, every framework

    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.

  3. 03

    Four transport tiers, one API

    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.

  4. 04

    Relay traffic stays end‑to‑end encrypted

    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.

  5. 05

    Presence, RPC, Yjs — one room

    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.

  6. 06

    Operate it like a normal Rust service

    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

Four tiers of fallback, one room snapshot.

Calls degrade transparently along this path. You only configure where to start; the SDK negotiates the rest.

Configuration as code

Declare your realtime stack in one file

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");

Point anywhere

Localhost in dev, your VM in staging, a Quadlet behind Traefik in prod. Same client, different host.

Bring your auth

Mint short-lived JWTs from your existing backend. Dendri verifies scoped room access, never owns your user table.

Tune the wire

Swap STUN servers, force relay, opt into msgpack for tight binary loops. Defaults stay sane out of the box.

Compose providers

The same store backs chat, presence, RPC, and Yjs documents. No separate services to wire up.

Framework integrations

One API, every frontend stack

The same createDendriStore() snapshot model, four idiomatic bindings.

Vanilla JS integration

Examples

Reference apps you can run today

Every demo lives in dendri-examples/ and uses the framework-neutral packages directly.

Local setup

Run the full stack in minutes

1

Start the signaling server

cd dendri-server
cargo run -- \
  --host 127.0.0.1 \
  --port 9876 \
  --enable_relay \
  --allow_discovery
2

Install the SDK

npm install @afterrealism/dendri-client @afterrealism/dendri-client-y
3

Connect and join a room

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

Realtime workloads, four small snippets

Realtime chat channels

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" },
);

Live presence and cursors

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);
});

Collaborative docs with Yjs

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();

Binary payload transport

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

Test locally, ship on your own infra

A

Cross-framework E2E checks

cd e2e
npm install
npm run test:examples-local

Drives Playwright against Svelte, React, and Vue example apps plus the local signaling server.

B

Podman Quadlet rollout

cd dendri-infrastructure/deploy
# Quadlet units + bootstrap.sh
# for VM rollout

Use the deployment folder when you need a packaged service topology.

C

Cloud infrastructure

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

Where Dendri sits next to the rest.

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

What Dendri is built on, and what it borrows from.

Most of this project is the result of standing on prior work and gluing it together with opinions. Credit where it is due.

WebRTC, ICE, STUN, TURN

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.

Yjs & the CRDT lineage

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.

Tokio, Axum, msgpack

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.

The hosted‑realtime cohort

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.

Self‑host‑first ecosystem

The cultural reference points are projects like Ory, Hocuspocus, Matrix, Jitsi, and coturn — tools that ship the operability story alongside the feature surface.

Editorial docs as marketing

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

What is shipped, what is next, what is being thought about.

  1. Shipped v2.3.7 · current

    Client SDK + Rust server, four framework bindings

    • @afterrealism/dendri‑client — framework‑neutral SDK
    • @afterrealism/dendri‑client‑y — Yjs provider
    • Axum signaling server — relay, JWT, webhooks, host migration
    • 12 runnable example apps
    • End‑to‑end Playwright suite across Svelte, React, Vue
  2. In flight active branch

    Operability polish

    • Hardening the discovery service
    • Structured tracing spans for relay flows
    • Chaos coverage for host‑migration scenarios
    • Ready‑made Quadlet + Terraform modules
  3. Planned on the board

    Edge deploys, richer persistence, audit hooks

    • Worker‑edge deployment target for the relay
    • Pluggable Yjs persistence backends
    • Tighter audit‑hook surface for compliance‑heavy environments

Open source

Built in the open. Ship from source.

Every package in the Dendri stack is MIT-licensed and lives under one GitHub organization. Read it, fork it, contribute back.

Own your realtime stack.

Pair a tiny SDK with a Rust server and ship collaboration features without betting your roadmap on someone else’s pricing page.