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
# Signaling server
podman run -d --name dendri \
-p 9876:9876 \
ghcr.io/afterrealism/dendri-server:latest \
--host 0.0.0.0 --port 9876 \
--redis_url redis://host.containers.internal:6379 \
--enable_relay
A production-grade Podman Quadlet stack (server + Redis + coturn + website, with systemd units) lives in dendri-infrastructure, including Terraform for VM provisioning.
Run from source
git clone https://github.com/afterrealism/dendri-server
cd dendri-server
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. |
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",
});
apiKey) changes. No feature gates.
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 need to modify the server and keep those changes private while offering it as a service, a commercial license is available from Afterrealism: get in touch.
Deployment questions? Open an issue on GitHub.