OpenE2EE

React Native

Set up the bare React Native store on op-sqlite and the keychain vault, and get direct answers on key storage, retention, prekeys, reinstall, and backups.

Status
stable
Applies to
11.0.0
Platforms
React Native (bare) · Hermes
Prereqs
Quickstart, plus a React Native app you can make a native build of
Reading time
10 min

The SDK runs on bare React Native and Hermes. Its protocol code is Pure TypeScript. react-native-get-random-values gives the SDK its global random source. op-sqlite with SQLCipher and react-native-keychain provide encrypted device-local storage.

SQLCipher requires a native build

Turn on the op-sqlite SQLCipher build and rebuild the app. op-sqlite works in a native build or an Expo prebuild, not in Expo Go. An Expo app can use the Expo store instead.

Install

npm install @open-e2ee/signal-protocol-sdk@11.0.0 react-native-get-random-values @op-engineering/op-sqlite react-native-keychain

The store needs @op-engineering/op-sqlite 18.2.5 or later and react-native-keychain 10.0.0 or later. Both are optional peers of the SDK.

The SDK reads random bytes only from the global crypto.getRandomValues. Load react-native-get-random-values before the first SDK call, with import 'react-native-get-random-values'; at the top of the app entry, before any other import. Without a global source, the SDK throws SecureRandomUnavailableError.

Turn on the op-sqlite SQLCipher build in the package.json of the app:

{
  "op-sqlite": {
    "sqlcipher": true
  }
}

Then run pod install in ios/ and rebuild the app. Without that flag, op-sqlite cannot apply the database key, and the open fails with KEY_STORAGE_ERROR before it creates a file.

With SQLCipher on, op-sqlite conflicts on iOS with:

  • expo-sqlite, which also links SQLite. Do not install both in one app.
  • expo-updates, which also links SQLite. When it is the only conflict, add "expo.updates.useThirdPartySQLitePod": "true" to ios/Podfile.properties.json.
  • use_frameworks! in the Podfile, which breaks the build or makes it use the embedded SQLite of iOS, which has no cipher.

Open the store

The SDK owns the database: its file, its key, and its migrations. Open the store with one call and pass it to the client:

import { createHostedSignalProtocolClient } from '@open-e2ee/signal-protocol-sdk';
import { reactNativeStore } from '@open-e2ee/signal-protocol-sdk/local/store/react-native';

const signal = await createHostedSignalProtocolClient({
  adapters: { storage: await reactNativeStore() },
  // relayUrl: the Relay connection URL. Use a Sandbox URL during development.
  hosted: { relayUrl, getIdentityAssertion },
});

signal.registerHook('onMessageDecrypted', (envelope) => {
  appendToConversation(envelope.conversationId, envelope.content);
});

signal.startRelaySubscription();

On first use, await reactNativeStore() creates a 32-byte database key, writes it to the vault, and then creates the database file. Each open applies the SDK migrations before it returns the store. A file that a newer SDK wrote does not open (INVALID_STATE).

The options:

  • name: the database file name in the op-sqlite default location, the Library directory on iOS and the databases directory on Android. Letters, digits, ., _, and - only. Default: open-e2ee-signal-protocol.db.
  • vault: the SignalProtocolLocalSecretVault that holds the database key. Default: ReactNativeKeychainSignalProtocolSecretVault.
  • encryptionAtRest: pass false to store the database without encryption. The store then does not use the vault, and it logs a warning at each open. A database created with one setting does not open with the other. Default: true. See encryption at rest.
  • logger: receives the warning of each open without encryption.

A store holds its name until close(). An open or a reset of a name that an open store of the process holds fails with INVALID_STATE. Keep your application tables in a separate database file. The SDK database holds only SDK tables.

For replaced-prekey maintenance, pass the open store to createPreKeyMaintenanceStore(store). To bind the relay subscription to the app state, use useRelayLifecycle from /hooks, as on Expo.

What runs these claims

The private bare React Native example of the SDK runs this store on Hermes: CI builds its release APK and runs it twice on an Android emulator. No CI job runs the store on iOS.

  • Default automated checks (Linux, Node 26) run the storage contract on reactNativeStore over a Node test double of op-sqlite in SQLCipher 4 mode, with a test double of react-native-keychain. The same checks cover the first-use key, a wrong key, a lost key, the reset, the refusal of a build without SQLCipher, the plaintext mismatch, and two parallel first opens.
  • Android emulator job runs the release APK on Hermes with the native op-sqlite SQLCipher build and the default keychain vault. Each run must log a SQLCipher version, a database header that is not the SQLite header, and a wrong-key open that fails with SqliteKeyMismatchError. The second run must resume the stored identity. This job runs on a change to the SDK main branch that can reach the React Native runtime, and on demand. It does not run on a pull request.
  • Manual checks on iOS ran the release build of the example with Xcode 26.3 in the iOS 26.2 simulator. Each launch passed the same checks. The keychain item of the database key has the class kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, does not synchronize, and has no user-presence or biometric flag. The simulator keeps no file protection class and does not enforce Data Protection. So no check shows the file class or the keychain class on a locked device.

No check ran the store on a physical Android or iOS device.


The seven questions

These come up in every integration. Direct answers, from the SDK’s actual behavior, with the boundary marked wherever the SDK leaves the decision to you.

1. Where are the keys stored?

The database key is a random 32-byte key held under the identifier signal_protocol_sqlite_key.<name>, by default signal_protocol_sqlite_key.open-e2ee-signal-protocol.db, by ReactNativeKeychainSignalProtocolSecretVault from /local/vault/react-native-keychain. The vault sets these options on every write, and your app cannot change them:

OptionValueEffect
accessible (iOS)AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLYThe item stays on this device. It is not in iCloud Keychain or in a backup that restores to another device. A background task can read it after the first unlock.
storage (Android)AES_GCM_NO_AUTHThe value is encrypted with AES-GCM under a key in the Android Keystore. No biometric prompt.
securityLevel (Android)SECURE_SOFTWAREThe key must be in the Android Keystore, apart from the data.
cloudSync (iOS)not setreact-native-keychain 10.0.0 turns iCloud sync on when this option has any value, false included.

On iOS, a write deletes the existing item and then adds the new one, so a write is not atomic. A crash between the two steps leaves no item.

The SQLCipher database contains all other protocol data: identity keys, prekeys, sessions, ratchet state, contact trust decisions, group sender keys, and message records. SQLCipher encrypts the database file itself, not each row, so the database key is the only thing that protects the rows. The group sender-key rows are enough to read and to forge a sender’s group messages.

2. How long are they retained?

Retention is protocol behavior, not store behavior, so it is the same as on Expo: the identity key pair is indefinite, signed and Kyber prekeys rotate on keyRefreshIntervalMs (two days by default), and skipped message keys expire after keyExpirationMs (seven days). The Expo answer has the full table.

3. How do I keep keys away from my own backend?

The SDK does not contain a code path that sends a private key. syncToServer() uploads only public material. The realistic leak is your app: a synced table with a key column, a crash reporter that serializes store objects, a file backup that includes the database, or plaintext sent to a server for search. The Expo answer covers each one.

4. How many prekeys, and when do they refill?

The SDK generates 100 one-time prekeys at registration and on replenishment. checkPreKeyStatus() reports needsReplenishment below preKeyLowThreshold, 50 by default. Replenish when the app enters the foreground and when it receives a push, not only when it starts. The Expo answer has the full table.

5. What happens when the user reinstalls the app?

They lose their identity, and the SDK cannot recover it.

An uninstall removes the database file. The default vault keeps the key on this device and out of device backups. After a reinstall, the user is, cryptographically, a new device: a new identity key pair, new prekeys, and no sessions. Every contact sees an identity change.

iOS backs up the Library directory, and Android Auto Backup includes the databases directory unless your app excludes it. A file restored from a backup to a new device therefore has no key there. When the database file exists and the vault holds no key for it, reactNativeStore() fails with LOCAL_STORE_KEY_LOST and changes nothing. A new key cannot read the old file. Restore the vault entry, or call resetReactNativeStore(), which deletes the file, writes a new key, and returns an open, empty store. Close the open store of the name first. The device then needs a new identity.

A file created with encryptionAtRest: false fails an open without that option with SqliteKeyMismatchError (KEY_STORAGE_ERROR), not LOCAL_STORE_KEY_LOST. Open it with the setting that created it.

Decide the product response deliberately: recovery, backup, and migration sets out three named profiles. Handle the contact-side experience in identity change and safety numbers.

6. What does the relay store?

Public material and opaque envelopes. The relay never needs message plaintext or device private keys. It is not blind: it sees the outside of the envelope, and the outside of an envelope is real information. The Expo answer has the full ledger. The database key and the SQLCipher database are never sent to the relay.

7. What should I do about backups?

Do not back up the SDK database file to a server or sync it between devices. The SDK does not include a backup mechanism, and the default vault keeps the key out of device backups, so a restored database file does not open on a new device. Without a backup, an uninstall or a lost phone means lost history and a new identity.

The choice between no backup, a user-held key backup, and provider-assisted escrow is yours, and each one gives something up. See the Expo answer and recovery, backup, and migration.


Production caveats

The SDK is 11.0.x. Public APIs and persisted formats follow semantic versioning.

Reviewed continuously by adversarial AI agents; not audited by any independent firm.

No CI job runs this store on iOS. Verify behavior on both iOS and Android release builds of your app: keychain and background-delivery behavior differ.

Next

On this page