Skip to content

Mailboxes

A Mailbox is where a device receives envelopes for one group. Every operation here proves ownership of a mailbox secret; none of them identifies as a device, and the server never learns that two mailboxes belong to the same one.

DeliveryAddress Structure
struct DeliveryAddress {
// Base32 identifier, 24 chars, derived from mailbox_public
prefix: String,
// Server domain (e.g., "chat.example.com")
server: String,
// Senders encrypt the outer envelope layer to this key. Reaches contacts
// inside MLS via AddressRotation, never via server.
mailbox_public: X25519PublicKey,
// Timestamp of when the address was generated
created_at: u64,
// Kept live through the rotation overlap window, so a contact who missed an
// AddressRotation still reaches a real mailbox
active: bool,
}
impl DeliveryAddress {
fn full_address(&self) -> String {
format!("{}@{}", self.prefix, self.server)
}
// Anyone holding the address can check if the prefix binds to the key
fn verify_binding(&self) -> bool {
let hash = blake3::hash(self.mailbox_public.as_bytes());
self.prefix == base32::encode(&hash.as_bytes()[..15])
}
}
MailboxProof Structure
/// Proof of mailbox ownership, produced by signing a server-issued nonce
/// with the mailbox secret. Used at registration and at retrieval; the
/// server verifies it against the address prefix and nothing else
struct MailboxProof {
// The mailbox this proves ownership of. The server checks that the
// address prefix derives from it.
mailbox_public: X25519PublicKey,
// Signature over the server's challenge nonce. Never a static value:
// a fixed signature would be replayable by anyone who saw it once.
signature: Ed25519Signature,
}

Obtains a nonce to sign. Used before registration and before opening a retrieval session.

  • Input: ChallengeRequest { prefix }
  • Output: MailboxChallenge { nonce, expires_at }
  • Auth: None
  • Idempotent: No
  • Errors: MALFORMED_REQUEST
  • Servers MUST issue a fresh, unpredictable nonce per attempt
  • Servers MUST expire a challenge quickly; 60 seconds is the default
  • Servers MUST NOT reuse a nonce across attempts
  • Servers MUST NOT reveal whether prefix is already registered
POST /api/v1/mailboxes/challenge

Creates a mailbox registration, or extends an existing one.

  • Input: RegistrationRequest { prefix, proof }
  • Output: Registration { prefix, expires_at }
  • Auth: PrivacyPass, Mailbox Ownership
  • Idempotent: By prefix - re-registering extends the registration
  • Errors: TOKEN_INVALID, TOKEN_REPLAYED, BUDGET_EXHAUSTED, MAILBOX_EXISTS, BINDING_INVALID, CHALLENGE_EXPIRED, CHALLENGE_INVALID
  • Registration MUST spend one token
  • Servers MUST verify that prefix derives from proof.mailbox_public
  • Servers MUST verify proof.signature against the challenge nonce
  • Servers MUST NOT accept a static signature in place of challenge-response
  • Servers MUST reject a prefix already registered to a different mailbox_public
  • Servers MUST treat a request under the same mailbox_public as a renewal and extend expires_at
  • Registrations MUST expire; 24 hours is the default
  • Servers MUST NOT record anything relating one registration to another
POST /api/v1/mailboxes

Renewal is essentially RegisterMailbox with the same prefix and a fresh proof. There is no separate operation: one registration path is easier to reason about than two, and the token cost is what bounds mailbox count.

A device in ten groups spends ten tokens a day keeping its mailboxes alive, against 12,000 issued daily at Established tier. The cost is not the point of the charge; uniformity is.

Exchanges a proof for a short-lived session authorizing retrieval from one mailbox.

  • Input: SessionRequest { prefix, proof }
  • Output: Session { token, expires_at }
  • Auth: Mailbox ownership
  • Idempotent: No
  • Errors: MAILBOX_UNKNOWN, BINDING_INVALID, CHALLENGE_EXPIRED, CHALLENGE_INVALID
  • A session MUST be scoped to exactly one mailbox
  • Servers MUST NOT accept one session as authorization for another mailbox
  • Servers MUST NOT retain any record relating two sessions to each other
  • Sessions MUST be short-lived; one hour is the default
  • Clients MUST establish sessions independently per mailbox
  • Retrieval spends no token
POST /api/v1/mailboxes/{prefix}/session

Retires a mailbox before its registration expires.

  • Input: DeregisterRequest { prefix, proof }
  • Output: None
  • Auth: Mailbox ownership
  • Idempotent: Yes
  • Errors: MAILBOX_UNKNOWN, BINDING_INVALID, CHALLENGE_EXPIRED, CHALLENGE_INVALID
  • Servers MUST require a fresh proof; deregistration is destructive
  • Servers MUST discard queued envelopes for the mailbox
  • Deregistration spends no token - it frees resources rather than consuming them
  • Servers MUST return success for a prefix that is already gone
DELETE /api/v1/mailboxes/{prefix}

Rotation is not a server operation. A device derives a fresh keypair, registers the new mailbox, broadcasts an AddressRotation inside MLS, and lets the old mailbox lapse after MAILBOX_ROTATION_OVERLAP.

The server sees two unrelated registrations and one expiry. It cannot tell a rotation from an unrelated device arriving and another leaving, which is the point.

  • The previous mailbox MUST stay registered for the overlap window, which means renewing it
  • Devices MUST NOT renew the previous mailbox once the overlap closes
  • A member who missed the AddressRotation recovers via AddressBookSnapshot