Message History¶
Message plaintext exists neither in the backend nor on the chain. Decrypted messages live only in the local MLS database (OPFS) managed by the XMTP browser SDK.
The read path¶
The app reads once when a conversation is first opened.
XmtpMessagingAdapter.listMessages
conversation.sync() fetch queued network messages, decrypt, store locally
conversation.messages() read from the local MLS DB
sync() has to come first. The local DB is not automatically current, so
messages a peer sent while you were offline only arrive at sync time.
The stream (streamAllMessages) yields only messages from the point of
subscription onward. This path is mandatory for putting past messages on
screen.
Messages read this way are merged with those received on the stream, deduplicated by message ID and sorted by send time.
Unresolved: a new installation cannot open the past¶
MLS keys differ per installation. A new installation cannot decrypt messages sent before it joined the conversation. The ciphertext is still on the network, but there is no key to open it.
So the past history is not recoverable in that browser when:
- Browser storage was cleared (
Clear site data) - You signed in with the same wallet on a different browser or device
- You hit the installation limit (10 per inbox) and revoked an existing installation
This is a consequence of end-to-end encryption, not a bug. To the user, though, it looks like "my conversation disappeared."
A candidate¶
XMTP provides a separate service for transferring history between devices.
HistorySyncUrls.dev = "https://message-history.dev.ephemera.network"
The historySyncUrl option on Client.create and syncAll use this path. An
existing installation has to still be alive to hand history to a new one — in
other words, once storage has already been cleared, this does not recover it
either.
Decisions to make when we take this up.
| Question | Note |
|---|---|
| How much do we guarantee | Adding a device can transfer; clearing storage cannot. How should the product explain that difference |
| Relationship to the limit | The 10-installation limit caps how many devices you can add |
| User messaging | When to tell users that clearing storage is unrecoverable |
Storing plaintext on the backend is not under consideration. It would make the conversation readable by the server.
Exception: the ciphertext archive for evidenced conversations¶
For messages used as evidence there is a path around the limit above. At proposal time the client locks the plaintext with AES-256-GCM and uploads it to the server, while the content key is delivered to participants only over an XMTP control message (E2EE). Right after signing, each participant wraps the content key with a key derived from their wallet signature and leaves it with the server; from then on they can restore the plaintext from any device with a single wallet signature.
- The server keeps only the ciphertext and the wrapped keys, and can open neither. The premise that "the server cannot read the conversation" is unchanged.
- Only wallets that actually signed are eligible to restore. For a participant who did not sign, the server does not even reveal that an archive exists.
- Restored content is displayed only after recomputing the content hash and matching it against the on-chain record. A server swapping the ciphertext would be exposed at this step.
- This relies on wallet signatures being deterministic (RFC 6979, EOA). Restoration may break for ERC-1271 smart contract wallets.