Architecture
For the people who won't run software they can't picture. Here's the stack, the media path, and where the encryption boundaries sit.
The big picture
┌─ Desktop client (Electron) ─────────────────────────────┐
│ UI / app state (single-source store, plain JS) │
│ MLS E2EE engine (mls-rs, Rust → WASM, worker pool) │
│ Media frame encryption (WebRTC insertable streams) │
│ Native helpers (own processes): │
│ · capture: DXGI (Win) / PipeWire·X11 + GStreamer (Lx) │
│ · audio: WASAPI process-loopback / PipeWire, DTLN NS │
│ · push-to-talk: OS-level input hook (evdev on Linux) │
│ · sender: libwebrtc native encoder pipeline │
└───────────────┬─────────────────────────────────────────┘
│ WSS (one port) + WebRTC media (one port)
┌───────────────▼─────────────────────────────────────────┐
│ Self-host server (Node.js) │
│ · Fastify HTTP/WebSocket + Better-Auth (2FA TOTP) │
│ · mediasoup SFU — routes encrypted media frames │
│ · E2EE delivery: opaque ciphertext store-and-forward │
│ · RBAC on every socket handler │
│ · PostgreSQL or embedded SQLite (Kysely, auto-migrate)│
│ · Automatic TLS (Let's Encrypt / win-acme) │
└─────────────────────────────────────────────────────────┘
Design decisions worth explaining
Native capture instead of the browser path
Chromium's built-in screen capture wasn't built for streaming games. Speakeasy captures with the platform's fast path — DXGI desktop duplication on Windows, PipeWire portals or X11 on Linux — encodes with hardware encoders when available (NVENC, QuickSync, VA-API), and hands encoded frames between native processes over a shared C ABI so frames never bounce through JavaScript. Per-app share audio uses the OS's own isolation primitives (WASAPI process loopback on Windows, PipeWire streams on Linux) rather than virtual audio cables.
Push-to-talk as a real input hook
PTT runs in a dedicated helper process reading OS-level input events (evdev on Linux — which is why it works on Wayland, where compositors block global shortcuts). It's a read-only listener, isolated so a crash restarts it without touching your call.
MLS for encryption, not a homegrown protocol
Private conversations use Messaging Layer Security (RFC 9420) via mls-rs, an established Rust implementation compiled to WebAssembly and run in a worker pool. Media gets the same treatment at the frame level: encoded audio/video frames are encrypted with MLS-derived keys through WebRTC insertable streams before reaching the server, so the SFU forwards ciphertext it cannot read. The precise boundaries — including what metadata the server still sees — are spelled out in the threat model.
An SFU, not peer-to-peer
Media routes through mediasoup on your server rather than full-mesh P2P. That's what makes ten-person rooms and multi-viewer screen shares work on normal upload bandwidth — the sharer uploads once, the server fans out. The frame encryption above is what keeps the SFU from being a confidentiality trade-off.
Boring, ownable server tech
The server is Node.js + Fastify + PostgreSQL — or embedded SQLite for guided installs, so a self-hoster deploys one service with zero external dependencies. Attachments live in the database (encrypted client-side for private conversations), so a backup is the database plus the configuration and certificate folders. Schema migrations run automatically at boot; upgrading is rerunning the installer. TLS is issued and renewed automatically. Two open ports total: HTTPS and one WebRTC media port.
Source access
All of this is inspectable — source access comes with Founding Host access, and security review of any claim on this page is explicitly welcome under the disclosure policy.