Concord: a Discord-compatible server, split into two planes
"Discord-compatible" sounds like marketing until you look at what the clients are made of. A notable share of open-source Discord desktop clients implement nothing at all: they're Electron shells that load the real Discord site in Chromium and allowlist their IPC bridge against a hardcoded host list. If a server answers the way the real one does, repointing such a client is a two-line change in a fork.
That's where Concord came from: a clean-room implementation of the Discord REST API (v9/v10), Gateway v10, and Voice Gateway v8 with its own SFU. Not a chat that resembles Discord — a server that other people's clients and bots connect to. What follows is how it's built inside, and the places where the protocol dictates the architecture.
#Two planes: control and media
The first and most expensive decision is splitting the system in two.
The reason is simple: they fail and scale differently.
- ▸REST is stateless. Replicas multiply trivially; a deploy is an ordinary container swap.
- ▸The gateway holds sockets. There's state, but it's rebuildable: the client reconnects and reassembles
READY. - ▸Voice holds media. That state is fundamentally unreplicable. You cannot roll a node carrying a live call without cutting it, however hard you try.
Fold all of this into one binary and the most frequent action — shipping a REST tweak — starts dropping the most sensitive thing you have. The split isn't about scale; it's about the blast radius of a deploy.
#How a change reaches a client
The second decision follows from the first: the REST plane knows nothing about sockets. It writes to storage and publishes to a bus; which gateway replica happens to hold the recipient's connection is none of its business.
Events are addressed by topic (guild.<id>, user.<id>, channel.<id>), never by connection id. That's the decision that lets gateway replicas come and go without anyone maintaining a registry of who sits where. The cost is that each replica receives events it may not need; at sane volumes that's cheaper than keeping a connection map correct.
#The small things a borrowed protocol dictates
The interesting part of a compatible implementation isn't the big blocks — it's the places where "whatever is convenient" isn't on the menu.
Identifiers don't fit in a JSON number. Discord snowflakes and permission bitfields passed 2⁵³ long ago, which is where JSON number precision ends. So on the wire they must be strings, or the client silently rounds them and starts confusing objects. Internally they're BIGINT: a snowflake is formally unsigned 64-bit, but signed holds until roughly 2084 — the same trade Discord itself made.
The real client IP can't be taken from a header on faith. The off-the-shelf middleware that rewrites the client address from X-Forwarded-For is trivially spoofable — the client sends that header. And since the login limiter counts attempts per IP, that turns into unlimited password guessing. So the header is honoured only from an explicitly configured trusted proxy, and read right to left. One config value standing in front of a whole class of attacks.
Voice cannot travel through Traefik. Traefik is an HTTP proxy; RTP is raw UDP. The media node publishes its port range directly and advertises its public address in ICE candidates. A separate trap: put a container-internal address there and the service starts with no error in the logs at all — while voice is broken for every external client.
Login has to be synchronous. The modern way in is a redirect to an identity provider. But the Discord contract is a plain POST /auth/login that answers with a token in the response body; there's nowhere to redirect, the client isn't ours, and rewriting it isn't an option. So it's a bridge: the password is verified against ZITADEL (the password grant — precisely the case it stayed legal for), while Concord mints its own session token. There is no password column in Concord's database at all.
| Protocol constraint | What it forces |
|---|---|
| JSON numbers lose precision past 2⁵³ | IDs and permissions as strings on the wire, BIGINT inside |
| The client can't be rewritten | synchronous login via password verification at the IdP |
| RTP is UDP | media node around the reverse proxy, public IP in ICE |
| Client dies on an unexpected status while booting | valid empty payloads on the boot path, not a 501 |
That last row is the honesty-versus-usability balance. Where it's safe, an unimplemented endpoint answers 501: the client learns the call didn't take effect instead of believing it did. But during boot the web client treats an unexpected status as fatal and simply refuses to mount — there, a well-formed empty response is the only option.
#The rate limiter's limitation — and why it's accepted
Rate limits in Concord are counted per replica. With N replicas the effective limit is N times higher. That's not a forgotten task but a deliberate trade: the limits exist so clients behave and an accidental loop doesn't take the service down — not as a quota anyone is selling. An exact global counter costs a Redis round-trip per request, and at this load the price exceeds the value.
Trades like this are worth writing down in the project's own docs. An unrecorded compromise turns, six months later, into a bug someone "fixes" at the cost of doubled latency.
#What Concord doesn't have
Voice is not end-to-end encrypted. Discord's DAVE isn't implemented; the SFU terminates SRTP and can see the media. The transport is encrypted, end-to-end is not. That's exactly the sort of thing it's tempting to phrase vaguely, so it sits in the README as its own line: do not describe Concord voice as E2EE.
Also missing: webhook execution, application interactions and slash commands, discovery and monetisation. All of it answers 501 or empty stubs, per the rule above.
#What to take from this
- ▸Split by deploy blast radius, not by layer aesthetics: only the parts you're happy to restart together belong together.
- ▸Address events by logical topic rather than connection, and replicas become interchangeable for free.
- ▸A borrowed protocol is a set of constraints, not preferences. Each one is worth writing down next to what it forced you to do.
- ▸Say plainly what isn't there. A vague sentence about encryption isn't caution — it's a promise the system doesn't keep.