For a friend
Pollis is an app for messaging: one-on-one, in small groups, or in larger spaces with channels. What makes it different is that we, the people running it, can't read what you write.
Before a message leaves your computer, it is sealed in a digital envelope that only the people you're writing to can open. We can't open it, and neither could anyone who broke into our servers. In a group, only current members can open its messages. Someone who joins later can read what's said from then on, but not what came before. That's on purpose.
We also keep what's written on the outside of those envelopes to a minimum. The stored copy of a message doesn't record who sent it; only people in the conversation can tell, from the sealed contents. So a stolen or handed-over copy of our database wouldn't show who said what. Text messages are padded to a standard size before sealing, so their length gives nothing away. The exception: when you hit send, our delivery server checks that you're allowed to post in that conversation, so at that moment it does see it's you. We're working on removing that.
If someone's cryptographic key changes without explanation (the kind of swap an attacker on our servers would try), Pollis catches it. A red shield appears next to their name in every conversation you share, and a notice appears in the chat so you can check with them another way. You only need to verify a person once: the green shield then follows them across your account.
You sign in with your email (we send a one-time code) and pick a four-digit PIN. The PIN unlocks the app on your computer, and after ten wrong guesses the app deletes the keys to your messages on that device. You also get a long backup code, the Secret Key. Keep it somewhere safe, in a password manager or on paper in a drawer. If you lose your computer or forget your PIN, that code is the only way back in, and we don't keep a copy.
You can use Pollis on more than one device. Sign in on the new one, confirm a code on the old one, and the two set up trust privately. A new device starts empty (it doesn't get your old messages), but both devices receive everything from then on.
Files (pictures, documents, voice notes) are sealed the same way, and so is voice chat: audio is encrypted on your computer before it reaches our voice servers, which route packets they can't listen to. The voice key comes from the same group keys that protect the channel's text, so only people in the channel can hear you.
The trade-offs. Because we can't read your messages, we can't recover them if you lose everything. We can see which conversations exist, who's in them, and roughly when they're busy; routing needs that. Someone who steals your computer while it's unlocked can read your messages. And your privacy also depends on the people you're talking to.
You can also check the app itself. We publish a fingerprint of every release to a public list that can't be quietly edited, and a small tool lets anyone confirm the app they're running is on that list. So we can't secretly give one person a tampered copy: it would either appear in the list permanently or be missing from it. On Linux we also rebuild the app from the public source after every release and confirm it matches byte for byte. That step doesn't exist yet for macOS and Windows.
In short, it works like a normal messaging app, except we can't read your messages even if we wanted to or were forced to. The cost is that you have to keep your PIN and Secret Key safe, because there's no help desk that can let you back in.
How Pollis Keeps Your Conversations Private
Pollis is a desktop chat app for groups. If you have used Slack, Discord, or Microsoft Teams, you know the shape: groups, channels inside groups, direct messages, voice, and files. The difference is what happens to a message after it leaves your device. Slack, Teams, and Discord store messages in a form the operator can read. In Pollis, a message is already ciphertext when it reaches the server, and only the people you sent it to can open it. This page explains how, without assuming a computer-science background, but naming the specific cryptography so you can look it up.
Start with who you have to trust. With most messaging apps you trust your device, the company that built the app, and the company running the servers. With Pollis you trust your device and the build of the app you installed. The servers (the remote database, file storage, voice routing, and the email service that sends sign-in codes) are assumed to be watched by people you have never met. Everything sensitive happens on your device first: messages are encrypted there, and private keys are generated there and never leave it in readable form. The server sees who is in which group, who talks to whom, and how big messages are, the way a mail carrier sees addresses on envelopes. It does not see what is inside, and unlike a paper envelope, this one cannot be steamed open.
Pollis also limits what those addresses reveal. The server used to store the sender's user ID next to every message; it no longer does. The stored copy carries only a placeholder, and the recipient learns the real sender from a signature inside the sealed contents. A stolen or subpoenaed message database therefore does not show who sent what. This covers stored records, not the moment of sending: the delivery service still checks that you are a member of the conversation when you send, and that check names you. A planned upgrade (anonymous membership proofs) would remove it. Two smaller leaks are also closed. Text messages are padded before sealing (the PADMÉ scheme, with a 256-byte floor and about 12% worst-case overhead), so the server cannot tell a one-word reply from a long paragraph by size. Files are not padded, because the way they are shared depends on their real size. And the real-time "new message" ping no longer carries the sender; it is a nudge to fetch the sealed message, and the recipient learns the sender only after opening it.
Group encryption in Pollis is MLS (Messaging Layer Security), a public internet standard, RFC 9420, finalized in 2023. It was designed for groups that change over time: people join and leave, use several devices, and groups can have thousands of members. Wire and Cisco Webex use it too. Pollis implements it with openmls, a Rust library. MLS lets each group pick a bundle of algorithms, called a cipher suite. Pollis runs exactly one, and every part of it is chosen to resist a future quantum computer. Key agreement is X25519 combined with ML-KEM-768, so two devices reach a shared secret without transmitting it. HKDF-SHA384 turns that secret into keys. ChaCha20-Poly1305 encrypts the message and detects tampering: if a single bit of ciphertext is altered in transit, the recipient refuses to decrypt. ML-DSA-44 is the signature scheme a device uses to prove a message came from it. An older classical suite ran alongside this one during the migration and has since been retired, so there is no non-quantum-resistant path left.
The suite is built this way because of harvest now, decrypt later. X25519 is secure against every computer that exists today, but a large enough quantum computer would break it. No such machine exists, and it may never. But an adversary with enough storage can record encrypted traffic now and keep it in case one arrives. So Pollis's key agreement also uses ML-KEM-768, which NIST standardized in 2024 as FIPS 203. Because ML-KEM is new, and new algorithms occasionally turn out to be flawed, Pollis does not replace X25519 with it. It combines both using a construction called X-Wing, so the shared secret stays safe as long as either algorithm holds. The cipher is ChaCha20-Poly1305 rather than AES-GCM; they are of equivalent strength, and ChaCha20-Poly1305 is what the vetted quantum-resistant code path provides. Signatures use ML-DSA-44, standardized in 2024 as FIPS 204. Signatures face a different threat: a forged signature only fools someone when the message is checked, so a quantum computer fifteen years from now cannot retroactively forge today's messages. It matters for things that must stay checkable for years: the identity keys that say which devices are yours, and the public transparency log, whose signed statements about history should be re-checkable indefinitely. The cost is size: an ML-DSA-44 signature is 2,420 bytes against Ed25519's 64, and a public key 1,312 bytes against 32.
MLS does not let a running group change cipher suite, so existing groups were not converted. Pollis created a successor group on the new suite, moved the whole membership across in one step, and retired the old group. Each conversation keeps a generation counter so every device agrees which group is current. The switch waited until no active device in the service was too old to speak the new suite, since a group cannot know who will be invited tomorrow; if that check failed, the group stayed on the older suite rather than risk getting stuck. This only protects traffic from the switch onward. Anything sealed under the classical suite before then stays that way.
Pollis has three layers of identity. The first is your account identity: one long-lived ML-DSA-44 signing key per user, generated on the device where you signed up. Its public half is published by the server; its private half never leaves your devices. The second is the device identity: each computer you use has its own identifier and signing key. The third is what MLS calls a leaf, your device's position inside a particular group's encrypted state. Each leaf is signed by the device key, and the device key is signed by your account key. This chain is what stops a malicious server from inserting a fake device into your account: the fake device would have no signature from your account key, and other members would notice.
On top of cross-signing, Pollis has a person-level check borrowed from Signal: safety numbers. Each pair of users shares a sixty-digit number derived with SHA-512 from both account public keys, shown as twelve blocks of five digits. If you and your contact read the same number aloud or scan each other's QR code, you have confirmed that nobody, including someone with write access to the Pollis server, has swapped a key. Both sides compute the same number, so either can start the check. Until you verify, a contact's key is pinned the first time your device sees it (trust on first use). The pin is checked on every direct message and every group reconciliation involving that person, so if the server later swaps their key, your device shows a red shield wherever that person appears and a notice in the chat. Verification is per person, not per conversation: once you verify someone, the green shield follows them across DMs, member lists, and channels, and turns red everywhere at once if their key changes.
Pollis has no password, because passwords get reused, phished, and leaked. The first sign-in on a device uses a six-digit code sent by email through a service called Resend, which proves you control the address. After that, on that device, you use a four-digit PIN instead. The PIN never leaves the device, and we cannot recover it because we never had it. It unlocks a small encrypted blob in your operating system's keychain that holds the keys your device needs to open its local message database and sign on your behalf. Without the PIN, someone looking inside the keychain sees only ciphertext.
The PIN is turned into a key with Argon2id, a deliberately slow, memory-hard function. Pollis tunes it to 64 MB of memory and about a quarter of a second per attempt. Inside the app, ten wrong tries delete the encrypted keys from the device. That limit does not apply to an attacker who has copied the encrypted keystore and guesses offline: ten thousand PINs at a quarter second each is under an hour of computation. The PIN slows an attacker down; the keychain and your computer's own login and disk encryption are what keep the keystore out of their hands. The keys themselves are wrapped with XChaCha20-Poly1305, whose long random nonce rules out accidental nonce reuse.
The PIN protects the device; the Secret Key protects the account. At sign-up the app shows a string like A3-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX: six groups of five characters from a 32-character alphabet (digits and letters, minus I, L, O, and U, which are easy to confuse). The thirty random characters give 150 bits of entropy, far beyond what anyone can guess. The Secret Key lets you recover your account on a new device when no other device is signed in. The server stores your account identity key encrypted under a key derived from the Secret Key (HKDF-SHA256, then AES-256-GCM), and never sees the Secret Key itself. Pollis shows it once. If you lose it and every device you were signed in on, your account is gone, and nobody can recover it for you.
A second device can be added in two ways. The usual one is approval from a device you already have. The new device generates a temporary key pair and asks the server to notify your other devices. Both screens show an eight-character code computed from that temporary public key, using the same alphabet as the Secret Key. When you confirm the codes match on an existing device, it runs an X25519 Diffie-Hellman exchange with the temporary key, and the two devices get a shared secret the server cannot compute. They use it (via HKDF-SHA256 and AES-256-GCM) to wrap your account identity key, which the new device then unwraps. Because the code is derived from the temporary key, an attacker who controls the server cannot substitute their own key without changing the code you see. Finding a substitute key that produces the same code means about a trillion attempts (eight characters from 32 symbols is 40 bits). The code is not secret, and the server can compute it too; its job is to bind what you compare by eye to the key being handed your account. The fallback is Secret Key recovery, which works the same way with the Secret Key in place of an existing device. Either way, the new device then sets its own PIN, publishes a device certificate signed by the account key, and joins each of your groups via an MLS external commit, using a public snapshot the group publishes for that purpose.
Data on your computer is encrypted too. The local database, which holds decrypted messages, group state, and caches, uses SQLCipher, a version of SQLite with page-level encryption: AES-256 in CBC mode, with HMAC-SHA512 on each page against tampering. Its key is a random 32-byte value generated at sign-up and stored only in the keystore, encrypted under the PIN-derived key. A copy of your disk taken while the app is locked yields a file indistinguishable from random noise. On phones there is one exception: to display a photo or video, the mobile app writes a decrypted copy to its private storage and deletes it when you close the photo or video, sign out, or next open the app after a forced close; until then that copy is protected by the phone's own encryption and app sandbox rather than by your PIN. The local database does not hold your contact list or group list; those are fetched from the server, so an offline computer cannot show names until it reconnects. The cryptography library underneath SQLCipher differs by platform: OpenSSL on Linux, Apple's CommonCrypto on macOS, and on Windows the Rust cryptography libraries the app already carries (OpenSSL there conflicts with the library the video stack uses). The database format and algorithms are the same on all three. A test runs on a real Linux, Windows, and macOS machine for every change that could affect this: it writes a message the way the app does, reads the file back byte by byte to confirm the message is not in it and that the file cannot be opened with the wrong key or with none, and checks that the build linked the expected cryptography library for that platform.
An app writes more than its database, and five things Pollis wrote were not covered by its encryption. All five are fixed. First, pasting or dragging a file into the message box saved a plain copy to the temporary folder under its original name, and nothing deleted it. Pasted files are now kept in memory, handed to the uploader, and released after upload, when you remove the attachment, or when you lock or sign out. Second, the local server that hands decrypted pictures and videos to the app window did not set caching headers, so the browser engine could write decrypted media into its own cache. Every response, including the partial responses used when seeking in a video, now says do not store. Third, the routine that clears the picture cache read the account from state that had already been cleared, so on sign-out and unlock it emptied the wrong (empty) folder and reported success; and "wipe this computer" removed a different folder from the one the cache lived in. Both are fixed, and the account is now passed explicitly. Fourth, on Linux and macOS the app created its files (the list of signed-in accounts, which includes your email address; the keystore; the database; the picture cache) with default permissions, usually readable by other users on the same computer. They are now readable only by you. Windows already restricts files in your user profile to you, the system, and administrators. Fifth, the terminal client's log file, which goes to the shared temporary folder by default, recorded your full email address during sign-in; the file is now private to you and logs only the domain. None of this protects against someone already logged in as you. It protects against other users of a shared computer, and against leftover temp files or browser caches.
Files use convergent encryption. The app computes the file's SHA-256 hash and derives the encryption key and nonce from it with HKDF-SHA256. The file is encrypted with AES-256-GCM in 4 MB chunks, each with its own nonce and authentication tag, and uploaded to Cloudflare R2 through a short-lived URL the Delivery Service issues for that one object. Two people uploading the same file therefore produce the same R2 object, so it is stored once. The cost is that someone who already has a candidate file can compute the same hash and check whether anyone has uploaded it. They cannot read it, but they can confirm it exists. MEGA makes the same trade-off. Pollis accepts it for deduplication; it can be replaced with per-conversation keys if that trade-off is judged wrong.
Voice is end-to-end encrypted as well. The connection to our LiveKit server uses DTLS-SRTP, like every WebRTC app (Slack Huddles, Teams, Google Meet). That encryption ends at the server: LiveKit is a selective forwarding unit (SFU) that receives each participant's audio and forwards it, and in a normal deployment it sees the audio in plaintext. Pollis adds a second layer: each audio frame is encrypted with AES-128-GCM after Opus compression and before it leaves the device, using libwebrtc's FrameCryptor through the LiveKit Rust crate. The SFU can still route packets, since headers stay readable, but the payload is ciphertext. Discord's 2024 DAVE protocol and the LiveKit JavaScript SDK's setupE2EE use the same approach.
The voice key comes from the channel's MLS group, the same group that protects its text. Pollis takes the MLS exporter secret at the current epoch with the label pollis/voice/v1 and the epoch number as context, and uses the resulting 32 bytes as the channel's voice key. Every group member already has the exporter secret, so every member derives the same key and the server never sees it. When membership changes, MLS advances the epoch and Pollis rotates the voice key on the live call without reconnecting: new members hear audio from the new epoch, and removed members cannot decrypt the next frame. The defaults match the LiveKit JavaScript SDK so other clients could interoperate. Voice encryption is always on and cannot be turned off.
Other limitations. The app holds no database credential at all: every read and write goes through the Delivery Service, signed by the device's own key. The credentials for Cloudflare R2, LiveKit, and Resend are not in the app either; the Delivery Service holds them and hands the app a short-lived URL or token for one action at a time. If an inbound MLS commit adds a device that is not cross-signed by its account, the commit is still applied, because refusing it would leave this device stuck behind the rest of the group. Instead the device records it in your security log and removes that device with a normal removal commit, so the rogue device can read the group from the epoch it was added until that removal lands. Rotation of a device's own MLS keys, which heals a group after a compromise, runs when the app runs, not on a background timer. A device rotates its keys in a group when it joins, and afterwards on startup whenever its keys in a group are more than about a week old, with some random spread so a large group does not rotate all at once. A group whose members are all offline for a month does not heal during that month. There is no backup of message history: a new device cannot read messages from before it joined, because MLS's forward secrecy makes that impossible without a backup mechanism, and Pollis deliberately has none. If you forget your PIN and have lost your Secret Key, your account cannot be recovered. These are deliberate trade-offs of convenience for privacy.
The app itself is also checkable. Everything above assumes the copy of Pollis on your computer matches the public source. A code signature only proves Pollis's signing key produced the file; it does not prove the file matches the source, and would not stop a build made to backdoor one person. Pollis publishes a fingerprint of every release to a public, append-only log at verify.pollis.com, the same kind of tamper-evident log used for account keys and group history. A command-line tool, pollis-verify release, confirms that your version is in that log, trusting only one fixed key. That makes it hard for anyone, including a Pollis operator under legal compulsion, to quietly give one user a tampered build: its fingerprint is either in the public log permanently or missing from it.
Three more checks have landed. After every release, an automated workflow rebuilds the Linux AppImage from public source and fails if its hash does not match the log. Every release artifact on all three platforms carries build provenance and a keyless signature anchored in Sigstore's public Rekor log, which Pollis does not operate and which needs no Pollis key to check. And the app's security screen has a "verify this build" button that hashes the running binary and checks it against the log.
Their limits. The rebuild covers only the Linux AppImage. The macOS and Windows payloads are in the log, and anyone can recompute their fingerprints from the installer, but neither is reproducible from source yet, and the Linux .deb and .rpm are not rebuilt. Reproducing a build needs the published build recipe as well as the source, because one build-time value is baked into the binary. Each rebuild's verdict is recorded per release on the assurance page, including releases that were never checked and one whose rebuild ran on a different CI machine image (re-run on a matching image, it reproduced exactly). Provenance is not a release gate, and two earlier releases shipped without it. The in-app check is advisory: it shows the binary you are running was published, not that it was built from the source, and reports pending on a new release until the log's next daily rebuild.
Compared with other apps: Signal and WhatsApp encrypt end to end using the Signal Protocol (X3DH and the Double Ratchet); Pollis uses MLS, which handles large groups and multiple devices better. Slack and Microsoft Teams do not encrypt end to end. Slack Enterprise Key Management and Microsoft 365 Customer Key let large customers control the keys used at rest, but the server still reads plaintext for search, indexing, and compliance. Discord's server reads chat, but its 2024 DAVE protocol encrypts voice and video end to end; Pollis's voice encryption is comparable. Element and Matrix encrypt group chat with Megolm, which gives up some of MLS's post-compromise security in exchange for server-side history backup, which Pollis does not offer. iMessage encrypts pairwise rather than per group, and by default backs up to iCloud with keys Apple holds unless you turn on Advanced Data Protection. Cryptographically, Pollis is closest to Wire, which also uses MLS via OpenMLS. As a product, it is closest to Slack, minus the server reading your messages.
In summary: Pollis encrypts messages on your computer, with keys that exist only on your devices, using MLS. Local data is encrypted on disk under a key that is itself encrypted under your PIN. Your account can be moved to a new device only by an existing device of yours or by a 150-bit Secret Key that only you hold. Files and voice are encrypted before they leave your computer, and the voice key comes from the same MLS group as the channel's messages. The full technical writeup, with the RFCs and parameter choices, is on the Whitepaper tab.