Migrate from libsignal-protocol-javascript or @privacyresearch/libsignal-protocol-typescript
A re-architecture guide with no wire compatibility, an honest comparison of the packages you are leaving, and a staged cutover that does not require a flag day.
- Status
- pre-1.0
- Applies to
- 0.1.0
- Platforms
- Expo · Browser · Node
- Prereqs
- An existing deployment on a JavaScript Signal Protocol library
- Reading time
- 17 min
This is a re-architecture, not a dependency swap
There is no wire compatibility between @open-e2ee/signal-protocol-sdk and either
libsignal-protocol-javascript or @privacyresearch/libsignal-protocol-typescript.
Existing sessions do not migrate. Stored ratchet state does not migrate. Ciphertext
produced by one is not readable by the other. An import change or compatibility shim
cannot solve this mismatch. The packages use different key-agreement and message formats,
not only different APIs.
If you expected a dependency swap, stop and revise the plan. Teams can otherwise start an afternoon task and later discover that they must re-establish every production session.
Budget a re-architecture: new storage layer, new key distribution, a cutover plan, and a user-visible safety-number change for every conversation. The rest of this page is how to do that without a flag day.
Intended audience
This guide is for teams that run a JavaScript Signal Protocol implementation in production. They need post-quantum key agreement, groups, active maintenance, React Native or Expo, or another licensing path.
Prerequisites
- A working deployment on one of the packages below, with its own key-distribution backend.
- Read Keys, identity, and sessions: the concept mapping assumes it.
- The ability to ship two client versions and run them side by side for a period measured in weeks.
Install
Install alongside your existing library. Nothing here removes it:
npm install @open-e2ee/signal-protocol-sdk@0.1.0On Expo, the storage adapter and the bootstrap vault need peer packages:
npm install @open-e2ee/signal-protocol-sdk@0.1.0 expo-secure-store expo-sqliteThe Expo store requires a development build and is not available in Expo Go. Keep the old
package in package.json until stage 4 below.
Starting points
Every package below is a real project that did real work. The dates are from the GitHub and npm registry APIs, measured 2026-07-24. Overstating a competitor's condition would be a worse failure than saying nothing.
signalapp/libsignal-protocol-javascript
Archived. Last push 2021-08-04. 1,960 stars, 19 issues frozen at archive time. Its own repository description reads: "This library is no longer maintained."
It remains the top search result for "signal protocol javascript" today.
A tutorial updated in January 2026 still uses this description:
"the official JavaScript implementation." The package lost that status in 2021 and received no security fix in five years.
@signalapp/libsignal-client: the official replacement
Actively maintained: v0.99.1 published 2026-07-23, roughly 125,000 downloads per month. This is the implementation Signal Messenger itself uses, and it is healthy, well-engineered software.
It is also a Node native addon. Prebuilt binaries cover only Windows, macOS, and Debian-flavoured Linux. It has no iOS, Android, WASM, or browser build. Hermes has no N-API, so React Native cannot use this addon.
Its README states:
"Use outside of Signal is unsupported. In particular... All APIs and implementations are subject to change without notice."
The license is AGPL-3.0 with no commercial option.
This description defines scope, not quality. @signalapp/libsignal-client is a viable
choice for a Node server on supported Linux when you can meet AGPL obligations. It does
not target browsers, phones, or closed-source products.
@privacyresearch/libsignal-protocol-typescript
Last npm publish 2023-05-06. Last repository push 2023-07-18. GPL-3.0-only. Roughly 27,128 downloads in June 2026, so it is still carrying real deployments.
No sender keys and no group support. No PQXDH, so no post-quantum key agreement. No README statement about audit status either way.
Issue #92 opened in June 2026 and remains open. Its title is "Possible inbound PreKey
trust-check bypass : isTrustedIdentity() Promise is not awaited in
SessionBuilder.processV3()." The issue concerns an unmaintained package and the inbound
prekey trust path. It does not confirm exploitation. The 2023 maintenance dates make a
fix unlikely.
WASM is not the escape hatch
The common plan: compile a Rust or C implementation to WebAssembly and run it everywhere: does not hold on React Native. The failure is concrete:
Error: Unable to bind Webassembly to React Native JSI., js engine: hermesreact-native-webassembly (405 stars) has had no commits since 2023-11-03. Budget this
as an unsolved problem rather than an integration task.
Comparison
| Expo / React Native | Browser | Maintained | Post-quantum | TypeScript-native | Commercial license | |
|---|---|---|---|---|---|---|
@open-e2ee/signal-protocol-sdk | Yes | Yes | Yes — 0.1.x, active | Yes — PQXDH + ML-KEM, default and fails closed | Yes | Yes |
@signalapp/libsignal-client | No — prebuilds for Windows, macOS, Debian-flavoured Linux only | No | Yes — very active, v0.99.1 on 2026-07-23 | Yes | No — Rust core with TypeScript bindings | No — AGPL-3.0 only |
libsignal-protocol-javascript | No | Yes | No — archived, last push 2021-08-04 | No | No — JavaScript | No — GPL-3.0 |
@privacyresearch/libsignal-protocol-typescript | No documented path | Yes | No — last publish 2023-05-06, last push 2023-07-18 | No | Yes | No — GPL-3.0 |
What does not carry over
Be specific with your team about this list, because each line is work.
- Sessions. Every session must be re-established from a fresh prekey bundle. There is no import path for an existing ratchet.
- Ratchet state. Chain keys, message numbers, and skipped-message keys are gone. The new client cannot decrypt messages in flight under the old format after cutover.
- Stored keys. The SDK regenerates identity keys, signed prekeys, and one-time prekeys. Your users' identity keys change.
- Wire format. PQXDH with ML-KEM-1024 is not X3DH. The SDK requires exactly
0x0A || raw ML-KEM-1024 bytesfor public keys and ciphertexts, andpostQuantum: 'required'is the default. - Your key-distribution backend's contract. It becomes an
ISignalProtocolRelayServerimplementation with different responsibilities.
What users experience. Every conversation shows a safety-number change on the first message after cutover, because every identity key is new. If your product surfaces identity changes, and it should, you will produce one for every contact of every migrating user, at once. Plan the copy before you plan the code. See Identity changes and safety numbers for why a mass identity-change event is exactly where warning fatigue does damage.
The cutover does not affect message history because it is your application's data. Decrypted rows stay where they are. Only the ability to continue an existing cryptographic session ends.
Concept mapping
Four store interfaces become one adapter. Both older packages require custom identity, prekey, signed-prekey, and session stores. You must keep those four stores consistent. This SDK requires one adapter:
import { createSignalProtocolClient } from '@open-e2ee/signal-protocol-sdk';
import { expoStore } from '@open-e2ee/signal-protocol-sdk/local/store/expo';
const client = await createSignalProtocolClient({
identity: { userId },
adapters: { storage: expoStore({ relay }), relay },
});ISignalProtocolLocalStore is the single required adapter, and shipped implementations exist for
Expo (/local/store/expo, primary supported), Node (/local/store/node), the browser
(/local/store/web), bare React Native (/local/store/react-native, over a
key-value backend you supply), and development (/local/store/memory,
development only). Your hand-written stores become configuration rather than
code.
Your key distribution becomes ISignalProtocolRelayServer. Both older packages leave
prekey upload, bundle fetch, and one-time-prekey consumption to you. This SDK defines an
adapter contract with explicit responsibilities. It covers key uploads, device records,
envelope delivery, provisioning state, and stale-device cleanup. Bundle fetch "must not
hand out the same one-time prekey as if it were still unused." Replenishment is a call:
await client.syncToServer();
const status = await client.checkPreKeyStatus();
// { oneTimePreKeysRemaining, needsReplenishment }ONE_TIME_PREKEY_BATCH_SIZE issues 100 prekeys, and MAX_EC_PREKEYS limits EC prekeys to
200. preKeyLowThreshold defaults to 50. keyRefreshIntervalMs defaults to 172800000
milliseconds (2 days). An existing bundle backend can become an
ISignalProtocolRelayServer implementation. See relay and prekeys.
Group support adds two APIs. @privacyresearch/... has no sender keys. This SDK can
distribute a sender key once for each member, then call
encryptGroupMessage(groupId, plaintext) and
decryptGroupMessage(groupId, senderId, senderDeviceId, framedMessage). GroupsV2 also
adds encrypted group state. The backend "stores opaque encrypted group state and
sequences changes," while member devices keep the secrets. See groups.
Post-quantum support becomes the default. protocol.postQuantum defaults to
'required'. 'compatible' is an explicit opt-in and "does not allow downgrade
recovery." PQXDH follows Revision 3 from 2023-05-24, last updated 2024-01-23. "PQXDH
uses standardized FIPS 203 ML-KEM-1024 behavior." This statement describes the
algorithm, not FIPS validation. Pure JavaScript cryptography is not FIPS 140-validated.
What you gain
- Post-quantum key agreement by default, failing closed rather than downgrading.
- Sender-key groups and GroupsV2 encrypted group state.
- Multi-device through Sesame. The backend allocates linked device IDs 2 through 5, with at most 5 devices for each user.
- Sealed sender, within its documented limits.
- Encrypted attachments through a brokered object store: see Encrypted attachments with R2 and S3.
- Safety numbers with an authenticated confirmation token, and
acceptIdentityRotation()for handling change explicitly. - Six direct production dependencies, resolving to six packages in total, and a public
CI running
npm ci,build,typecheck, andnpm audit --omit=dev. The published assurance run on 2026-08-10 covered 384 modules and 6,893 assertions, 2 skipped, 0 failed. - A commercial license option. The SDK is
AGPL-3.0-or-later, with a proprietary path. See licensing.
A staged cutover that is not a flag day
You cannot decrypt old-format messages with the new client, so the plan is not "migrate", it is "run both and let conversations move on next contact."
Stage 1: ship both, send with neither. Add the SDK alongside the existing library.
Initialize it, create storage, and call syncToServer() so the user has fresh keys
published, but keep sending on the old path. Nothing user-visible changes. Validate
key generation, storage, and prekey upload on every runtime you
support.
const client = await createSignalProtocolClient({
identity: { userId },
adapters: { storage: expoStore({ relay }), relay },
});
await client.syncToServer();Stage 2: receive on both. Route inbound traffic by format. Your relay knows which
envelopes it carries. Tag them at the transport layer rather than sniffing bytes.
Old-format envelopes go to the old library, new-format envelopes to
client.processIncomingEnvelopes(). Both write into the same application message
table, because your decrypted rows were never the library's data.
Stage 3: migrate a conversation on next contact. When a user sends to a peer, check for that peer's new-format keys. If those keys exist, establish a session with the SDK and send on the new path. That action migrates the conversation. Otherwise, send on the old path. Migration follows real usage, so active conversations move in days and dormant ones never need to.
Stage 4: set a cutoff and retire the old path. Pick a date past which the old library no longer sends. Keep it receiving for at least as long again, because in-flight messages exist. Then remove it.
In-flight messages. The old library can decrypt a delayed message while it retains the session state. Therefore, stage 4 continues receiving after it stops sending. Do not delete the old storage when you stop sending. Delete it after the receive window closes. Some messages can still be lost, so build a gap marker before cutover.
Trust boundaries: what crosses which line
The boundaries change shape when you migrate, and it is worth restating them because the old packages left all three to you.
Device to relay. Envelopes cross sealed. Public identity keys and prekey bundles cross because they must be fetchable. The relay never needs message plaintext or device private keys. Under your old DIY backend this boundary existed only as a convention in your code. Here it is an adapter contract.
Device to application storage. Decrypted rows and local files are yours after the client decrypts them. This does not change, and it is why message history survives migration.
Library to application. "The client owns protocol coordination. The host application owns persistence, authentication, authorization, and product policy." Your four custom store interfaces implemented this boundary four times.
What the backend can see
Opacity ledger
| Artifact | Stays on device | Sent to relay | In object store | Visible as metadata |
|---|---|---|---|---|
| Old library's session state | yes | no | n/a | no |
| Old library's stored identity key (private) | yes | no | n/a | no |
New identityKeyPair private half | yes | no | n/a | no |
New identityKeyPair public half | yes | yes | n/a | yes — visibly a new key |
| Signed prekey (private half) | yes | no | n/a | no |
| Signed prekey (public half) | yes | yes | n/a | yes |
| One-time prekeys (public halves) | yes | yes | n/a | count and consumption rate |
| ML-KEM prekeys (public halves) | yes | yes | n/a | yes — presence signals the new client |
| Message plaintext | yes | no | n/a | no |
| Message ciphertext | yes | yes | n/a | envelope size, routing, arrival time |
Session record (version: 4) | yes | no | n/a | no |
| Which protocol version a user is on | yes | yes | n/a | yes — unavoidable during cutover |
| Decrypted message rows (app-owned) | yes | no | n/a | no |
The second-to-last row shows the migration-specific leak. While both paths run, the relay can identify clients on the new path because they publish different key material. That is inherent to a staged cutover, not a defect you can engineer away. It bounds how long you want stage 3 to last.
Failure and recovery behaviour
Production caveats
0.1.x; public APIs and persisted formats may change before 1.0. Persisted formats
are the material risk in a migration: the SDK versions session records (version: 4) and
rejects and resets older formats instead of migrating them. A format change before 1.0 could
cost your users a second round of session re-establishment. Weigh that against the state
of the package you leave.
The SDK is reviewed continuously by adversarial AI agents; it is not audited by any
independent firm. Neither libsignal-protocol-javascript nor
@privacyresearch/libsignal-protocol-typescript has a published audit. You must still complete your own security review.
The Expo store is the primary supported adapter and requires a development build; it is not available in Expo Go. The browser store is supported; a deployment requires the browser threat-model review. The bare React Native store is supported over a key-value backend you supply and verify with the exported backend-conformance kit. The in-memory store and in-memory relay are development only.
Next
- Choosing adapters: which storage adapter replaces your four stores
- Relay and prekeys: turning your key-distribution backend into an
ISignalProtocolRelayServer - Runtime support: the runtime matrix behind the comparison table
- Licensing: AGPL-3.0-or-later and the commercial path
Design offline, device recovery, and identity-change behaviour safely
A design guide for the three product decisions that end-to-end encryption forces on you, with the SDK's real bounds, errors, and operations attached to each.
Reference
Exact surfaces rather than explanation — the client API, the adapter contracts, every importable subpath, the error taxonomy, runtime support, and the security policy.