Voice chat setup
Voice runs on the server itself: no third-party service, no account, no audio leaving your network unless the mesh transport relays through TURN. It is off by default on a self-hosted server. Two things are easy to miss: browsers need HTTPS for the microphone, and the built-in media server needs one open port that cannot go through your reverse proxy.
Turn it on
Admin, Server settings, Voice chat, Enable, or VOICE_ENABLED=1. Then pick the transport.
Two transports
| Media server (SFU) | Mesh | |
|---|---|---|
| How it works | The server receives every microphone and forwards to every listener, in process. | Browsers connect to each other; the server only referees. |
| Ports | One port, UDP and TCP, default 44444, open straight to the host. | None. Works through a tunnel. |
| Enforced rules | Floor enforcement, side rooms, speaking indicator, proximity gains applied by the server. | Proximity gains applied by each listener; no speaking indicator, floor enforcement or side rooms. |
| Scale | Any table. | Small tables; each device sends to every other. |
| Best for | A server with a public address and a firewall you control. | A tunnel-hosted server, or a home server where opening a port is not an option. |
Worlds hosted from the desktop and Android apps use mesh and manage this for you.
HTTPS
Browsers grant microphone access only on HTTPS or on localhost. A single machine playing on localhost works as-is; anyone else needs the server behind a reverse proxy with a certificate. The apps count as a secure context, so a table entirely on the desktop and Android apps can use voice on a plain-http LAN server. See Reverse proxy and HTTPS.
The media port
- Open
44444(orVOICE_RTC_PORT) for both UDP and TCP on the host firewall and, behind a router, forward it to the host. It is one port, not a range, however many tables run. TCP on the same port is the fallback for networks that block UDP. - Audio is not HTTP: the port cannot be proxied by nginx or Caddy and cannot go through Cloudflare's proxy or Tunnel.
- Docker publishes the port for both protocols in the compose file already.
The announced address
Behind NAT or inside Docker, the server must be told what address to hand to clients, or it announces its private one and every call "connects to silence". Set Announced address in the admin panel or VOICE_ANNOUNCED_IP to your public IP, or Voice domain / VOICE_DOMAIN to a hostname, which wins when set. A voice domain must be a plain DNS record: a Cloudflare-proxied (orange cloud) name resolves to Cloudflare, which does not carry UDP. The announced address is visible to players in their browser's WebRTC internals; that is inherent to the transport.
Mesh: STUN and TURN
Mesh calls need STUN to find each other and, on strict networks, a TURN relay. By default the server points clients at the project's broker, which hands out Cloudflare STUN always and short-lived TURN credentials within a monthly budget. Set ODM_ICE_BROKER_URL=off to use public STUN only, or point it at your own broker.
Per-campaign rules
All in the Setup tab, all off by default except turn-taking shown: turn-taking (ignored, shown, enforced), proximity with 30 feet of hearing, a 5-foot whisper and a 120-foot shout, walls that muffle, downed characters going deaf, and side rooms the Dungeon Master runs. The Dungeon Master is never muted. A member the party lead has muted is silent on the call too. A raised hand is a request only. A server restart ends every call; players rejoin from the dock.
Checking
| Symptom | Cause |
|---|---|
| Microphone button does nothing in a browser | Plain http. HTTPS or localhost only. |
| Everyone joins, nobody hears anything (media server) | The media port is closed, or the announced address is unset or wrong. |
| Works on the LAN, silent from outside | The port is not forwarded at the router, or the voice domain is Cloudflare-proxied. |
| Mesh call fails for one player on mobile data | TURN was not available; check the broker setting, and that the budget is not exhausted. |