Why we left Discord

My friends and I left Discord over a driver’s licence. Not anybody’s specifically — mine, hypothetically, eventually, after enough policy ratchets. The announcement that started it landed late in 2025: to verify ages for “age-appropriate experiences,” Discord would begin asking users in certain jurisdictions to upload government identification. A third-party verification provider would handle the documents. They wouldn’t be stored long-term. The comms team picked the warmest possible verbs.

I appreciate the offer of having my licence sealed behind thinly-secured infrastructure for the sake of age-appropriate experiences as much as the next guy. Nobody in the group did either. The disagreement wasn’t about whether the policy was reasonable on the day it shipped. It was about the shape of the pipeline you build to enforce it.

Once a private company has a workflow for “send us a government ID,” the cost of expanding it is zero. The cost of removing it is infinite. The next regulatory ratchet asks for a slightly bigger version of the same thing. The integration partner you’ve never heard of gets breached and the notification email arrives eighteen months later. The licence you uploaded for a chat app turns up in a credential dump next to your address. The probability of any individual step is small. The probability of some step over five years isn’t.

We left.

The hunt

The open-source answer to Discord is, depending on who you ask, Matrix, Matrix, or Matrix. Federated protocol, end-to-end encryption in the spec, several independent homeservers, several independent clients. You pick a homeserver, you pick a client, federation handles your friends being elsewhere.

For the homeserver we landed on Continuwuity — the community continuation of Conduit, the lightweight Rust homeserver. Single binary, sqlite-backed, modest footprint. The right answer when you don’t want to operate Synapse and its Postgres habit on a side-project budget.

For the client, the obvious answer was Element. Element is the reference Matrix client, made by the people who made Matrix. It’s mature, cross-platform, and handles every corner of the spec. For most people it is exactly the right thing.

It wasn’t the right thing for us.

What Element wasn’t shaped like

Element is solving a more general problem than we had. The mental model is rooms: text and voice are both rooms, you navigate between them, they’re consistent objects in a consistent list. That’s coherent. It also isn’t how we use a chat app.

We came from Discord. The mental model there is a server: a sidebar with voice channels at the top, text channels under them, a chat panel, a member list. You see who’s in voice without joining. You drop into voice mid-message. Voice isn’t a button you press; it’s a room you’re either sitting in or you aren’t.

Element’s voice goes through Element Call, which is excellent technology — the same WebRTC/LiveKit stack we eventually built on. But the UX is “start a call,” not “spend a Sunday hanging out in a voice room.” For a group that lives in voice for hours at a stretch those are different products.

The other things were smaller. No soundboard. A GIF picker that didn’t match our muscle memory. No ambient presence — who’s online, who’s in which voice channel, who’s muted, who’s screen-sharing — visible all in one sidebar. Discord shows you all of that without you asking, and that ambient awareness is the difference between feeling like you’re in a room with your friends and feeling like you’re sending emails.

None of this is a failure of Element. Element is the right tool for the general case. The specific case — a Discord-shaped client for a small group of friends on a private homeserver — was never going to be a few patches against the general one.

Three options, one we picked

Forking Element didn’t survive thirty seconds of thinking about it. Element is React on top of a deeply factored matrix-js-sdk integration, and the parts we’d have rewritten were the load-bearing parts. A permanent divergence from a project that ships fast is a bad ratio.

The other Matrix clients — Cinny, Fractal, FluffyChat, SchildiChat — are all good. None of them was shaped the way we wanted. The fork-overhead argument applied to each.

Writing our own was the option I assumed I’d talk myself out of and didn’t. Flutter handles “native desktop app, mobile later, one codebase” well enough. The Matrix Dart SDK is actively maintained. The LiveKit Flutter SDK exists. Matrix is moving toward LiveKit-backed voice via MSC4143 regardless. The pieces were sitting there waiting.

The project is called Havok. v0.1 ships this month.

What v0.1 is

A Discord-style four-panel layout. Server list on the far left, channel list per server with text and voice channels intermixed by category, chat in the middle, member list on the right. Voice channels show their occupants inline. You can see who’s where without clicking into anything.

Voice rooms you can sit in. A voice channel is a Matrix room with type: io.element.video. You drop in, the room state updates, other clients see you. Per-user volume sliders. Local mute-others — mute somebody for yourself without muting them for the room. Deafen. Noise gate. Built on LiveKit, with the homeserver advertising its service URL through .well-known/matrix/client.

A soundboard. Upload short clips, press them in voice, everyone hears them. Receiver-side ffmpeg so any common audio format works. Rate-limited and capped at the receiver so it can’t be turned into a denial-of-service tool.

End-to-end encryption is on by default. The Matrix Dart SDK does the work; we just have to not break it. Threaded replies. Screen share with explicit per-stream subscription, so receivers opt in instead of auto-pulling every share in a busy channel. A GIF picker, because of course.

What’s not in v0.1: mobile builds. The Flutter scaffolding for Android and iOS is in the repo, but those targets aren’t tested for public use yet. v0.2 problem.

What the open protocol actually buys you

The thing that keeps catching me off guard, coming from Discord: when a feature doesn’t exist, you write it. The protocol is open. The homeserver is yours. The state event for “user X is in voice channel Y right now” is documented in MSC3401. When Havok needed to broadcast voice presence to other clients, that wasn’t a feature request on somebody else’s roadmap. It was a state event we wrote.

It’s the same feeling as the first time you self-host email and realize “I want this forwarded to my pager” is a sieve filter, not a customer support ticket. The control isn’t theoretical.

Where it goes

v0.1 is the smallest thing I’m willing to put a name on. We use it daily. Install is curl | bash on Linux and an install.ps1 on Windows; building from source needs a working Flutter toolchain. The two forked dependencies — a patched flutter_webrtc and a patched livekit_client — are pinned to specific commits so builds are reproducible.

The next things, roughly in order: Android, then iOS. Better message search. Custom emoji parity with what Discord refugees expect. A second pass on the soundboard UI — the abuse limits underneath it are solid, the surface is rough.

If you want to try it: github.com/HamzaKarh/Havok. If you want the homeserver to go with it, Continuwuity is the pairing.

We left Discord because we didn’t want our IDs sitting in someone else’s database waiting for the breach notification email. We stayed gone because what we built fits better than what we left.

← all posts