# React Native (https://docs.open-e2ee.dev/start/react-native.md)



- 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](https://docs.open-e2ee.dev/start/expo.md) instead.

## Install [#install]

```bash
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:

```json
{
  "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 [#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:

```ts
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](https://docs.open-e2ee.dev/build/local-storage.md#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](https://docs.open-e2ee.dev/start/expo.md#bind-the-relay-to-the-app-state).

### What runs these claims [#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 [#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? [#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:

| Option                    | Value                                 | Effect                                                                                                                                                            |
| ------------------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `accessible` (iOS)        | `AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY` | The 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_AUTH`                     | The value is encrypted with AES-GCM under a key in the Android Keystore. No biometric prompt.                                                                     |
| `securityLevel` (Android) | `SECURE_SOFTWARE`                     | The key must be in the Android Keystore, apart from the data.                                                                                                     |
| `cloudSync` (iOS)         | not set                               | react-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? [#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](https://docs.open-e2ee.dev/start/expo.md#2-how-long-are-they-retained) has the full table.

### 3. How do I keep keys away from my own backend? [#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](https://docs.open-e2ee.dev/start/expo.md#3-how-do-i-keep-keys-away-from-my-own-backend) covers each one.

### 4. How many prekeys, and when do they refill? [#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](https://docs.open-e2ee.dev/start/expo.md#4-how-many-prekeys-and-when-do-they-refill) has the full table.

### 5. What happens when the user reinstalls the app? [#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](https://docs.open-e2ee.dev/build/recovery-backup-migration.md) sets out three named profiles. Handle the contact-side experience in [identity change and safety numbers](https://docs.open-e2ee.dev/build/identity-change-safety-numbers.md).

### 6. What does the relay store? [#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](https://docs.open-e2ee.dev/start/expo.md#6-what-does-the-relay-store) has the full ledger. The database key and the SQLCipher database are never sent to the relay.

### 7. What should I do about backups? [#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](https://docs.open-e2ee.dev/start/expo.md#7-what-should-i-do-about-backups) and [recovery, backup, and migration](https://docs.open-e2ee.dev/build/recovery-backup-migration.md).

***

## Production caveats [#production-caveats]

> **Not covered**
>
> This page gives you a working client. It does not cover production identity setup. For the [OpenE2EE Signal Protocol Relay](https://docs.open-e2ee.dev/relay.md), continue with [identity and authentication](https://docs.open-e2ee.dev/relay/identity.md). Read [relay and prekeys](https://docs.open-e2ee.dev/build/relay-and-prekeys.md) if you implement `SignalProtocolRelayServer` yourself.

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 [#next]

* [Local encrypted storage](https://docs.open-e2ee.dev/build/local-storage.md): the store and vault boundary in full.
* [Runtime support](https://docs.open-e2ee.dev/reference/runtime-support.md#react-native-bare): the Hermes gate and Metro bundling.
* [Choosing adapters](https://docs.open-e2ee.dev/start/choosing-adapters.md): if bare React Native is not the only runtime you ship.
* [Production checklist](https://docs.open-e2ee.dev/operate/production-checklist.md): before you ship.
