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.