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 routing fields - sender DID, sender public key, chat id, key generation, encryption mode - into the wire envelope.

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_mode all travel in the clear and are visible to any relay a message passes through.

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

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