https://github.com/oddsock/distokoloshe
Self hosted E2EE video/voice/screen sharing. TLS by design, docker containers. Quick Deployment
https://github.com/oddsock/distokoloshe
containers docker-compose ephemeral-messaging streaming-video video-call voice-call
Last synced: 3 months ago
JSON representation
Self hosted E2EE video/voice/screen sharing. TLS by design, docker containers. Quick Deployment
- Host: GitHub
- URL: https://github.com/oddsock/distokoloshe
- Owner: oddsock
- License: mit
- Created: 2026-02-18T15:01:34.000Z (5 months ago)
- Default Branch: main
- Last Pushed: 2026-03-07T21:36:29.000Z (4 months ago)
- Last Synced: 2026-03-07T21:51:50.166Z (4 months ago)
- Topics: containers, docker-compose, ephemeral-messaging, streaming-video, video-call, voice-call
- Language: TypeScript
- Homepage:
- Size: 399 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# disTokoloshe
Self-hosted real-time voice, video, and screen-sharing web application. Built on LiveKit (WebRTC SFU), with end-to-end encryption, per-user volume controls, and high-framerate screen sharing (up to native resolution/refresh rate with AV1/VP9).
## Architecture
```
┌─────────┐ ┌──────────────┐ ┌─────────────┐
│ Browser │──────▶│ nginx (web) │──────▶│ API (Node) │
│ (SPA) │ │ :80 / :443 │ │ :3000 │
└────┬─────┘ └──────────────┘ └──────┬──────┘
│ │
│ WebRTC (media) │ LiveKit token
│ │ generation
▼ ▼
┌──────────────────────────────────────────────────────┐
│ LiveKit SFU (host network) │
│ WS :7881 · UDP :50201-50400 · TURN :3478/5349 │
└──────────────────────────────────────────────────────┘
▲
│ audio track
┌──────────────────────┐
│ Music Bot (Python) │
│ DJ Tokoloshe :3001 │
└──────────────────────┘
```
- **web** — nginx serving the Vite/React SPA. Reverse-proxies `/api/` to the API container and `/livekit/` to the LiveKit WebSocket.
- **api** — Node.js/Express. Handles user auth (JWT + SQLite), room management, votes, punishments, whispers, soundboard, chat, music state, SSE events for real-time presence, and LiveKit token generation.
- **music** — Python bot ("DJ Tokoloshe") that streams internet radio into a dedicated LiveKit room. Exposes a small HTTP API on `:3001` for playback control; notifies the API server of state changes for SSE broadcast.
- **desktop** — Tauri v2 desktop client (Windows/Linux/macOS). Shares UI code with the web app via `@distokoloshe/ui` package. Connects to any disTokoloshe server via configurable URL.
- **livekit** — LiveKit SFU running in host network mode for proper WebRTC ICE negotiation. Includes built-in TURN relay for restrictive NATs (production only).
- **certbot** — (production only) Auto-renews Let's Encrypt TLS certificates every 12 hours.
## Quick Start (Development)
```bash
# 1. Clone and generate secrets
git clone && cd distokoloshe
./infra/scripts/init.sh
# 2. Build and start
docker compose build
docker compose up -d
# 3. Open in browser
open http://localhost:3080
```
Register a user, create a room, and start talking. No TLS required for local development.
## Production Deployment
### Prerequisites
- A server with a public IP
- A domain name with DNS A record pointing to that IP
- Ports open on your firewall:
| Port | Protocol | Purpose |
|------|----------|---------|
| 80 | TCP | HTTP (ACME challenge + redirect to HTTPS) |
| 443 | TCP + UDP | HTTPS + HTTP/3 (QUIC) |
| 7881 | TCP | LiveKit WebSocket signaling |
| 3478 | UDP | TURN relay |
| 5349 | TCP | TURN/TLS relay |
| 50201-50400 | UDP | WebRTC media (RTP) |
### Step 1: Generate secrets
```bash
./infra/scripts/init.sh
```
### Step 2: Edit `.env`
Open `.env` and set your domain and email:
```
DOMAIN=chat.yourdomain.com
ACME_EMAIL=you@yourdomain.com
```
For production, set ports to standard values:
```
WEB_PORT=80
WEB_TLS_PORT=443
LK_PORT=7881
```
The rest (`LIVEKIT_API_KEY`, `LIVEKIT_API_SECRET`, `JWT_SECRET`) were auto-generated by `init.sh`. Don't change them after users have registered (JWT_SECRET invalidates all sessions, LIVEKIT keys break media relay).
### Step 3: Get TLS certificates
```bash
# Test first (doesn't hit rate limits)
./infra/scripts/init-certs.sh --dry-run
# If dry run succeeds, get a real certificate
./infra/scripts/init-certs.sh
```
This will:
1. Start nginx in HTTP-only mode on port 80
2. Run certbot to get a Let's Encrypt certificate via webroot challenge
3. Save the certificate to `./data/certs/`
4. Restart all services with TLS enabled (including the certbot renewal container)
### Step 4: Verify
Visit `https://chat.yourdomain.com`. You should see the login page with a valid TLS certificate.
LiveKit's entrypoint automatically detects TLS certs and switches to production mode:
- **Dev mode** (no certs): `use_external_ip: false`, TURN disabled
- **Prod mode** (certs present): `use_external_ip: true`, TURN enabled with TLS
### Certificate Renewal
Certbot runs as a persistent container (under the `production` profile) and checks for renewal every 12 hours. Certificates are stored on disk at `./data/certs/` (bind-mounted into containers), so they survive `docker volume prune`.
> **Note:** After automatic renewal, nginx and livekit must be restarted to pick up the new certificate. Add a host-level cron job to handle this:
> ```
> 0 3 * * * cd /path/to/distokoloshe && docker compose restart web livekit 2>/dev/null
> ```
To manually force a renewal:
```bash
docker compose run --rm --entrypoint "certbot" certbot renew --force-renewal
docker compose restart web livekit
```
### Backup & Restore
All persistent data lives in `./data/`. To back up the SQLite database and certificates:
```bash
# Backup
tar czf distokoloshe-backup-$(date +%Y%m%d).tar.gz ./data/api/ ./data/certs/
# Restore
tar xzf distokoloshe-backup-YYYYMMDD.tar.gz
docker compose up -d
```
The database is at `./data/api/distokoloshe.db`. Soundboard audio clips are stored as BLOBs in the database, so a single backup covers everything.
### Troubleshooting
**Can't connect / audio drops in restrictive networks (corporate, strict NAT)**
LiveKit uses TURN relay in production mode (when TLS certs are present). If clients can connect via direct P2P but audio drops for some users, check:
1. Ports `3478/UDP` and `5349/TCP` are open in your firewall
2. The TURN server is working — test with: `curl -s https://chat.yourdomain.com/api/server-info`
3. LiveKit logs: `docker compose logs livekit --tail=50`
If TURN isn't helping, ensure `DOMAIN` is set correctly in `.env` — LiveKit uses it to advertise its public IP to clients.
**Linux Docker: `host.docker.internal` not resolving**
The API and web containers use `host.docker.internal` to reach the LiveKit container (which runs in host network mode). This is pre-configured via `extra_hosts: host.docker.internal:host-gateway` in `compose.yml` — no additional setup required on Linux as long as you're using Docker Engine ≥ 20.10.
**HTTP/3 (QUIC) not working**
Port `443/UDP` must be open in your firewall for HTTP/3 to function. Clients fall back to TCP automatically if UDP 443 is blocked, but HTTP/3 improves latency and performance when available.
### Updating
```bash
git pull
docker compose build
docker compose up -d
```
The SQLite database in `data/api/` persists across rebuilds.
## Environment Variables
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `DOMAIN` | Yes (prod) | `localhost` | Domain for TLS certs and TURN server |
| `ACME_EMAIL` | Yes (prod) | — | Email for Let's Encrypt registration |
| `LIVEKIT_API_KEY` | Yes | — | LiveKit API key (generated by init.sh) |
| `LIVEKIT_API_SECRET` | Yes | — | LiveKit API secret (generated by init.sh) |
| `JWT_SECRET` | Yes | — | JWT signing secret (generated by init.sh) |
| `E2EE_SECRET` | Yes | — | E2EE key derivation secret (generated by init.sh) |
| `WEB_PORT` | No | `3080` | Host port for HTTP |
| `WEB_TLS_PORT` | No | `3443` | Host port for HTTPS / HTTP/3 |
| `LK_PORT` | No | `7881` | LiveKit WebSocket port |
| `SERVER_CITY` | No | — | Server location label (shown in connection quality UI) |
| `MUSIC_ROOM_NAME` | No | `Music` | LiveKit room name the music bot joins |
| `GITHUB_REPO` | No | — | GitHub repo for auto-update sync (e.g., `user/distokoloshe`) |
| `GITHUB_TOKEN` | No | — | GitHub token (only needed for private repos) |
## Features
- **Voice & video rooms** — Create voice or video rooms. Auto-joins your last room on login. Click any participant's camera to spotlight/maximize it (same as screen shares)
- **Screen sharing** — Quality picker (720p/1080p/1080p60/native), browser-aware codec selection (AV1 on Firefox, VP9 on Chromium, VP8/H.264 on Safari), optional audio capture
- **End-to-end encryption** — AES-GCM via LiveKit Encoded Transforms with per-room HMAC-SHA256 key derivation. Graceful fallback to unencrypted on unsupported browsers (Safari)
- **Per-user volume control** — Individual volume sliders (0-200%) with persistent settings per participant. Audio routed through Web Audio API for proper stereo upmix across all platforms
- **Soundboard** — Upload audio clips (max 10 seconds) to play to the room. Inline clip trimmer with waveform visualization, draggable start/end handles, preview playback with animated scrubber, and trim-to-WAV before upload. Per-clip preview (local-only) and room playback with volume control
- **Democratic moderation** — Vote to jail disruptive users. 30-second voting window, quorum of 3, simple majority. Passed votes auto-create a timed jail room and kick the target from LiveKit. Room creator can lift punishments early
- **Whispers mode** — Room mode that shuffles participants into a random chain (Fisher-Yates). Chain updates live as users join/leave
- **Music bot** — DJ Tokoloshe streams internet radio into rooms via a Python LiveKit bot. HTTP API for playback control, now-playing display with marquee scroll, E2EE compatible
- **Sound notifications** — 4 synthesized sound packs (Mystical Chimes, Mischievous Pops, Retro Arcade, Digital Whispers) with 8 events each. All generated via Web Audio API oscillators — no audio files
- **Real-time presence** — SSE-based online/offline status, room membership, vote/punishment/whispers events (13 event types)
- **Connection quality** — Live signal strength indicator with RTT (round-trip time) and jitter stats, server region display
- **Desktop client** — Tauri v2 app (Windows/Linux/macOS) sharing UI code with web via `@distokoloshe/ui`. Configurable server URL, window state persistence, global shortcuts (OS-level mute/deafen with modifier keys), and self-hosted auto-updater with ed25519 signed updates
- **Noise cancellation** — Real-time background noise removal via WASM AudioWorklet. Choose between RNNoise (ML neural network, best voice isolation) or Speex DSP (lighter CPU). Toggle and engine preference persist across sessions
- **Soundbites** — Save the last 10 seconds of any participant's audio as a WAV file. Client-side ring buffer recording (~1.9 MB/participant), fully E2EE compatible. Participants can opt out via settings
- **Light/dark theme** — Toggle with persistent preference
- **Device selection** — Choose microphone, speaker, and camera from settings
## Project Structure
```
├── compose.yml # Docker orchestration
├── .env # Secrets + config (generated by init.sh, gitignored)
├── package.json # npm workspaces root
├── apps/
│ ├── web/ # Vite + React SPA (nginx + Docker)
│ │ ├── nginx.conf # Dev proxy config
│ │ ├── nginx.tls.conf # Production TLS config (HTTPS + HTTP/3)
│ │ └── src/ # Web-specific entry (App.tsx, main.tsx)
│ ├── desktop/ # Tauri v2 desktop client
│ │ ├── src/ # Desktop entry (App.tsx, ServerConfig.tsx)
│ │ └── src-tauri/ # Rust shell (global shortcuts, window state)
│ ├── server/ # Node.js/Express API (JWT + SQLite)
│ │ └── src/
│ │ ├── routes/ # auth, rooms, users, votes, punishments, whispers,
│ │ │ # soundboard, chat, music, updates, events (SSE)
│ │ ├── livekit.ts # Token generation + E2EE key derivation
│ │ └── events.ts # SSE client tracking + room membership
│ └── music/ # DJ Tokoloshe — Python music bot
│ ├── main.py # Entry point + HTTP API (:3001)
│ ├── bot.py # LiveKit participant (publishes audio track)
│ ├── player.py # Internet radio playback (FFmpeg)
│ └── stations.py # Station list
├── packages/
│ └── ui/ # Shared React components, hooks, and utilities
│ └── src/
│ ├── pages/ # Login, Room
│ ├── hooks/ # useLiveKitRoom, useScreenShare, useAudioMixer, etc.
│ ├── components/ # ScreenShareView, VolumeSlider, UserList, etc.
│ └── lib/ # api.ts, sounds.ts, utils.ts, codec.ts, imageResize.ts
├── infra/
│ ├── livekit/ # LiveKit SFU config + entrypoint
│ ├── music/ # Music bot Node.js dependencies (build cache)
│ └── scripts/ # init.sh, init-certs.sh
└── data/ # Persistent data (gitignored)
├── api/ # SQLite database
├── certs/ # TLS certificates (Let's Encrypt)
└── certbot/www/ # ACME challenge webroot
```
## Desktop Auto-Updates
The desktop app supports self-hosted auto-updates via ed25519-signed binaries. Updates flow through the server the client is connected to.
### Setup
1. **Generate signing keys**: `npx tauri signer generate -w ~/.tauri/distokoloshe.key`
2. **Configure**: Put the public key in `apps/desktop/src-tauri/tauri.conf.json` under `plugins.updater.pubkey`
3. **CI**: Add `TAURI_SIGNING_PRIVATE_KEY` and `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` as GitHub Actions secrets
4. **Server**: Set `GITHUB_REPO=youruser/distokoloshe` in `.env` — the server auto-downloads new releases from GitHub every 60 minutes
### Pipeline
Push a version tag (e.g., `git tag v1.1.0 && git push --tags`) → CI builds + signs → GitHub Release created → server syncs within 60 min → connected clients auto-update.
Without `GITHUB_REPO`, admins can manually place the `.exe` + `.sig` files in the server's `data/api/updates/` directory.
### Security
The public key is baked into the app binary at build time. Only updates signed with the matching private key are accepted — even a compromised server cannot push unsigned updates.
## macOS Desktop App
The macOS build is currently unsigned (no Apple Developer certificate). You may encounter:
- **"disTokoloshe is damaged and can't be opened"** — Run `xattr -cr /Applications/disTokoloshe.app` to remove the quarantine flag, then open normally.
- **Keychain password prompt** ("distokoloshe wants to use your confidential information...") — This appears because the E2EE encryption keys are stored in the macOS Keychain. Click **"Always Allow"** to permanently grant access and stop the prompt from recurring.
These issues will be resolved once the app is signed with an Apple Developer certificate.
## Planned Features
- **Desktop: system tray menu** — Context menu with mute/deafen/quit actions.
- **Apple Developer signing** — Codesign + notarize the macOS build to eliminate Gatekeeper and Keychain prompts.
## Security
- **Auth** — bcrypt password hashing (12 rounds), JWT with 7-day expiry (HS256), rate-limited login (20/15min) and registration (5/hour per IP)
- **E2EE** — AES-GCM end-to-end encryption via LiveKit Encoded Transforms. Per-room key derivation using HMAC-SHA256 with a dedicated `E2EE_SECRET` (separate from `JWT_SECRET`)
- **TLS** — TLS 1.2/1.3 only, HSTS with 2-year max-age, OCSP stapling, HTTP/3 (QUIC)
- **Headers** — CSP (frame-ancestors 'none', strict script/connect/media sources), X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy strict-origin-when-cross-origin, Permissions-Policy (camera/mic/display-capture self-only)
- **API** — CORS disabled at Express level (`origin: false`), nginx whitelists only Tauri desktop origins. 16KB body limit, parameterized SQL queries (no injection), input validation with regex/whitelist on all user inputs
- **Database** — SQLite with WAL mode, foreign key constraints enabled
- **SSE** — Per-user connection cap (3) and global cap (500), 30s keepalive ping
- **Desktop** — Tauri CSP restricts connect-src, media-src, script-src. WASM eval allowed only for livekit-client
## License
MIT