~/potatohd.ruRU
← All posts
Jul 12, 2026·9 min read

Concord: a Discord-compatible server built from scratch

GoDiscord APIWebRTCagents

Concord is a Discord-compatible server — a from-scratch implementation of the protocol Discord's own clients speak: REST API v9/v10 for requests, Gateway v10 for the realtime event stream, Voice Gateway v8 with an SFU (selective forwarding unit) for calls. The goal is byte-level compatibility: a server that answers the way the real Discord would, independent of Discord's own infrastructure. Concord has a sibling: an earlier platform speaking the Telegram wire protocol, built on the same stack — chi router, pgx driver, structured logging, Postgres and Valkey, the same deploy pipeline — sharing not one line of code.

#Three protocols, one server

The REST layer covers the ordinary ground — guilds, channels, messages, users — with v9 and v10 live side by side, because the real Discord keeps both alive at once. Wire compatibility runs down to Discord's quirks: Discord returns 200 for almost every successful call, including resource creation, and never touches 201 Created. Small detail. It comes back later.

Gateway v10 is the realtime half: one persistent connection per client, with events pushed the instant they happen, nothing polled for.

Voice runs on its own signaling channel, Voice Gateway v8, with audio routed through an SFU, a selective forwarding unit. An SFU is not a dumb packet relay: every inbound encrypted packet gets decrypted exactly once, held in plaintext just long enough to re-encrypt it separately for each recipient.

None of this matters unless a real client can talk to it. A handful of open-source Discord clients aren't protocol implementations at all — one popular one is just an Electron shell wrapped around Discord's website, gated by a hardcoded hostname allowlist, no protocol underneath. Once the protocol on the other end is real, pointing a client at a different, compatible server is a config change.

#Design decisions

Message storage is pluggable between Postgres and ScyllaDB, defaulting to Scylla — the reverse of the Telegram sibling, where Postgres is the default and the Scylla backend exists in code but was never deployed. That sibling also runs a separate ID-allocator service to keep message ordering consistent. Concord skips it: Discord's message IDs are snowflakes, a timestamp encoded into the ID itself, so sorting by ID already sorts by time — no coordination service required.

Login runs on OAuth2's Resource Owner Password Credentials grant, a direct username/password exchange for a token, skipping the usual redirect to an identity provider's login page and back. Discord's login endpoint requires exactly that shape: POST /auth/login is synchronous, taking credentials in the request body and handing back a token in the same response, no callback involved. ROPC is the one grant shaped like that exchange, even though it's normally discouraged for third-party clients — it hands the raw password to the client itself, a trust model most OAuth2 flows exist to avoid. A dev-mode flag bypasses the identity provider for local development and CI, with a fixed login and password.

Billing, ported over from the sibling's own client, fails closed. Unreachable or unconfigured billing leaves a user on empty, default subscription state. Premium access never gets granted silently.

#Five workstreams, one build

Five parallel AI coding agents each owned one vertical slice: auth and users, guilds, channels and messages on Scylla, the realtime gateway, voice and the SFU. The first run got killed mid-build by an unrelated interruption before a single file existed, so the second run started clean.

File ownership between the five barely overlapped. The only real friction was a scattering of naming collisions in a shared internal helper package, and that resolved itself — periodic build runs surfaced the conflicts, fixed in place, no manual coordination required.

#What the live stack found

Every package's unit tests were green well before anyone stood up the real stack — Postgres, Scylla, Valkey, api, gateway, voice, all running together, nothing mocked. End-to-end runs against it turned up seven bugs, and unit tests had caught precisely none of them: nothing for a mock to disagree with.

Two of the seven were the same mistake made twice: a Docker build-context path assuming the wrong root, once in a Dockerfile COPY instruction, once in a compose file's dockerfile: field. The same bug class the Telegram sibling had already documented — and it recurred anyway, in different code.

The status-code issue was the one foreshadowed earlier: Discord answers 200 for nearly every successful call, including resource creation, and never touches 201 Created. Nobody had pinned that down in writing, so all five independently-built slices guessed the textbook default. Nine call sites across two files came back wrong, invisible until end-to-end testing checked real responses against the real protocol.

The last one hid inside a single JOIN, where a column name happened to exist in both tables being joined. It compiled. It passed review. It worked against a mock. Run against a real schema, Postgres rejected the identical query outright as ambiguous, an ambiguity that exists only in a real SQL planner.

#The bug no unit test could see

On paper, voice was finished. The gateway package passed its tests. The voice package passed its tests. The build was clean, vet was clean, coverage looked like everything else in the codebase. All that seemed left was flipping it on.

Calls didn't work. Not intermittently, not under some specific edge case — nothing happened, from the first step to the last. When a client connects to a voice channel, the realtime gateway generates an access token for it to present to the separate voice service. Token generation worked fine. The token just never got saved anywhere.

The voice service's connection handler does exactly what it's supposed to: it takes the incoming token and looks for a matching row in a database table the gateway was supposed to write when it issued that token. There was no row. There had never been one — the write meant to create it didn't exist in the code at all.

Neither service's unit tests stood a chance of catching this, and it wasn't a testing-discipline problem. Gateway's tests mock everything that isn't gateway. Voice's tests mock everything that isn't voice. The bug lived in the untested seam between the two, the handoff no test suite could see, where each service simply trusted the other had honored its half of the contract. It surfaced only after someone asked directly whether voice actually worked end to end, all the way from clicking call to hearing sound. Green unit tests weren't a good enough answer to that question. The fix added the missing write at token-issue time, plus a regression test that now asserts the ordering explicitly: the row exists before the token ever reaches a client.

#The two-participant proof

The regression test fixed the ordering bug. It said nothing about whether the SFU actually forwarded audio correctly once two real participants were on a call together. No existing tool spoke this exact wire format, so a two-participant voice test client got built from scratch: raw websocket for signaling, UDP for the media, AES-256-GCM matching the server's exact byte layout.

Each participant holds a distinct encryption key, unrelated to the other's. Participant one sends a packet encrypted under their own key. The SFU receives it, decrypts it exactly once, and holds the plaintext in memory for no longer than forwarding requires. What reaches participant two comes back encrypted again, this time under participant two's key. Participant two decrypts it with that key and gets back audio identical, byte for byte, to what participant one originally sent.

That's the SFU's whole job, demonstrated directly: streams don't get mixed together, a sender's stream identifier survives the round trip intact, and nobody hears their own voice routed back to them. Small and mechanical to check. Also the only way to actually know a voice server built this way works at all.

#Where things stand

Locally: build, vet, and tests green across 14 packages, no data races. Against the live stack: 36 of 36 REST end-to-end tests, 6 of 6 gateway websocket tests, 11 of 11 two-participant voice tests.

Several corners stay unfinished. Discord's end-to-end voice encryption, the DAVE protocol, isn't built — the SFU still sees unencrypted media on its own side, with only transport-level encryption between client and server. Webhook execution is routed but stubbed to return "not implemented." Message search comes back empty; neither storage backend has a full-text index yet. The WebRTC voice path for browser and native clients still rejects every connection — the SFU's media-forwarding logic exists and is tested in isolation, just not wired into that path yet, pending a real client handshake capture to confirm the exact wire format it expects.

// available for hire

Need a website or setup, done right?