Reference
Self-hosting
The signaling server is a single Rust (Axum/Tokio) binary backed by Redis. Run it on a $5 VPS, point the SDK at it, and the entire realtime stack is yours: no usage caps, no vendor.
Requirements
- Redis (or a Redis-compatible store): default
redis://127.0.0.1:6379, override with--redis_url. - A container runtime (Podman or Docker), or a Rust toolchain to build from source.
- For production: a domain with TLS (either terminate at the server with
--sslkey/--sslcert, or put it behind a reverse proxy). - For strict-NAT users: a TURN server such as coturn (below).
Run with Podman / Docker
# Redis
podman run -d --name dendri-redis -p 6379:6379 docker.io/redis:7
# Build the server image from the public source
git clone https://codeberg.org/afterrealism/dendri
cd dendri
podman build -t dendri-server:local .
# Signaling server
podman run -d --name dendri \
-p 9876:9876 \
dendri-server:local \
--host 0.0.0.0 --port 9876 \
--redis_url redis://host.containers.internal:6379 \
--enable_relay
For production, run the same image under systemd (Podman Quadlets) next to Redis and coturn; the Dockerfile in the server repo is the deployment unit. TURN with coturn is covered below.
Run from source
git clone https://codeberg.org/afterrealism/dendri
cd dendri
cargo build --release
# Local development
cargo run -- --host 127.0.0.1 --port 9876 --enable_relay --allow_discovery
# With TLS terminated by the server itself
cargo run -- --host 0.0.0.0 --port 443 \
--sslkey /path/to/key.pem --sslcert /path/to/cert.pem \
--enable_relay
Key configuration flags
Every flag can also be set via environment variable; the full reference is in the server repo.
| Flag | Env | Default | Purpose |
|---|---|---|---|
--port | PORT | 9000 | Listen port. |
--host | — | :: | Bind address. |
--key | DENDRI_KEY | dendri | Connection key namespace (must match the client's key). |
--path | PEERSERVER_PATH | / | URL prefix (must match the client's path). |
--redis_url | — | redis://127.0.0.1:6379 | State backend. |
--enable_relay | DENDRI_ENABLE_RELAY | false | Tier-4 encrypted WebSocket relay. |
--allow_discovery | DENDRI_ALLOW_DISCOVERY | false | Enable the GET /:key/peers listing endpoint. |
--turn_secret | TURN_SECRET | — | HMAC secret for minting ephemeral TURN credentials. |
--turn_servers | — | — | TURN server URLs handed to clients. |
--jwt_secret | DENDRI_JWT_SECRET | — | Require and validate JWTs from clients. |
--sslkey / --sslcert | — | — | Terminate TLS in the server itself. |
--cors | — | — | Allowed CORS origins (repeatable). |
--concurrent_limit | — | 10000 | Max simultaneous clients. |
HTTP endpoints
| Method | Path | Description |
|---|---|---|
GET | /health | Liveness probe. |
GET | /metrics | Prometheus metrics. |
GET | /{base}/:key/id | Generate a peer ID. |
GET | /{base}/:key/peers | List peers (requires --allow_discovery). |
GET | /{base}/:key/turn-credentials | Ephemeral TURN credentials (requires --turn_secret). |
GET | /{base}/dendri | WebSocket upgrade; the primary signaling transport. |
GET | /{base}/http/sse | Server-Sent Events fallback transport. |
POST | /{base}/http/send | HTTP send (pairs with SSE / long-poll). |
GET | /{base}/http/poll | HTTP long-poll fallback transport. |
Per-tenant JWT room ACLs (multi-tenant)
In a multi-tenant deployment each tenant can carry its own jwt_secret, so your backend
mints short-lived HS256 tokens that gate which rooms your end-users may join. The client
passes the token as jwt next to apiKey — on the WebSocket, SSE,
and polling transports alike.
Precedence when a connection's API key resolves to a tenant:
- The tenant's
jwt_secret, when set — the JWT must verify against it. - Otherwise the global
--jwt_secret, when set. - Otherwise JWT auth is not enforced for that connection.
Tokens must be HS256 with exp. The rooms claim (array of room
IDs) gates ROOM-JOIN — absent means all rooms. The optional tid
claim must equal the tenant's id when present.
import jwt from 'jsonwebtoken';
function boardToken(user, roomIds) {
return jwt.sign(
{ sub: user.id, tid: 't_xxxxxxxxxxxx', rooms: roomIds },
process.env.DENDRI_TENANT_JWT_SECRET,
{ algorithm: 'HS256', expiresIn: '5m' }
);
}
Provision the secret via the admin API (--admin_token): set
jwt_secret on POST /admin/tenants (returned once, like the API
key), or rotate later with POST /admin/tenants/:id/rotate-jwt-secret.
GET responses expose only has_jwt_secret — never the secret itself.
TURN with coturn
Without TURN, peers behind symmetric NAT (many mobile carriers, some offices) can't establish direct connections. Run coturn next to the server with a shared HMAC secret:
listening-port=3478
fingerprint
use-auth-secret
static-auth-secret=YOUR_SHARED_SECRET
realm=turn.example.com
--turn_secret YOUR_SHARED_SECRET \
--turn_servers turn:turn.example.com:3478
Clients then opt in with fetchTurnCredentials: true: the SDK fetches short-lived
credentials from /turn-credentials before connecting, so no static TURN passwords
ever ship in your frontend bundle.
Pointing the SDK at your server
const store = createDendriStore({
url: "wss://signal.example.com", // your domain
fetchTurnCredentials: true,
enableRelay: true,
signalingTransport: "auto",
});
Scaling and operations
- State lives in Redis, so the server process is restartable without dropping room state; clients auto-reconnect and re-send membership.
- Monitoring: scrape
/metricswith Prometheus; probe/healthfrom your load balancer. - Sizing: signaling is tiny: handshakes and heartbeats only. The routing hot path costs nanoseconds in-process (criterion: ~56 ns peer lookup, ~44 ns fan-out to a 50-peer room), so the default
--concurrent_limit 10000is realistic on a small VM — the data path is peer-to-peer, and rooms cap at--max_room_size 50by default. TURN bandwidth (when used) is the real cost driver. - Reverse proxy: if you terminate TLS at nginx/Caddy, make sure WebSocket upgrade headers are forwarded and idle timeouts exceed the 60 s heartbeat.
Licensing
| Component | License | What it means for you |
|---|---|---|
@afterrealism/dendri-client, @afterrealism/dendri-y |
Apache-2.0 | Use, modify, and embed in any app (commercial or closed-source) with attribution. |
dendri-server, dendri-infrastructure |
AGPL-3.0 | Run it freely. If you modify it and offer it as a network service, you must publish your modifications under AGPL. |
Running the unmodified server for your own product is fine and requires nothing beyond keeping the license notice. If you modify the server and offer it as a network service, publish those modifications under AGPL — that's the only condition.
Deployment questions? Open an issue on Codeberg.