How Morse works.

Follow one message from your phone to @ada’s, past every server in between. Tap any part of the map to see what it gets to see.

Cloudflare

Send a message

  1. You write it.

    Your message exists as words in one place: your phone. The Morse core, written in Mesh and compiled into the app, does every bit of the cryptography right there.

  2. Locked with a key used once.

    You and @ada share a Double Ratchet session that turns out a fresh key for every message. ChaCha20-Poly1305 encrypts it, and the key is destroyed once it’s used.

  3. Sealed to her phone, padded to a standard size.

    The encrypted message goes inside a second seal (HPKE) that only @ada’s phone can open, together with who it’s from and which chat it belongs to. It’s padded to one of nine sizes, from 256 bytes to 64 KB. Each of her devices, and each of your other linked devices, gets its own copy.

  4. Addressed, sealed again, and paid for.

    The envelope, with her mailbox address on it, is sealed once more, this time to the delivery service’s key. Then your phone does a small proof of work, so sending has a cost without an account or ID attached to it.

  5. The privacy edge passes it on.

    The first server to touch it is the privacy edge, a Cloudflare deployment of its own. It sees your IP address and checks the work. It can’t open the seal, so it never learns whose mailbox this is.

  6. Delivery opens the outer seal.

    On the way in, the front door strips every header except the few the protocol needs, so no Mesh service gets your IP address, location or user agent. Delivery opens the outer seal and finds a mailbox address, an expiry and ciphertext. It can’t read a word of it, or tell who sent it.

  7. Saved before anyone is told.

    The envelope and its delivery job are committed to PostgreSQL in one transaction. After that commit, a lost wake-up can delay the message but not lose it. What’s stored is ciphertext, not a conversation.

  8. Her phone gets a nudge.

    If Morse is open on @ada’s phone, a WebSocket carries a single event: encrypted-wakeup. No content, no IDs. If it’s closed, a push notification does the same job (see Wake a phone).

  9. She collects it.

    Her phone asks for everything after the last envelope it saw, and signs the request with its device key. A mailbox address only lets people drop envelopes in. Taking them out needs that signature.

  10. Opened, checked, saved, then acknowledged.

    Her phone opens both seals, checks the message really came from you, and rejects anything tampered with, replayed or already seen. It saves the message to its encrypted history first, and only then acknowledges it, so delivery can delete its copy.

Find someone

  1. Keys are made on the phone.

    When you pick a username, your phone makes your account key, this device’s signing and encryption keys, and a supply of one-time prekeys. The private halves go into its local database, sealed under a key the operating system protects, and never leave it unencrypted.

  2. Only the public halves are published.

    Registering is anonymous, so instead of an account it costs a proof of work. The directory learns your username and your devices with their public keys. Nothing else.

  3. Written into a public log.

    Every change to an account’s devices becomes a leaf in a Merkle tree, the key-transparency log, and the directory signs a checkpoint of the whole tree. Quietly swapping someone’s keys would mean forking that history.

  4. Two witnesses countersign.

    Witness A and Witness B each run as their own deployment, with their own signing key and their own record of the last checkpoint they signed. Each checks the directory’s new checkpoint before countersigning it. They see a root hash and a tree size, never a username.

  5. @ada looks you up.

    To write to you, @ada needs your exact username. Her phone gets your devices, a proof that they’re in the log, the signed checkpoint and the witnesses’ signatures. Lookups cost proof of work too, and each proof works once.

  6. Checked before a word is encrypted.

    Her phone checks the proof, the directory’s signature, and that enough of the witnesses it trusts signed that exact checkpoint. It refuses evidence more than five minutes old. If anything fails, nothing gets encrypted. If it all passes, her first message reaches you as a request you can accept or block.

Share a file

  1. Encrypted before it’s split.

    Your phone encrypts the file under a key made for this file alone and cuts it into encrypted parts of up to 64 KB. It also picks a random object ID and two random passes: one for uploading, one for downloading.

  2. Space, granted anonymously.

    The object store grants room for a set number of parts, for seven days at most. The request carries a proof of work and nothing else about you: no account, no device, no conversation.

  3. The parts land in R2.

    The encrypted parts go into a Cloudflare R2 bucket. The object store keeps only hashes of the two passes, and checks each part’s hash and length before it will hand it back.

  4. The key travels in a message.

    The file’s key, its name and type, and the download pass ride inside an ordinary message, sealed and delivered exactly like any other.

  5. She downloads and decrypts it.

    @ada’s phone shows the download pass, pulls down the parts and decrypts them on the phone. The object store only ever handled noise.

  6. Gone on schedule.

    Once the expiry passes, the object store stops serving the file and a cleanup job deletes it. As a backstop, R2 deletes every stored part after eight days, no matter what.

Wake a phone

  1. She turns on notifications.

    @ada’s phone gets a push token from Expo and seals it to the push broker’s public key. The directory keeps that sealed token next to her mailbox, and can’t open it.

  2. A message arrives while she’s away.

    Delivery commits the envelope and a push job together. A scheduler, a Cloudflare Durable Object, is given only the transaction’s ID and runs the job once the commit lands. No message, mailbox or token passes through it.

  3. Delivery calls the broker.

    It hands over the sealed token and a wake hash that is new for every message. The broker is the only part that holds the key to open the token.

  4. A generic nudge goes out.

    The broker opens the token and asks Expo to send a wake-up that says only that there’s something encrypted to fetch. No sender, no recipient, no text.

  5. Apple or Google deliver it.

    Expo hands it to Apple or Google. They learn the push token, the app and the time of each wake-up, which any notification needs. Who wrote, and what, never reaches them.

  6. Her phone fetches the real thing.

    Awake, her phone fetches the envelope, decrypts it and acknowledges it, exactly as in Send a message.

Every part, and what it can see.

Our servers carry sealed envelopes, and each one gets only what its job needs. Here is exactly what that is.

Your phone, and theirs

iPhone · Android · Mac · Windows

The only place your messages are ever words. The Morse core, written in Mesh and compiled to native code, makes every key, does all the encryption, and keeps your history in a local database, encrypted under a key the operating system protects.

Sees
Everything
Never shares unencrypted
Private keysSession secretsYour history

Privacy edge

Cloudflare Worker · Mesh · its own deployment

Checks the proof of work and forwards sealed envelopes. It holds one secret, the credential it forwards with, and serves one route: sending.

Sees
Your IP addressWhen you sendEnvelope sizeProof of work
Never sees
Whose mailboxWho sent itThe message

Front door

Cloudflare Worker

Everything that skips the edge comes through here: lookups, fetches, files and push sign-up. It strips every header but the few the protocol needs, so the Mesh services never get your IP address. Request logging is off.

Sees
Your IP addressRequest timing
Never sees
Message content

Directory + delivery

Mesh service · Cloudflare Container

The directory half keeps usernames, public keys and the key-transparency log. The delivery half keeps a mailbox for each device and nudges phones that are online over a WebSocket.

Sees
UsernamesPublic keysNames looked upMailbox addressesSize bucketArrival and expiryPublic or contacts-only
Never sees
Who sent itWhich chatMessage typeThe message

Witness A and Witness B

2 × Cloudflare Worker · Mesh · own signing keys

Each keeps its own record of the last checkpoint it signed. Phones accept a lookup only when enough of the witnesses they trust signed that exact checkpoint.

Sees
Tree sizeRoot hashSequence number
Never sees
UsernamesDevicesMailboxesMessages

Object store

Mesh service · Cloudflare Container

Grants space, checks every part’s hash and length before handing it back, and lets files expire within seven days.

Sees
Random object IDEncrypted partsApproximate sizeRead timesExpiry
Never sees
File keyName or typeWho sent itWhich chat

Encrypted parts

Cloudflare R2

A lifecycle rule deletes everything in the bucket after eight days, a day past the longest a file can live.

Sees
Encrypted partsRandom names
Never sees
Anything readable

Push broker

Mesh service · Cloudflare Container

The only part holding the key that opens push tokens. A wake-up it sends says only that there’s something encrypted to fetch.

Sees
Push tokensWake-up times
Never sees
Who wroteThe message

Expo, Apple and Google

Only if notifications are on

Expo passes each wake-up to Apple or Google, which deliver it to the phone. Expo gets a random installation ID, never your account’s device ID.

Sees
Push tokenThe appConnectionWake-up times
Never sees
Who wroteTo whomWhat

PostgreSQL

Neon · AWS US East 1

One database. Each service has its own role that can reach only its own schema, and connections verify TLS. It holds ciphertext, not conversations.

Sees
Queued envelopesDirectory and logFile recordsSealed push queue
Never sees
PlaintextDecryption keys

The map is drawn from these working documents, which go further: exact formats, sizes and failure modes.