Adapter interfaces
The four adapter slots — local store, relay, remote object store, protocol manager — with the guarantees each implementation must provide.
Applies to @open-e2ee/signal-protocol-sdk 0.1.x. Public APIs and persisted formats may change before 1.0.
Adapters are security boundaries, not convenience callbacks.
The SDK ships no persistence, no transport, and no account model. You inject them. The split the package draws: the client owns protocol coordination. The host application owns persistence, authentication, authorization, and product policy. Anything on your side of that line is yours to get right, and the protocol's guarantees depend on you getting it right.
The four slots
| Slot | Config field | Type from | Required | Role |
|---|---|---|---|---|
| Local store | adapters.storage | @open-e2ee/signal-protocol-sdk/local/store | Yes | Device-local protocol state |
| Relay | adapters.relay | @open-e2ee/signal-protocol-sdk/remote/relay/types | No | Authenticated device, prekey, and envelope service |
| Remote object store | adapters.remoteObjectStore | @open-e2ee/signal-protocol-sdk/remote/object-store | No | Brokered encrypted-object operations |
| Protocol manager | adapters.protocolManager | @open-e2ee/signal-protocol-sdk/types | No | Advanced override; tests and specialised integrations |
Two adjacent interfaces are not adapter slots but appear alongside them:
| Interface | From | Role |
|---|---|---|
ISignalProtocolLocalSecretVault | /local/vault | Small platform-managed bootstrap secrets |
SignalProtocolClientMediaConfig | client config media | Application-owned attachment bytes and caches |
Omitting relay gives you a local-only client: encryption and decryption work, but there is no sync, no prekey publication, and no delivery. Omitting remoteObjectStore disables attachment upload and download only.
ISignalProtocolLocalStore
Device-local protocol state: identity keys, prekeys, session records, sender keys, Sesame device state, message records, and metadata. It is the one adapter you cannot skip.
It composes four narrower interfaces and adds maintenance operations:
| Composed interface | Covers |
|---|---|
IProtocolStore | Identity keys, prekeys, sessions, contact identity trust |
ISesameStore | Per-device Sesame session state |
ISenderKeyStore | Sender-key records for group messaging |
IMessageRecordStore | Message record bookkeeping for dedup and receipts |
Store-level additions: clearAllKeys(), clearAllSessions(), deleteAllPreKeys(), getEcSignedPreKeyMaxId(), getKyberPreKeyMaxId(), getDetailedStats(), getMetadata(key), setMetadata(key, value).
IProtocolStore also carries the two operations that make trust changes atomic: commitSessionTrust(commit) and acceptContactIdentityRotationAndDeleteSessions(...).
What a correct implementation must guarantee
Protect local key material at rest. Identity private keys, prekey private keys, and session records are the whole secret. What "at rest" means is platform-specific, SQLCipher on Expo, filesystem permissions on Node, an application-managed wrapping key on the web, but it is never nothing.
Keep contact identity trust decisions stable and explicit. A silent trust-state reset makes a detectable identity change undetectable. A lookup that returns "trusted" after a read failure has the same effect. Fail closed.
Persist linked-device identity state atomically. Startup verification must not enter a half-linked state. A crash between writes must not leave the device and account with different link states.
Preserve session record semantics. Do not treat sessions as opaque blobs without honoring update and archive behavior. Sessions have update rules and archive rules. A store that round-trips bytes but loses ordering, or that resurrects an archived session, breaks the ratchet.
Make the trust commit atomic. commitSessionTrust writes a session record and its trust decision together. If your backend can persist one without the other, the store is not correct. In the React Native adapter's own words, atomicWrite is a security boundary, not a batching optimization.
Session record format
Current persisted session format is version 4. The SDK rejects and resets older session formats instead of migrating them. It deletes the rejected session and establishes a new one. The peer sees a new session, not silent corruption. Plan for that on upgrade: see upgrades.
Shipped store adapters
| Subpath | Factory | Backing | Maturity |
|---|---|---|---|
/local/store/expo | expoStore(options?) | Expo SQLite with SQLCipher | Primary supported adapter |
/local/store/node | await nodeStore(config?) | Filesystem under dataDir | For non-mobile and server-side use |
/local/store/web | await indexedDbStore() | IndexedDB | Supported; deployment requires the browser threat-model review |
/local/store/react-native | reactNativeStore(...) | Injected key-value storage | Supported; verify the injected backend with assertBackendConformance |
/local/store/memory | inMemoryStore() | In-memory | Development only; never ship it |
nodeStore({ dataDir }) warns against one deployment shape explicitly: do not place the store on an eventually consistent or multi-writer network filesystem. Two processes sharing one store directory over NFS is a corruption path, not a scaling strategy.
The web adapter README defines the limit of browser encryption at rest. It is not an XSS defense. A script on your origin has access to the page's data.
Choosing between them: choosing adapters and local storage.
ISignalProtocolRelayServer
An authenticated device, prekey, and envelope service. It is a transport and a directory, not a trusted party: the relay never needs message plaintext or device private keys. It does see routing metadata. Limits and metadata draws that boundary.
The interface extends IProvisioningService and IKeyRotationService. Methods by group:
| Group | Methods |
|---|---|
| Envelope delivery | send, subscribe, markDelivered |
| Device registry | getDevices, registerDevice, removeDevice, markDeviceConnected, markDeviceDisconnected, heartbeat |
| Identity keys | provisionIdentityKey, rotateIdentityKey, getIdentityKey |
| Prekeys | uploadPreKeys, fetchPreKeyBundle, getPreKeyCount, clearStaleKemPreKeys, uploadEcSignedPreKey, uploadKemLastResortPreKey |
| Group membership | getGroupMembers, getActiveDevices |
| GroupsV2 state | createGroupState, getGroupState, getGroupChanges, submitGroupChange |
| ZK credentials | issueAuthCredential, refreshGroupSendEndorsements? |
| Sealed sender | fetchSenderCertificate?, sendUnidentified?, sendMultiRecipientUnidentified? |
| Retry requests | sendRetryRequest?, subscribeRetryRequests? |
Optional methods gate features rather than failing at runtime: without the sealed-sender trio, isSealedSenderEnabled stays false.
What a correct implementation must guarantee
Bundle fetch must atomically consume one-time prekeys for concurrent callers. The contract states: "The relay must not hand out the same one-time prekey as if it were still unused." This is the most consequential requirement on this page. Serving a prekey twice degrades the initial-message forward secrecy that the handshake provides. If two clients call fetchPreKeyBundle concurrently, only one can receive a given one-time prekey. Use an atomic claim, conditional update, or transactional pop. Do not use a read followed by a delete.
Running out of one-time prekeys is a defined, safe state: the bundle falls back to the last-resort key. Serving a stale one is not.
Device registration, unlink, and stale cleanup must stay consistent across active identity types. ACI and PNI key material must not drift apart. A device removed for one identity type and left registered for the other is a routing bug that presents as undecryptable messages.
Provisioning slot assignment is server-owned, not client-owned. The server owns linked-device slot allocation. Clients must not choose linked deviceIds. registerDevice returns the assigned id. A relay that accepts a client-proposed deviceId lets a client collide with, or impersonate, an existing device slot.
Public-key reads and writes must enforce correct account ownership rules. Anyone may read a public identity key. Only the owning authenticated account may write one. An unauthorized write creates an identity-substitution vulnerability. The attacker becomes that account for any peer that has not verified a safety number.
Authentication is entirely yours. The SDK has no session token, user table, or authorization model. It does not verify a caller's claimed identity.
Shipped relay adapters
| Subpath | Factory | Maturity |
|---|---|---|
/remote/relay/convex | convexRelay(options) | The reference backend implementation |
/remote/relay/memory | inMemoryRelay() | Development only; in-memory, single process |
/remote/relay/convex also exports ConvexGroupServer and ConvexGroupServerApi for GroupsV2 state, plus the ConvexSignalProtocolRelayApi and ConvexSignalProtocolRelayOptions types.
Building your own: relay and prekeys and the Convex relay.
SignalProtocolRemoteObjectStore
Brokered encrypted-object operations. Optional. Required only for attachments.
The shape is deliberately indirect. The client asks for an operation by requestId, the broker resolves that to an objectId and a provider-side URL, and the provider key never reaches the client. The client uploads ciphertext it encrypted locally and receives an opaque handle back.
What a correct implementation must guarantee
The broker holds the provider credential. The client never does. If your implementation ships an S3 key to the device, you replace a broker with a public bucket.
Authorize every request against the calling account. Mapping requestId to objectId is an authorization decision. An unauthenticated or unscoped broker exposes every ciphertext object. Encryption protects content, but object membership and access patterns leak.
Do not accept a client-supplied objectId. Assignment is broker-owned for the same reason device slot assignment is server-owned.
Never handle plaintext or content keys. Encryption happens on the device before upload. The object store handles ciphertext bytes and nothing else.
Authorize deletion and remove the data. Attachment deletion propagates through deleteRemoteAttachment() and the linked-device sync path. A broker that reports success after a soft delete makes deletion and revocation a lie.
Shipped object-store adapters
| Subpath | Factory | Maturity |
|---|---|---|
/remote/object-store/convex-r2 | convexR2ObjectStore(config) | Supported |
/remote/object-store/convex-r2/server | server-side broker helpers | Supported |
/remote/object-store/s3 | s3ObjectStore(config), S3ObjectStoreBroker | Supported |
See attachments and object storage and the R2/S3 guide.
ISignalProtocolManager
An override for the protocol coordination layer itself. Supplying one replaces the SDK's own session and ratchet orchestration.
This exists for tests, for instrumentation, and for integrations that must drive the protocol from an existing engine. It is not a customisation point for ordinary applications, and the package ships no implementation to copy. If you use it to change behaviour, first check ratchetConfig, protocol, and protocolStrategy for the required option.
ISignalProtocolLocalSecretVault
Not an adapter slot on the client. A tiny interface used by store adapters that need a platform-managed bootstrap secret, such as a database wrapping key.
interface ISignalProtocolLocalSecretVault {
getSecret(name: string): Promise<Uint8Array | null>;
setSecret(name: string, value: Uint8Array): Promise<void>;
deleteSecret(name: string): Promise<void>;
}Values are bytes, not strings. /local/vault/expo-secure-store ships an implementation over expo-secure-store. The scope is narrow by design: platform secret managers are appropriate for tiny keys and bootstrap values, but not full session databases. A typical key name is "signal-store-wrapping-key".
Next
- Choosing adapters: which shipped adapter fits your runtime
- Relay and prekeys: implementing the relay contract
- Local storage: implementing or configuring the store
- Package subpaths: where to import each type
API reference
The curated client surface — createSignalProtocolClient options, every SignalProtocolClient method by category, properties, hooks, and the standalone modules.
Package subpaths
The complete exports map of @open-e2ee/signal-protocol-sdk — every importable subpath, what it exports, and each entry's support status.