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.

MIT licensed TypeScript & Rust React · Vue · Svelte
// 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" });

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

Features

Everything a realtime app actually needs

One SDK and one server. No proprietary cloud, no per-message billing, no lock-in.

01

Framework-neutral SDK

A single createDendriStore() works in React, Vue, Svelte, and vanilla JS. Wrap with framework helpers, never rewrite the room.

02

Self-host first

No hidden hosted defaults. Your app always points at a signaling server you operate — container, VM, or bare metal.

03

WebRTC with 4-tier fallback

Direct P2P first, then STUN, TURN, and a TLS relay through your server when carriers and NATs get in the way.

04

Typed presence & rooms

Subscribe to a room snapshot of peers, presence maps, and connection state with end-to-end TypeScript types.

05

RPC + ACK delivery

Request/response semantics with message acknowledgement. Build commands, not hopeful broadcasts.

06

Yjs CRDT bridge

@afterrealism/dendri-client-y ships a provider that pipes Yjs updates over Dendri topics — collaborative docs in a few lines.

07

Binary payloads

Send Uint8Array chunks for assets, audio frames, or CRDT updates with no JSON overhead. Optional msgpack serializer.

08

Host migration & relay E2E

Rooms survive host churn. Relay-mode payloads stay end-to-end encrypted so your server never sees plaintext.

09

JWT auth & webhooks

Issue scoped room tokens. Forward connect, disconnect, and lifecycle events to your backend over signed webhooks.

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
# Alibaba + Cloudflare
# Terraform modules

Start from Terraform modules if you need managed infrastructure provisioning.

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.