Guide 3 of 9

How a message travels

From keystroke to delivery: ECDH, HKDF, ChaCha20-Poly1305, the envelope, the pool, the flood, the ticks.

3

How a message travels

Every message follows the same path from your device to the recipient's, and every step of that path is either cryptographically closed or openly disclosed as metadata - nothing in between.

Encrypt on device

Your chat private key and the recipient's chat public key run through static-static ECDH on secp256k1, the shared secret is stretched with HKDF-SHA256, and the plaintext is sealed with ChaCha20-Poly1305 under a fresh 96-bit nonce.

Assemble the envelope

The ciphertext, nonce and auth tag are packaged with the sender details into the wire envelope, and that whole envelope is then sealed a second time under an ephemeral key generated fresh for this one message.

Stream to a service node

The envelope goes out over an authenticated, bidirectional gRPC stream to whichever service node your device is connected to.

Persist locally, then flood

That node writes the ciphertext to its own LevelDB pool, delivers it immediately to any of the recipient's devices with a live stream open, and forwards it to every peer node it is connected to.

Drain or expire

An offline recipient's devices pick the message up from any peer's pool once they reconnect, up until the retention window closes.

The honest parts matter as much as the mechanism. A regular message is retained for up to 14 days undelivered; configuration messages (key material) get 30 days. Each account is capped at 10,000 stored messages per relay, oldest evicted first, with an hourly cleanup sweep. Replication is a genuine flood, not a routed swarm: a message is copied to every connected peer node rather than assigned to a specific subset, and there is no sharding, no consistent hashing and no route healing - describing this as onion routing, a mixnet, or a selective mesh would be wrong. Pending sends retry to unconfirmed peers for up to 30 minutes, once a minute, and a reconnecting device gets its full backlog flushed to it; duplicate delivery is suppressed by a 45-minute in-memory seen-set. Ephemeral traffic - call signalling, typing indicators, presence - is never written to a pool at all, only best-effort fanned out.

Only the ciphertext field is confidential. sender_did senderPublicKey chat_id keyGeneration and encryption_modeare sealed inside the outer envelope, so a relay carries a value that looks random and changes with every message rather than anything that identifies the sender.

EncryptedPayloadEnvelope, wire JSON {
  "ciphertext":     "<base64>",
  "iv":            "<base64, 12 bytes>",
  "authTag":       "<base64, 16 bytes>",
  "senderPublicKey": "<hex, 33-byte compressed>",  // sealed
  "keyGeneration":   <int>,  // sealed
  "chat_id":       "<string>",  // sealed
  "encryption_mode": "ecdh" | "sender_key" | "ecdh_bootstrap" | "none",  // sealed
  "sender_did":      "<string>"  // sealed
}

The ticks track a precise, small state machine: Sent (one tick) means a relay accepted the envelope; Delivered (two grey ticks) means the recipient's device pulled and decrypted it; Read (two blue ticks) means the recipient opened it. Two further internal states exist - Received and Retrying - for delivery bookkeeping the UI does not surface directly. Read and delivered receipts are not a side channel: they are ordinary encrypted messages sent back to the original sender, batched per sender as a high-water-mark receipt, the same pattern as Telegram's readHistory(max_id).

Related: FAQ - what do the ticks mean? · Spec §3 - direct messages