dSpeak is a self-hosted communication platform for text chat, presence, voice, video, screen sharing, soundboards, and notifications. It runs as one long-lived Nuxt and Nitro application. PocketBase stores persistent data; native WebRTC and mediasoup carry realtime media.
dSpeak is free software released under the GNU Affero General Public License version 3 (AGPL-3.0). You can use, modify, and distribute it under the terms of that license.
If you modify dSpeak and run a modified version on a publicly accessible network service, you must make the modified source code available to all users who interact with it, as required by section 13 of the AGPL-3.0.
- Rooms with text, voice, camera, screen sharing, and shared audio
- Room roles, branding, media policies, notifications, and member nicknames
- Protected room soundboards and personal system-sound settings
- Direct WebRTC for two participants and optional mesh for three or four
- Automatic mediasoup SFU routing when direct media is unavailable or unsafe
- IPv6-first SFU connectivity with Playit IPv4 and TURN fallbacks
- RTC diagnostics, health checks, and Prometheus-compatible metrics
- Same-origin HTTP and WebSocket endpoints by default
dSpeak is a Nuxt 4 monolith:
| Layer | Responsibility |
|---|---|
app/ |
Vue interface, Pinia state, browser capture, and media playback |
server/routes/ |
Nitro HTTP and WebSocket endpoints |
server/utils/ |
PocketBase access, authorization, migrations, and media orchestration |
| PocketBase | Persistent users, rooms, messages, policies, and notifications |
| mediasoup | Process-owned SFU workers, routers, transports, and RTP forwarding |
The application must run as a persistent Node.js process. Serverless and edge runtimes are unsupported because WebSockets and mediasoup resources live in process memory. Run one application instance unless a distributed signaling backplane and mediasoup router piping have been implemented.
Every call starts on the SFU so participants have a reliable media path. dSpeak may then move the whole room to a verified direct route without restarting local capture.
| Participants | Preferred stable route |
|---|---|
| 1 | mediasoup SFU |
| 2 | Direct WebRTC after qualification |
| 3–4 | SFU, with a full mesh upgrade when every edge qualifies |
| 5 or more | mediasoup SFU |
The current route remains active until every client confirms the replacement. Membership changes and unhealthy direct connections return the room to the SFU. See Hybrid media topology for timing, health, handoff, bitrate, and recovery details.
- Bun for installation, development, testing, and builds
- Node.js 24 for the production server
- PocketBase with an administrator account available to Nitro
- FFmpeg and ffprobe when running outside Docker
- A public IPv4 or IPv6 route for production WebRTC traffic
Docker includes FFmpeg and ffprobe. A non-container host must provide both tools
on PATH for soundboard conversion.
cp .env.example .env
bun install
bun run devThe development server listens on http://localhost:3000.
At minimum, configure the authentication service and PocketBase connection:
AUTH_PATH=https://api.example.com/auth
DSPEAK_PUBLIC_ORIGIN=https://app.example.com
DSPEAK_METRICS_TOKEN=replace-with-a-long-random-secret
POCKETBASE_URL=https://pocketbase.example.com
PBASE_ADMIN_EMAIL=admin@example.com
PBASE_ADMIN_PASSWORD=replace-this-valueAUTH_PATH is server-only and must expose the DWS one-time handoff endpoints.
DSPEAK_PUBLIC_ORIGIN must be the exact browser origin; the generated
authentication callback is <DSPEAK_PUBLIC_ORIGIN>/auth.
Nitro applies pending PocketBase migrations during startup. A migration failure stops the application so it cannot run against a partially updated schema. The initializer can prepare a fresh PocketBase instance and repairs a missing required collection on a later startup. See the database design for the collection model and initialization contract.
bun install --frozen-lockfile
bun run test
bun run build
bun run startbun run start loads .env and starts .output/server/index.mjs. The default
container setup exposes Nitro on host port 31100 and mediasoup on TCP and UDP
port 40000.
For the complete Coolify, Docker Compose, DNS, firewall, Playit, and TURN setup, follow the deployment runbook. Do not expose a production instance until the runbook's external connectivity checks pass.
| Path | Purpose |
|---|---|
/health |
Application and configured TURN health |
/metrics |
Bearer-protected Prometheus media metrics |
/socket |
Media signaling WebSocket |
/api/presence |
Presence WebSocket |
/api/chat/socket |
Realtime chat WebSocket |
/api/room/* |
Room management |
/api/channel/* |
Text and media channels |
/api/chat/* |
Messages, read state, and push subscriptions |
/api/soundboard/* |
Protected room soundboard operations and media |
Protected application APIs and WebSockets are same-origin so the server-owned HttpOnly session is used consistently. Follow the production readiness gate before release.
Run the repository gates before release:
bun run format
bun run format:check
bun run test
bun run buildAfter deployment, verify the production process and its public routes:
curl --fail https://app.example.com/health
curl --fail -H "Authorization: Bearer $DSPEAK_METRICS_TOKEN" \
https://app.example.com/metricsMedia releases also require the real-browser and external-network checks in Hybrid media topology and Deployment. Unit tests cannot prove browser ICE behavior, hardware capture, firewall rules, or public IPv4 and IPv6 reachability.
- PocketBase administrator credentials remain server-only.
- Nitro validates authentication, room membership, and authorization.
- Soundboard files and icons are served through membership-protected endpoints.
- WebSocket messages are authenticated and validated before changing state.
- VAPID private keys and TURN shared secrets must never reach the browser.
- The RTC hostname must be DNS-only; an HTTP proxy cannot carry mediasoup RTP.