LeChat is a Windows app that gives a group of friends voice, video, screen share, and text chat with no central service and no accounts. The app contains its own server: whoever launches it first on a machine is the host, and everyone else connects to them. A bundled LiveKit SFU carries the media, SQLite holds the chat, and Tailscale Funnel makes the host’s home machine reachable from the internet without touching the router.
Funnel gives exactly one public TLS front door. That constraint produced the mux, and the mux is sixty-six lines.
the constraint
LiveKit hardcodes turns:<domain>:443 in what it advertises to clients. A browser that cannot reach the SFU directly, which under carrier-grade NAT is every browser, falls back to the TURN relay, and the SFU tells it to find that relay on port 443 of the same hostname the web app is served from.
So one hostname and one port have to serve the web app, the signaling traffic, and the TURN relay. There is one certificate, terminated once, at the edge of the app.
the mux
Funnel passes raw TLS on public 443 to a small mux listening on the loopback address. The mux terminates TLS once, with ALPNProtocols: ['http/1.1'] to keep browsers off HTTP/2, because the backend speaks 1.1 and negotiating h2 at the edge would break it.
Then it buffers until the protocol is decidable and routes on eight bytes:
head.length >= 8
&& head[0] <= 0x03
&& head.readUInt32BE(4) === 0x2112a442
The first two bytes of a STUN message are the method and type, whose high bits are always zero, so the first byte is at most 0x03. Bytes four through seven are the STUN magic cookie, 0x2112A442, present in every RFC 5389 message. A stream that opens with that shape is TURN and goes to the relay on its internal port. Everything else is web and goes to the Express server, which also proxies the SFU’s signaling path so that rides the app’s own origin. One hostname, one certificate, three protocols.
Two details keep the mux honest. It must see eight bytes before deciding, because a browser whose first TLS record arrives split across two packets sends fewer, and deciding early misroutes it to the relay. That is written down as an invariant in the project’s guidance file, alongside “never put a TLS-terminating proxy in front of it”, because a proxy that terminates TLS would hand the mux plaintext HTTP with no STUN cookie to find. A 1.5-second deadline routes any client that sends fewer than eight bytes and then stalls by whatever it did send, so a half-open port scanner cannot hold a socket forever.
why relay-only
Direct media through forwarded router ports would be faster, and the SFU supports it. It is ruled out as the default because under carrier-grade NAT a forwarded port is unreachable no matter how it is configured. The ISP’s NAT sits in front of the home router, and there is nothing to forward through. So public media rides the TURN relay, and the cost is measured rather than assumed: about 300 ms round trip through a distant Funnel front end, against 23 ms through a same-city relay when one is available.
One consequence worth stating because it invalidates an obvious alternative: LiveKit is ICE-lite and never opens its own TURN allocation. The SFU must be directly dialable by the relay. A hosted TURN service cannot make an unreachable host reachable, so “just use a TURN provider” is not a fix for the NAT problem, only for the client side of it.
host election is a port race
There is no config flag saying who is the host. The app tries to bind its port. The first launch on a machine succeeds and runs the server. A second launch hits EADDRINUSE, logs that it is running as a client, and connects to the first. No coordination, no leader election protocol, no state to get out of sync.
hearing yourself
Screen share captures the computer’s audio through Windows loopback, so friends hear whatever is playing. Without care, that includes the app’s own playback, and everyone hears themselves echoed back a moment later. Capture is opened with the flag that excludes the app’s own output, and a probe script measures the difference: without it, the app’s output reached the share at 0.53 peak RMS, with it every phase sits at the noise floor. Capture is raw, with voice processing disabled, so shared music is not mangled by an echo canceller built for speech.
what it is
Version 1.1, LiveKit pinned to one release, rooms of up to sixteen with six to ten in practice, share presets from 1.5 to 14 Mbps. The SFU binary is fetched at build time rather than committed. The public repository is a squashed single publish, so the commit count says nothing about the work in it. It runs on a machine in my house, and the people it was built for use it.