OpenE2EE

Runtime support matrix

Which runtimes the SDK runs in, which store adapter each one uses, and the exact documented position on Expo Go.

Applies to @open-e2ee/signal-protocol-sdk 0.1.x. Public APIs and persisted formats may change before 1.0.

The protocol implementation is portable. The storage adapter varies by runtime and determines runtime support. Read the matrix as a statement about adapters.

The matrix

RuntimeStatusStore adapterNotes
Expo, development buildSupported — primary target/local/store/expoexpoStore()Expo SQLite with SQLCipher; requires application-owned database bootstrap
Expo GoNot available for the Expo storeSee below
React Native, bareSupported/local/store/react-nativeawait reactNativeStore({ storage })Full store contract; you supply the key-value backend and verify it with the exported backend-conformance kit
BrowserSupported/local/store/webawait indexedDbStore()Full store contract; deployment requires the browser threat-model review
Node >= 18Supported for non-mobile use/local/store/nodeawait nodeStore({ dataDir })Filesystem-backed; single writer; full store contract
Any, tests and local developmentDevelopment only/local/store/memoryinMemoryStore()In-memory; never ship it

The package is ESM-only and declares engines.node >= 18.0.0. Every peer dependency is optional. Install the ones your chosen adapter needs. Mapping in Package subpaths.

Expo Go

The one statement the SDK repository makes about Expo Go, verbatim: "SQLCipher requires a development build and is not available in Expo Go."

It appears in the Expo store's own README, in the section describing the application-owned encrypted-database bootstrap. That is the whole documented position. This page does not extend it.

Read literally, the constraint is on SQLCipher, which the Expo store adapter requires. The practical consequence for an application using expoStore() is that it needs an Expo development build, npx expo run:ios, npx expo run:android, or an EAS build, rather than the Expo Go client.

The repository does not claim that another adapter combination supports Expo Go. It also does not claim that a future release will change support. Test your build if you need a definitive result. A database bootstrap failure appears during client creation, not during a session.

Per-runtime guidance

Expo

Use the Expo-specific storage adapter instead of browser shims. Call configureSignalProtocolExpoDbBindings() with your Drizzle and raw database handles before expoStore(). Your application owns the database bootstrap by design. It must open SQLCipher, apply the key before schema access, and create or migrate exported tables. The related key management requires a product decision.

Treat backup, device transfer, and secure-storage availability as explicit product policy. Your app decides whether iOS or Android device backups include the encrypted store. It also decides what happens during device transfer. The SDK does not make these choices. See recovery, backup, and migration.

Verify behaviour on both iOS and Android release builds. Debug builds differ in keychain behaviour, background execution, and native module availability, and a store that works in development can fail on a signed release.

Start at Expo setup.

Browser

Treat IndexedDB and local storage as application-controlled persistence boundaries. They are not a secure enclave. The web adapter's own README is unambiguous about the ceiling: encryption at rest is therefore not an XSS defense. Script executing on your origin has whatever the page has, encrypted database or not.

Consider cross-tab coordination and origin isolation. Two tabs of the same origin are two writers against one store. The ratchet does not tolerate concurrent divergent writes. Decide explicitly whether you run one client per origin with a leader tab, or accept the constraint of a single active tab.

The IndexedDB adapter is supported. It implements the core store contract, Sesame records, sender-key state, retry records, and recovery helpers, and its graduation gates — contract suites in real Chromium, Firefox, and WebKit, multi-tab, interruption, storage-pressure, and soak — run on every change to the source repository.

Start at browser setup.

Node

NodeSignalProtocolStore implements the complete ISignalProtocolLocalStore contract. This includes identity keys, prekeys, sessions, trust decisions, Sesame devices, sender keys, and message records. The class declares implements ISignalProtocolLocalStore. A missing or changed member therefore fails the SDK build instead of appearing at runtime. Multi-device linking and both group APIs are available on this adapter.

Keep storage paths and file permissions explicit. nodeStore({ dataDir }) writes protocol state to a directory you name. The confidentiality of that directory is the confidentiality of the identity keys.

Do not place the store on an eventually consistent or multi-writer network filesystem. The Node adapter's README states this requirement directly. Two processes against one store directory over a network mount is a corruption path.

Do not assume process memory can be reliably scrubbed. JavaScript engines copy, intern, and garbage-collect without giving the program control. The SDK does what it can with secureZeroBytes, and that is a mitigation, not an erasure guarantee.

Test the bundled artifact under every maintained Node line you support. ESM resolution, engines enforcement, and native-free crypto all behave slightly differently across major versions, and the thing you ship is the bundle, not the source tree.

Start at Node setup.

React Native, bare

The bare adapter takes a key-value backend you provide: await reactNativeStore({ storage }), where storage implements ReactNativeKeyValueStorage. Its atomicWrite is a security boundary, not a batching optimization: an implementation that loses atomicity there loses the trust-commit guarantee described in Adapter interfaces.

The subpath exports assertBackendConformance, a backend-conformance kit that proves a backend upholds the contract — atomicity, prefix isolation, unicode round-trips, and, with the reopen hook, durability across restart. Run it against your backend from your own tests before you ship. The SDK runs the same kit against its reference backend on the Hermes engine on every change.

Why the implementation is pure TypeScript

From the project README: "Pure TypeScript. No native modules, no prebuild step, no platform binaries to ship." and "Runs where your app runs. Expo, React Native, modern browsers, and Node from one package." That property is the reason one matrix covers this many runtimes.

The alternative, platform-native crypto, does not span them:

ConstraintEffect
Hermes, the default React Native engine, has no WebCryptoA WebCrypto-based implementation has no React Native path at all
expo-crypto provides hashing and random bytes, not key agreementX25519 cannot be delegated to it
X25519 in WebCrypto reaches roughly 84% of global browser usage — Chrome and Edge 133+, Firefox 130+, Safari 17.0+A browser-only implementation still leaves a real tail of users unserved

That browser-support figure is an external ecosystem statistic from the time of writing. It provides design context, not an SDK support claim. The SDK does not depend on WebCrypto X25519. Every runtime uses @noble/curves for key agreement. The same code path therefore supports Hermes, Safari 16, and Node.

The cost is honest: pure-JavaScript cryptography cannot make the constant-time guarantees of a hardened native implementation. It also lacks FIPS 140 validation. Security and protocol policy states both points plainly.

Next

On this page