What our servers can see — and what they can't
Five things you can count on. Each one is checked against the running code; the detail behind every claim is further down the page.
We don't keep logs.
Most services write down every connection. We don't. Here is exactly what that means.
- No access logs. Our web servers write no access log. Nothing records your IP address, your browser or which pages you requested.
- No connection history. There is no record of who connected, from where, or from which device.
- No mail-transport logs. Mail that crosses to or from the open internet is handed over and forgotten. We keep no log of who it was from or to.
The one thing derived from your IP address is a rate-limit counter: a keyed hash of the address, kept in memory for at most an hour so nobody can flood sign-up. It cannot be turned back into the address. What the service must hold to run at all — your username, the encrypted rows of your mailbox and their timestamps — is listed item by item below.
What we can and cannot see
Every data item, sorted into five tiers. "Where" names the actual store; "how long" is the lifetime of the value in that store.
- 1Never leaves your device
- 2Ciphertext only
- 3Transient, then gone
- 4Hash only
- 5Stored in the clear
| Data item | Tier | Where it lives | How long | Why |
|---|---|---|---|---|
| Message content | ||||
| Message bodies — mail and chat | 2 Ciphertext | Database, content column, PGP-encrypted on your device | Until you delete it, or the self-destruct timer fires | It is your mailbox; it has to persist somewhere |
| Attachments, voice messages, inline images | 2 Ciphertext | Inside the encrypted body, as MIME | With the message | Same blob as the body |
| Attachment names, counts and types | 2 Ciphertext | Inside the encrypted body and encrypted headers | With the message | Not needed for delivery |
| Subject lines of internal mail | 2 Ciphertext | Encrypted headers blob, AES on your device | With the message | Not needed for delivery |
| Sender line on a delivered copy ("name, address") | 3 Transient → 2 | Read in the request, encrypted to the recipient's public key, then stored as ciphertext | Clear for one request | The recipient's client has to know who wrote |
| Reactions (emoji, and which message they refer to) | 2 Ciphertext | One encrypted row per participant; the two rows share no value | Until withdrawn | Delivered like a message |
| Unsend / destroy instructions | 2 Ciphertext | Encrypted instruction row for the peer; deleted once applied. The kind ("unsent" or "destroyed") is in the clear | Until the peer's app applies it | The servers must stamp the kind onto the peer's row |
| Email signatures | 2 Ciphertext | Database, AES-encrypted on your device | Until you delete them | Yours to sync across devices |
| Drafts | 2 Ciphertext | Database, same encryption as sent mail | Until you delete them | Sync across devices |
| Message size (ciphertext length) | 5 Clear | Database, used_bytes | With the message | Storage accounting; preload limit |
| Addressing — who talks to whom | ||||
| Recipient addresses on an internal send | 3 Transient | The send request, in memory. Resolved to mailboxes, one encrypted copy written per recipient, and the list is not stored on any row | One request | Delivery needs a destination |
| Recipient addresses while undo-send is open (mail only) | 5 Clear, short-lived | Job-queue table, so the send can fire or be cancelled. Cancelling deletes the job; a fired job is disabled in place. Chat is never queued | Your undo window, 0–60 s (default 10 s) | Undo-send needs the request kept until it fires |
| Who you correspond with (peer identity on a conversation) | 4 Keyed hash | Database, derived_peer_email: HMAC of the peer's address under a key derived from your master key. The peer's row uses their key, so the two never match | Life of the conversation | Your app needs to find the right thread |
| Group roster — name, members, admin | 2 Ciphertext | One encrypted, signed state row per member; each encrypted separately, so no two rows share a blob. Group id is an HMAC under each member's own key | Life of the group | A member who reinstalls has to read the group back |
| Number of recipients at send time | 3 Transient | The recipient list in the send request | One request | Delivery |
| Message ids and reply chains | 4 Keyed hash | derived_message_id: HMAC of the Message-ID under your key. In-Reply-To is read once to pick the thread and not stored | With the message | Threading inside your own mailbox |
| Thread grouping inside your account | 5 Clear | Database: which of your rows belong to one thread, and the newest row per folder | With the thread | Fast mailbox listing |
| Autocomplete history of people you wrote to | 2 Ciphertext | Encrypted blob per address, keyed by an HMAC under your key | Until removed | Sync across devices |
| Timing and state | ||||
| Timestamps on your rows | 5 Clear | Database. Each recipient's copy is shifted by an independent random offset of up to 1 s (up to 60 s on group state), so copies of one send never share a value | With the row | Sorting your mailbox; expiring timers |
| Read and starred state | 5 Clear | Database, two booleans on your own row. There are no read receipts, so nothing is sent to the other side | With the message | Unread counts and sync |
| Delivery state of an outgoing mail | 5 Clear | mailed_at, scheduled_at on your row | With the message | Undo-send and the "sending" indicator |
| Self-destruct timer on a chat | 5 Clear | Duration and mode, one row per participant; who set it is encrypted | Until changed | The servers run the sweep, so they must read the deadline |
| Self-destruct deadline on a message | 5 Clear | self_destruct_at, with the recipient's copy pushed later by a random few seconds | Until the sweep, every minute | Server-side deletion |
| Typing indicators | 3 Transient | Relayed over the WebSocket; never written | Milliseconds | Live UI |
| Online presence | 3 Transient | Redis flag with a 100 s expiry, refreshed by the app's keepalive | 100 s after your last ping | Decides whether to send a push instead of a socket message |
| External mail (ordinary SMTP) | ||||
| Incoming mail from outside: headers, sender, body | 3 Transient → 2 | Readable on arrival. The whole raw message, the header set and the sender line are each encrypted to your key at once and stored as ciphertext. The importer writes nothing to disk in the clear | Clear for one import | SMTP delivers plaintext; we cannot change what the sender's provider did |
| Envelope addresses of mail crossing to or from the internet | 3 Transient | The mail transport, in memory while the message is handed over. No transport log is kept | One hand-over | The other provider needs a sender and a recipient |
| Outgoing mail to outside addresses: To, Cc, Bcc, Subject | 3 Transient | Set by the servers as SMTP headers and handed to the mail transport; not stored (except during an undo window, above) | One request | The other provider needs them |
| Outgoing external body | 3 Transient, or 2 if you imported their key | Passes through once. Domains and IP addresses found in an unencrypted outgoing mail are looked up against DNS blocklists; ciphertext is never scanned | One request | Delivery; outbound spam prevention |
| Imported public keys of outside contacts | 2 Ciphertext | Key and its metadata, AES-encrypted on your device; the address is an HMAC under your key | Until you remove them | Sync across devices |
| Keys and credentials | ||||
| Passphrase | 1 Never sent | Your device only | — | — |
| Private key | 2 Ciphertext | Database, encrypted under a key derived from your passphrase, on your device | Life of the account | So a new device can fetch it and unlock it locally |
| Master key (AES and HMAC keys) | 2 Ciphertext | Database, encrypted to your key | Life of the account | Same reason |
| Public key | 5 Clear | Database | Life of the account | Public by definition; others must encrypt to it |
| Password | 1 Never sent | OPAQUE: your device proves knowledge without transmitting it. We store an OPAQUE record that cannot be reversed | Record: life of the account | Sign-in |
| Sign-in handshake state | 3 Transient | Redis, 10 s expiry | 10 s | OPAQUE is a two-step protocol |
| Session tokens and DPoP proofs | 3 Transient | Tokens are stateless and bound to a key on your device; each proof id is remembered in Redis for 14 s to refuse replays. Rotated refresh tokens are blacklisted until expiry | Access 30 min, refresh 90 days | Authentication |
| Sign-in and token-refresh records | 5 Clear | Database: token id, issue time and expiry per account, so a replayed or revoked refresh token can be refused | Until the token expires, then pruned | Session revocation and replay protection |
| Two-factor (TOTP) seed | 5 Stored, server-key encrypted | Database, AES-256-GCM under a key kept outside the database. A dump alone cannot mint codes; the running service can | Until you turn 2FA off | The servers verify codes before your key is unlocked |
| Quick-unlock half key (PIN and biometric mode) | 5 Stored, server-key encrypted | Database, same scheme; handed back only to the device whose DPoP key created the session | Session expiry; swept daily | Your device combines it with its own half |
| Recovery address | 4 Salted hash | Argon2 hash in the database. In the clear only in Redis, for the 15-minute verification window plus 5 minutes after use, then expired | Hash: until you remove it | To check the address you type during recovery |
| Sign-in email (email-code accounts) | 4 Keyed hash | HMAC-SHA256 under a server secret | Life of the account | Look you up at sign-in without storing the address |
| WebSocket ticket | 3 Transient | Redis; PGP-signed by your key; deleted on use, 10 s life | 10 s | Authenticate the live connection |
| Account, devices and invitations | ||||
| Username, display name, account creation date | 5 Clear | Database | Life of the account | Your address is your username; the display name goes on outgoing mail |
| Whether 2FA is on; undo-send length; unlock-method flags and salt; setup-checklist state | 5 Clear | Database, flags on your account | Life of the account | The server enforces them |
| Push tokens and device names | 5 Clear | Database. The push itself says only "New email" or "New chat message" — no sender, no preview | Until the token is revoked or rejected | Apple and Google need the token to deliver |
| Device pairing (QR) channel | 3 Transient | Redis: 30 s while the QR is shown, 90 s once a phone claims it. What is relayed is encrypted to the new device | At most 2 minutes | Two devices need a meeting point |
| Whether you have ever paired a device | 5 Clear | One boolean | Life of the account | Setup checklist |
| Chat invitation by email | 2 + 4 | The address AES-encrypted by you, plus a keyed hash so the accepting account can be matched. The invitation mail itself passes through once | Until accepted or expired (24 h) | Match the invitee when they arrive |
| Invite links (QR or URL) | 5 Code + 2 | A random code in the clear, plus the guest's proposed name encrypted under a key that travels only in the URL fragment, which browsers never send to us | Until redeemed, revoked or expired (7–30 days) | Single-use redemption needs a lookup value |
| Sign-up invitation codes | 5 Clear | Random code, expiry, used flag | Until used or expired | Invite-only sign-up |
| Network and operations | ||||
| IP address, browser, requested page | 3 Transient | Seen on the connection, never written down. There is no access log, and the application stores no IP address in the database | The connection | Answering your request |
| Rate-limit counters | 4 Keyed hash | Redis, keyed by HMAC of the IP under the server secret, expiring within one hour | ≤ 1 h | Brute-force and sign-up flood protection |
| Proof-of-work challenges | 3 Transient | Redis, 2-minute expiry | 2 min | Bot resistance on public endpoints |
Tiers 1–4 are what we designed for. Tier 5 is what a mail service cannot run without; every item in it is listed above, and nothing is left out because it looks unflattering.
How the database is laid out
The usual way to store mail is one table with a sender column and a recipient column. Ours has neither. This is a conversation between Ada and Ben, as we see it.
random offset ≤ 1 s per copy
Every conversation key is computed by you, not by us
The identifier that ties your messages to a peer or a group is an HMAC of that address under a key that exists only on your device once you have entered your passphrase. Ada's HMAC of Ben's address and Ben's HMAC of Ada's address are two unrelated values. There is no table with a "from" and a "to", and no value that appears in both accounts' rows.
The copies of one message share no value
A send writes one encrypted copy per recipient. Left alone, those rows would carry the same timestamp, and a GROUP BY on it would recover a roster. So every copy is moved by an independent random offset — up to a second for message rows, up to a minute for group state — and nothing in the table is a join key.
What this does not hide, stated exactly
Two things remain. The send request itself names the recipients, because a message has to be delivered somewhere; that list exists in memory for the request (and in the job table during an undo window) and is not written onto any message row. And a full dump still shows that rows in different accounts were written at about the same time — a weak statistical signal on quiet servers, weaker as the service grows. We would rather say that plainly than claim more.
What a full database dump yields
Assume someone walks away with a complete copy of the database and nothing else — no server keys, no running application.
They would not get
- Any message body, subject, attachment, reaction, signature or draft — all PGP or AES ciphertext under keys they do not have
- Your private key or master key — encrypted under your passphrase
- Your password — the OPAQUE record cannot be reversed or used to log in
- Your recovery address — an Argon2 hash with a random salt; guessing would have to be done one candidate at a time, per account
- Working 2FA codes or quick-unlock secrets — encrypted under a key that is not in the database
- The sender or recipient of any message, or the roster of any group — encrypted per participant with no shared value
- The names of your correspondents from autocomplete, imported keys or invitations — ciphertext and keyed hashes
They would get
- The list of accounts: usernames, display names, public keys, creation dates, which have 2FA on
- Per account: how many messages, their sizes, their timestamps, which are read or starred, how they group into threads, and when it signed in
- Self-destruct durations and deadlines, in the clear
- Push tokens and device names; invitation codes and their state
- Any undo-send job still in the table: recipient addresses and, for external mail, the outgoing message
- Write-time proximity across accounts, from which a determined analyst could attempt statistical correlation
What a legal request could yield
We can only hand over what exists. This is what exists, and what does not, for a named account. It describes the system as built; it is not legal advice and makes no promise about jurisdiction or procedure.
Does not exist on our side
- The content of any message, mail or chat, or of any attachment
- Subject lines of internal mail
- A list of who the account has written to or heard from
- Group memberships
- The password, passphrase or private key
- The recovery address in readable form — we could only confirm or deny a candidate address supplied to us
- Any log of IP addresses, connections or requested pages — none is written
- Any log of mail passing to or from the outside — none is written
Exists, and could be produced
- That the account exists, its username, display name, public key, creation date and 2FA status
- Counts, sizes and timestamps of its messages; read and starred flags; self-destruct settings
- Sign-in and token-refresh times, for as long as those token records are kept
- There is no setting or mode in the service that makes content readable. The keys that would be needed are not on our side
Mail exchanged with outside providers is only as private as the other provider makes it. Their copy is theirs.
Four numbers to remember
Each one comes straight from the code.
Don't take our word for it
The same facts are written up in the help center, and two public documents say what has — and has not — been asked of us.
Below: the screens in the app that correspond to rows in the list. The captures are unedited.
Questions
Write to [email protected]. If a row on this page is wrong, or has gone out of date, we would like to know — the matrix is maintained against the code, and corrections go in.
For how the encryption itself works, see Encryption.