Design an end-to-end encrypted messaging backend
Design the backend for a messenger where the server cannot read messages: prekey distribution for X3DH and PQXDH, what the double ratchet means for the server, multi-device fan-out, groups, metadata minimisation with sealed sender, key transparency, and the hard trade-off of abuse reporting.
01Understand the problem and size it
- Explain X3DH/PQXDH and the double ratchet at the level a backend designer needs: what the server stores and what it must never see
- Design the key distribution service: prekey bundles, one-time prekey consumption, replenishment
- Handle multi-device and groups with client-side fan-out or sender keys
- Minimise metadata (sealed sender, no stored social graph) and reason about abuse reporting without breaking encryption
What they probe: what the server stores, what happens when one-time prekeys run out, how a second device gets messages, how a malicious server could swap keys and how users would notice, what metadata remains, how abuse is reported if the server sees nothing. Where candidates slip: encrypting in transit and calling it end-to-end, the server generating keys, ignoring multi-device, "we just won't log" as the metadata story.
End-to-end encryption (E2EE) means only the communicating devices hold the keys; the server relays ciphertext it cannot decrypt. This changes the backend's job: it becomes a key directory, a store-and-forward queue and a push notifier, and its security goal shifts to not learning metadata and not being able to impersonate users.
Functional requirements
- Users send 1:1 and group messages and media; recipients get them when they come online.
- Each user may have several devices (phone plus desktop), all receiving all messages.
- The server cannot read content and cannot silently substitute keys.
- Users can report abuse and block senders.
Non-functional requirements
- 100 M daily active users, 50 messages sent each per day.
- Delivery < 500 ms p99 when the recipient is online.
- Offline messages held up to 30 days, deleted on acknowledgement.
- Store and log the minimum metadata needed to operate.
Back of the envelope
messages sent 100 M x 50 = 5 B / day ≈ 57,900 / s, ~175k / s peak (x3)
device fan-out 1.5 devices per user -> 7.5 B deliveries / day
envelope size ~1 kB ciphertext (media goes to a blob store, key travels in the message)
relay bandwidth 175k x 1.5 x 1 kB ≈ 260 MB/s at peak
persistent connections 30 M concurrent, 100k per gateway node -> 300 nodes (+ headroom)
prekey storage (classical)
150 M devices x 100 one-time prekeys x 32 B = 480 GB
prekey storage (post-quantum, ML-KEM-1024 public key 1,568 B)
150 M devices x 10 one-time PQ prekeys x 1,568 B ≈ 2.35 TB <- why PQ prekey counts are kept small02The protocol, from the server's point of view
X3DH and PQXDH: starting a session with someone offline
Alice wants to message Bob, who is offline. Bob has uploaded a prekey bundle in advance: his long-term identity key (IK), a signed prekey (SPK, rotated every week or so, signed by IK), and a batch of one-time prekeys (OPK). Alice fetches the bundle, verifies the SPK signature, generates an ephemeral key (EK), and computes several Diffie-Hellman (DH) values:
DH1 = DH(IK_A, SPK_B) mutual authentication (Alice's identity)
DH2 = DH(EK_A, IK_B) mutual authentication (Bob's identity)
DH3 = DH(EK_A, SPK_B) forward secrecy
DH4 = DH(EK_A, OPK_B) only if a one-time prekey was available: replay protection, better forward secrecy
SK = KDF(DH1 || DH2 || DH3 || DH4) X3DH
PQXDH adds a post-quantum KEM (ML-KEM / Kyber) prekey PQPK_B, signed by IK_B:
(CT, SS) = KEM.Encaps(PQPK_B) Alice sends CT in her first message
SK = KDF(DH1 || DH2 || DH3 [|| DH4] || SS) PQXDH: harvest-now-decrypt-later resistant
Server stores: IK_B (public), SPK_B + signature, OPK_B[], PQPK_B + signature, PQ one-time keys
Server never sees: any private key, EK_A private, SK, message keysDouble ratchet: why the server does not care, mostly
After setup, each message uses a fresh key from a symmetric ratchet (a KDF chain), giving forward secrecy: stealing today's keys does not decrypt yesterday's messages. Each time the conversation changes direction, a new DH exchange advances a DH ratchet, giving post-compromise security: after a key theft, the session heals once a fresh DH happens. For the backend this means: messages carry a ratchet header (sender's current DH public key, message number), clients keep a bounded cache of skipped message keys for out-of-order delivery (for example at most 1,000), and the server must deliver in order per device where it can, but never needs keys.
Key distribution service
PUT /v1/keys # device uploads, authenticated as account+device
{ identityKey, signedPreKey: { id, pub, sig }, pqLastResort: { id, pub, sig },
oneTimePreKeys: [{ id, pub } x 100], pqOneTimePreKeys: [{ id, pub, sig } x 10] }
GET /v1/keys/{account}/{deviceId} # returns ONE bundle; atomically removes the OPK it hands out
-> { identityKey, signedPreKey, oneTimePreKey?, pqPreKey } # pqPreKey: one-time if left, else last-resort
GET /v1/keys/count # device checks stock; refills when < 10 remain
PUT /v1/messages/{account} # per-device ciphertexts, see multi-device
{ messages: [{ deviceId, registrationId, type, body }] }- Atomic consumption: each one-time prekey must be handed out once. Use a per-device queue with an atomic pop (a conditional delete in the database), so two concurrent senders never get the same OPK.
- Exhaustion: if a device's OPKs run out (offline for a long time, or someone draining them), the server returns the bundle without an OPK; the session still works on DH1 to DH3, with weaker replay protection for the first message. Rate limit bundle fetches per requester to stop deliberate draining.
- The server is not trusted with keys, only with storing public ones honestly. That is exactly what key transparency checks.
03Deep dives: multi-device, groups, metadata, key transparency
Multi-device
Each device has its own prekeys and its own sessions. To send, Alice's client fetches Bob's device list, and encrypts the message separately for each of Bob's devices and each of Alice's own other devices (so her desktop sees what her phone sent). The server checks the submitted device set against the current list and rejects with 409 stale devices if it is out of date, so the client refreshes and retries. To stop the server silently adding a device of its own, the device list is signed by the account's identity key (or a primary device) and covered by key transparency, and users are notified when a device is added.
Groups
| Approach | How | Cost per message | Notes |
|---|---|---|---|
| Pairwise fan-out | Encrypt the message for every member device over 1:1 sessions | O(members x devices) | Simple, fine for small groups |
| Sender keys | Each member sends a sender key to every other member once; messages are encrypted once with it | O(1) encrypt, server fans out | Membership change means new sender keys; weaker post-compromise security |
| MLS (RFC 9420) | A ratchet tree shared by the group | O(log n) per membership change | Designed for large groups, standardised |
Metadata minimisation
- Sealed sender: the envelope's sender identity is encrypted to the recipient, so the server sees only the destination. The recipient's client verifies a short-lived sender certificate inside, issued by the server, so it knows the sender is a real account. To limit spam, sealed-sender delivery requires a delivery token derived from the recipient's profile key, which only contacts have.
- No stored social graph: contact discovery through hashed lookups inside a secure enclave or with private set intersection, not by uploading address books in the clear; group membership stored encrypted, with zero-knowledge credentials proving membership.
- Logs: no IP-to-account logs beyond what abuse prevention needs, short retention, and drop delivery receipts from server logs.
- What still leaks: the recipient, timing, size (pad ciphertexts to buckets), IP at connection time, and the fact that you use the service.
Key transparency
A malicious or compelled server could hand Alice a fake identity key for Bob and sit in the middle. Safety numbers (a fingerprint users compare in person or by QR code) catch this but few people check. Key transparency publishes every account-to-key mapping in an append-only, verifiable log (a Merkle tree, as in WhatsApp's Auditable Key Directory); clients check that the key they received is the one in the log and that their own entry has not been changed, and independent auditors check the log is append-only.
If clients accept whatever identity key the server returns and never notice changes, the server can read messages by swapping keys. Show key-change notices, support safety numbers, and add key transparency so substitution leaves public evidence.
04Abuse reporting, failure modes and trade-offs
Abuse reporting when the server cannot read
- User reports: the reporting user's client, which has the plaintext, forwards the reported messages (and recent context) to the trust and safety team. The reporter is choosing to share what they received, which does not break E2EE for anyone else.
- Message franking: the sender's client includes a cryptographic commitment to the message that the server records (without learning content). On report, the commitment proves the reported plaintext really was sent by that sender, so reports cannot be forged.
- Metadata-based signals: account age, message rate, fan-out to non-contacts, number of blocks and reports. These catch spam without content.
- Client-side scanning (matching content against hashes on the device before encryption) is the contested option: it re-introduces a content inspection point and a list nobody can audit. Name the trade-off rather than pick it silently.
Failure modes
| Failure | Behaviour | Why |
|---|---|---|
| Device offline > 30 days | Queue expires; sender sees undelivered; sessions may need reset | Storage is bounded |
| One-time prekeys exhausted | Bundle without OPK; client refills on next connect | Graceful, slightly weaker first message |
| Gateway node dies | Clients reconnect to another; undelivered messages stay in the durable queue | Queue, not connection, is the source of truth |
| Ciphertext fails to decrypt | Client requests a session reset; server just relays the reset message | Server cannot help with crypto state |
| Push provider (APNs/FCM) down | Messages queue; clients fetch on next open | Push carries no content anyway, just a wake-up |
Trade-offs to state
| Choice | Gain | Cost |
|---|---|---|
| Client-side fan-out for multi-device | Server never holds shared keys | Sending cost scales with devices; stale-device retries |
| Sealed sender | Server does not learn who talks to whom from envelopes | Harder spam control; IP and timing still correlate |
| Server-side backups (encrypted with a user PIN via an enclave) | Users keep history across phones | PIN strength and enclave trust become part of the security |
| User-initiated reporting only | No content inspection point | Abuse in private chats is found later and less often |
Never put message content in APNs or FCM payloads; they pass through Apple's and Google's servers. Send an empty wake-up and let the app fetch and decrypt.
05Final design, follow-ups and self-test
"The server is a key directory and a store-and-forward relay. Devices upload identity keys, signed prekeys and batches of one-time prekeys, including post-quantum ones for PQXDH; the directory hands each one-time key out exactly once with an atomic pop. Senders run the handshake and the double ratchet on the client, encrypt separately for every recipient device and their own other devices, and the server checks the device list is current. It can't read content, and to stop it swapping keys, device lists are signed and every key mapping goes in a transparency log. Sealed sender hides who sent what, and abuse is handled through user reports with message franking plus metadata signals, which I'd call out as a deliberate trade-off."
Q1What exactly does the server learn when Alice messages Bob with sealed sender on?+
Bob's identity as the destination, the time, the padded size, and the IP Alice connected from. Not Alice's identity from the envelope, though IP and timing correlation can reveal it to a determined server.
Q2Why rotate the signed prekey but use one-time prekeys too?+
One-time prekeys give each new session unique key material and replay protection for the first message. The signed prekey is the fallback when they run out, and rotating it limits exposure if one leaks.
Q3How would you add encrypted message backup?+
Client-encrypted backups with a key derived from a high-entropy recovery code, or from a PIN protected by an enclave with attempt limits. Never a server-held key.
Q4How do you migrate to post-quantum without breaking old clients?+
Clients advertise capability; bundles include PQ prekeys; new sessions use PQXDH when both sides support it and X3DH otherwise; eventually require it.
Two senders request Bob's prekey bundle at the same moment. Why must the directory pop one-time prekeys atomically?