Choosing adapters
The four adapter slots, which are required, the honest maturity of each shipped option, and how to decide.
- Status
- pre-1.0
- Applies to
- 0.1.0
- Platforms
- All
- Prereqs
- Quickstart
- Reading time
- 8 min
The SDK is a protocol core with four pluggable slots. Composition is the API: what you pass here determines where keys live, who can deliver messages, and what your backend can see. Adapters are security boundaries, not convenience callbacks.
const signal = await createSignalProtocolClient({
identity: { userId, deviceId, enablePniKeys },
adapters: {
storage, // required
relay, // optional
remoteObjectStore, // optional
protocolManager, // advanced
},
protocol: { /* ... */ },
});Note the shape: only those four live under adapters. Everything else, logger, hooks, sealedSender, groupsV2, prekey timings, is a top-level field on the config.
The four slots
| Slot | Interface | Required | What it owns |
|---|---|---|---|
storage | ISignalProtocolLocalStore | yes | Device-local protocol state: identity keys, prekeys, sessions, ratchet state, contact trust |
relay | ISignalProtocolRelayServer | no | Public key distribution, device registry, envelope delivery |
remoteObjectStore | SignalProtocolRemoteObjectStore | no | Brokered upload and download of encrypted attachment bytes |
protocolManager | ISignalProtocolManager | no | Advanced override, mainly for tests |
The design requires only storage. You can replace delivery or omit the relay. The local test path still encrypts and decrypts. You cannot omit the store because it holds ratchet state. If you lose the store, no server copy can recover it.
Storage adapters
npm install @open-e2ee/signal-protocol-sdk@0.1.0 expo-secure-store expo-sqlite drizzle-orm| Adapter | Subpath | Exports | Status |
|---|---|---|---|
| Expo | /local/store/expo | ExpoSignalProtocolStore, expoStore() | Primary. Requires a development build; SQLCipher is not available in Expo Go. |
| Node | /local/store/node | NodeSignalProtocolStore, nodeStore() | Supported. Encrypted filesystem storage; full store contract. |
| Web | /local/store/web | IndexedDbSignalProtocolStore, indexedDbStore() | Supported. Full store contract; its graduation gates run in real Chromium, Firefox, and WebKit on every change. Deployment requires the browser threat model review. |
| React Native (bare) | /local/store/react-native | reactNativeStore() | Supported. Full store contract; you supply the key-value backend and verify it with the exported backend-conformance kit. |
| In-memory | /local/store/memory | InMemorySignalProtocolStore, inMemoryStore() | Development only. Everything is lost on restart. |
Every store carries the same contract
Each shipped store declares implements ISignalProtocolLocalStore and carries the full contract. This includes Expo, Web, React Native, in-memory, and Node. The contract includes the Sesame device store, sender-key store, and message-record store. Multi-device and both group APIs therefore work on every store, including Node.
Factory call shapes differ, and this catches people porting between runtimes:
expoStore(options?) // synchronous
inMemoryStore() // synchronous
await indexedDbStore() // async
await nodeStore(config?) // async
await reactNativeStore(options) // asyncThere is a fifth piece that is not a store: the secret vault, ISignalProtocolLocalSecretVault, with ExpoSecureStoreSignalProtocolSecretVault at /local/vault/expo-secure-store. It holds small bootstrap secrets, on Expo, exactly one 32-byte database key, in the OS keychain. Platform secret managers are appropriate for tiny keys and bootstrap values, not full session databases. Do not try to put protocol state in it.
Relay adapters
| Adapter | Subpath | Exports | Status |
|---|---|---|---|
| Convex | /remote/relay/convex | ConvexSignalProtocolRelayServer, convexRelay() | Usable. You own the deployment, schema, auth, and authorization. |
| In-memory | /remote/relay/memory | InMemorySignalProtocolRelayServer, inMemoryRelay() | Development only. No auth, no persistence, no authorization. |
Omitting relay gives you local-only mode: real encryption and decryption, no distribution. That is a legitimate configuration for tests and for a first integration where you want the protocol working before the backend exists.
If neither adapter fits, implement ISignalProtocolRelayServer. Read reference → adapters first. The relay must preserve one-time-prekey consumption during concurrent requests. It must never issue a consumed prekey as unused.
Object store adapters
| Adapter | Subpath | Exports |
|---|---|---|
| Convex R2 | /remote/object-store/convex-r2 | ConvexR2ObjectStore, convexR2ObjectStore() |
| Convex R2 server helper | /remote/object-store/convex-r2/server | defineConvexR2ObjectStore() |
| S3 | /remote/object-store/s3 | S3ObjectStore, s3ObjectStore() |
Both are brokered, and this is not an implementation detail. An authenticated application backend maps a request to a canonical object ID and a private provider key, and issues scoped URLs. Cloud credentials and unrestricted provider clients do not belong in the app runtime. If you choose an adapter that takes an access key in the client, you choose the wrong adapter.
Omit remoteObjectStore and file upload operations throw. See encrypted attachments.
How to decide
Start from your runtime, because storage is the only required slot and it is the most constrained choice:
- Shipping an Expo app → use the Expo store and secure-store vault. Plan for a development build.
- Shipping a web app → use the IndexedDB store. Evaluate the browser threat model separately from the adapter.
- Shipping a bare React Native app → use the React Native store. You supply the key-value backend; verify it with the exported backend-conformance kit before you ship.
- Building a service, CLI, or test → the Node store, having read why a server-side identity is a real participant.
Then decide the relay, which is a backend question rather than a runtime one. Already on Convex: use the adapter. On something else: Supabase, your own API, anything: implement ISignalProtocolRelayServer. It is a bounded interface, and the SDK never asks it for anything private.
Then decide attachments, which most teams should defer. Get messaging working first.
Next
- Expo setup, Browser setup, Node setup.
- Local encrypted storage: the store and vault boundary in depth.
- Relay and prekeys: what a relay owes the protocol.
- Adapter interfaces: the full contracts.
- Package subpaths: every import path.
Node
Set up the Node filesystem store for services, integration tests, CLIs, and relay development — with direct answers on key storage, retention, prekeys, recovery, and what a server-side identity does to your threat model.
Build
The application architecture above the protocol, ordered by the sequence the decisions actually arrive in — from device registration through to the recovery policy you cannot avoid choosing.