OpenE2EE

Protocol and version upgrades

What changes when you upgrade the SDK, what happens to persisted sessions, and who owns version negotiation across a mixed device population.

Status
pre-1.0
Applies to
0.1.0
Platforms
Expo · Browser · Node
Prereqs
A working client from Start → Quickstart
Reading time
11 min

Two things change independently when you upgrade: the code, and the shape of what is already persisted on your users' devices. The first is your build pipeline's problem. The second is a user-visible event, and this page is mostly about that.

The maturity statement is the frame for everything below: 0.1.x. Public APIs and persisted formats may change before 1.0.

Persisted formats: reset, not migrated

Session records carry a format version. The current version is 4, and the SDK's storage boundary requires "rejection of stale-format writes before replacing current state" and "removal or rejection of stale persisted session records on read."

The rule is direct: the SDK rejects and resets older session formats instead of migrating them.

Here is what that means in a deployed application. A user upgrades. On the next read of an older session record, the SDK discards it instead of updating it in place. The session is gone. The next message to or from that peer establishes a fresh session from a prekey bundle.

What the user experiences:

  • The user cannot send to that peer until a bundle fetch succeeds. The relay must be reachable for this fetch.
  • Messages that were in flight under the old session, encrypted before the reset, become undecryptable. Depending on your handling they surface as SESSION_NOT_FOUND or DECRYPTION_FAILED.
  • On the peer's side, a new session establishment from someone they already had a session with. If your product surfaces session resets to users, this looks like an event. If it does not, it is invisible.

What the user does not experience: an identity change. A session reset does not rotate identity keys and does not void safety numbers. Verification survives. Say this explicitly in whatever notice you show, because "your session was reset" reads to a security-conscious user like "you were attacked" unless you distinguish the two.

Before an upgrade changes a persisted format, plan for many users to lose sessions on the same day. The resulting bundle-fetch burst can reach your relay at once.

Protocol policy

The defaults are postQuantum: 'required' and braid: 'required'. Required mode means post-quantum session establishment and the post-quantum message ratchet are both mandatory, and a peer with no post-quantum material fails closed. That failure surfaces as PQXDHRequiredError.

const client = await createSignalProtocolClient({
  identity: { userId },
  adapters: { storage, relay },
  protocol: { postQuantum: 'required', braid: 'required' },
});

'compatible' is an explicit opt-in, and its boundary is narrow. It permits a classical session only when a peer advertises no post-quantum material at all. It "does not allow downgrade recovery":

  • malformed post-quantum metadata fails closed.
  • cryptographic failure after post-quantum selection fails closed.
  • missing referenced local post-quantum prekey material fails closed.
  • successful post-quantum establishment keeps the post-quantum message ratchet mandatory.

'compatible' does not rescue broken sessions. It permits peers that never had post-quantum material. It does not suppress unrelated errors.

braid: 'disabled' keeps post-quantum establishment required and uses the direct ML-KEM SPQR mode instead of the Braid profile. The documentation defines it as an explicit escape hatch for product-reviewed constraints, not downgrade recovery. There is no public postQuantum: 'disabled' mode.

Spec revision pins

The profile pins specific published revisions instead of "the Signal Protocol" in general.

SpecificationRevision
X3DHRevision 1, 2016-11-04
PQXDHRevision 3, 2023-05-24 (last updated 2024-01-23)
Double RatchetRevision 4, 2025-11-04
SesameRevision 2, 2017-04-14
ML-KEM BraidRevision 1, 2025-02-21 (last updated 2025-09-26)

When a pin changes in a future release, read the release notes carefully. A revision change can alter wire behavior or persisted shapes.

There is no wire compatibility with Signal Messenger

This matters when someone in your organisation hears "upgrade" and thinks "interoperate."

"Public keys and ciphertexts require exactly 0x0A || raw ML-KEM-1024 bytes." And: "The 0x0A encoding is distinct from Signal Messenger deployments that use round-3 Kyber1024 tagged 0x08."

Untagged values, the 0x08 tag, unknown tags, and wrong lengths fail before secret derivation or state commit. This is a deliberate, checkable difference, not an accident of version drift, and no future release turns it into interoperability. Signal Protocol is the published specification family. Signal Messenger is an unaffiliated product. Upgrading this SDK never means talking to it.

Rolling upgrades across a device population

Your users do not upgrade at once. App-store rollout, corporate MDM, and delayed user updates create a mixed population. The two ends of a session can use different versions.

INVALID_MESSAGE_VERSION is the code you see when a client receives a message it cannot parse at the protocol version it implements. The SDK raises it and fails closed. It does not negotiate down, and it does not decide what your product should do about it.

The application owns version-negotiation policy. That is a product decision with real options and no SDK default:

  • Publish a minimum supported version and refuse to run below it, with a forced-update screen. Clean, and it locks out users you may not be able to reach.
  • Accept the split and surface an honest per-conversation message when a peer's client is too old. Keeps everyone running, adds a support category.
  • Stage the rollout so that the receiving capability ships in an earlier release than the sending behaviour that requires it. More release cycles, far fewer broken conversations.

Most teams select the third option after they try the first. If a release changes sent data, ship the receive capability one release earlier. Enable sending after telemetry shows that the receiving version reached the target population. client.getStats() and your version reporting provide the required data.

Rollback has two practical constraints. The SDK does not support downgrading a device after it writes version: 4 session records. An older build rejects the newer format, just as a newer build rejects an older format. An upgraded peer does not have to communicate with a peer that rolled back. Treat SDK upgrades as forward-only for each device. Use feature flags, not package versions, for rollback safety.

Upgrade checklist

  • Read the release notes for changes to the spec revision pins table.
  • Check whether the session record version changed. If it did, expect population-wide session resets and a bundle-fetch burst.
  • Confirm your outbound queue survives a period with no establishable session.
  • Confirm INVALID_MESSAGE_VERSION reaches a user-facing state, not a silent drop.
  • Verify your protocol policy is still what you intended. Do not adopt 'compatible' to suppress PQXDHRequiredError.
  • Stage receive-then-send if the release changes the outbound format.
  • Re-run the failure-path suite in testing against the new version before rollout.

Opacity ledger

ArtifactStays on deviceSent to relayIn object storeVisible as metadata
Session record (version: 4)yesnonono
Stale-format session recorddiscarded on readnonono
KyberPreKey public half (0x0A-tagged)yesyesnotag and length are visible
SDK version in useyesyour telemetry onlynonot carried by the protocol
INVALID_MESSAGE_VERSION occurrenceyesnonosafe as a counter
Message plaintextyesnonono

Next

On this page