Add end-to-end encrypted messaging to your TypeScript app.
The SDK encrypts on each device. The OpenE2EE Signal Protocol Relay operates the delivery: encrypted device mailboxes, group fan-out, private attachments, and push wakes.
Built for
- Expo
- React
- Browser
- Node.js
- Convex
- R2
- S3
npm install @open-e2ee/signal-protocol-sdk
import { createSignalProtocolClient } from "@open-e2ee/signal-protocol-sdk";
import { inMemoryStore } from "@open-e2ee/signal-protocol-sdk/local/store/memory";
import { inMemoryRelay } from "@open-e2ee/signal-protocol-sdk/remote/relay/memory";
const relay = inMemoryRelay(); // Devices post and collect envelopes from the relay.
const alice = await createSignalProtocolClient({ // Alice's device.
identity: { userId: "alice" },
adapters: { storage: inMemoryStore(), relay }, // Your keys stay in your store.
});
const bob = await createSignalProtocolClient({ // Bob's device. In an app, each runs its own.
identity: { userId: "bob" },
adapters: { storage: inMemoryStore(), relay },
});
bob.registerHook("onMessageDecrypted", async (message) => { // Fires after the SDK decrypts.
console.log(message.content); // plaintext, only on Bob's device
});
bob.startRelaySubscription();
// Encrypted on Alice's device before the relay carries it.
await alice.send("bob", "Dinner at 7. I got us the table by the window.");import { createHostedSignalProtocolClient } from "@open-e2ee/signal-protocol-sdk";
import { inMemoryStore } from "@open-e2ee/signal-protocol-sdk/local/store/memory";
const relayUrl = process.env.OPEN_E2EE_RELAY_URL!; // Devices post and collect envelopes from the relay.
// aliceSignIn and bobSignIn return each device's signed identity assertion from your identity provider.
const alice = await createHostedSignalProtocolClient({ // Alice's device.
hosted: { relayUrl, getIdentityAssertion: aliceSignIn },
adapters: { storage: inMemoryStore() }, // Your keys stay in your store.
});
const bob = await createHostedSignalProtocolClient({ // Bob's device. In an app, each runs its own.
hosted: { relayUrl, getIdentityAssertion: bobSignIn },
adapters: { storage: inMemoryStore() },
});
bob.registerHook("onMessageDecrypted", async (message) => { // Fires after the SDK decrypts.
console.log(message.content); // plaintext, only on Bob's device
});
bob.startRelaySubscription();
// Encrypted on Alice's device before the relay carries it.
await alice.send(bob.userId, "Dinner at 7. I got us the table by the window.");import { createSignalProtocolClient } from "@open-e2ee/signal-protocol-sdk";
import { nodeStore } from "@open-e2ee/signal-protocol-sdk/local/store/node";
import { inMemoryRelay } from "@open-e2ee/signal-protocol-sdk/remote/relay/memory";
const relay = inMemoryRelay(); // Devices post and collect envelopes from the relay.
const alice = await createSignalProtocolClient({ // Alice's device.
identity: { userId: "alice" },
adapters: { storage: await nodeStore(), relay }, // Your keys stay in your store.
});
const bob = await createSignalProtocolClient({ // Bob's device. In an app, each runs its own.
identity: { userId: "bob" },
adapters: { storage: await nodeStore(), relay },
});
bob.registerHook("onMessageDecrypted", async (message) => { // Fires after the SDK decrypts.
console.log(message.content); // plaintext, only on Bob's device
});
bob.startRelaySubscription();
// Encrypted on Alice's device before the relay carries it.
await alice.send("bob", "Dinner at 7. I got us the table by the window.");import { createHostedSignalProtocolClient } from "@open-e2ee/signal-protocol-sdk";
import { nodeStore } from "@open-e2ee/signal-protocol-sdk/local/store/node";
const relayUrl = process.env.OPEN_E2EE_RELAY_URL!; // Devices post and collect envelopes from the relay.
// aliceSignIn and bobSignIn return each device's signed identity assertion from your identity provider.
const alice = await createHostedSignalProtocolClient({ // Alice's device.
hosted: { relayUrl, getIdentityAssertion: aliceSignIn },
adapters: { storage: await nodeStore() }, // Your keys stay in your store.
});
const bob = await createHostedSignalProtocolClient({ // Bob's device. In an app, each runs its own.
hosted: { relayUrl, getIdentityAssertion: bobSignIn },
adapters: { storage: await nodeStore() },
});
bob.registerHook("onMessageDecrypted", async (message) => { // Fires after the SDK decrypts.
console.log(message.content); // plaintext, only on Bob's device
});
bob.startRelaySubscription();
// Encrypted on Alice's device before the relay carries it.
await alice.send(bob.userId, "Dinner at 7. I got us the table by the window.");import { createSignalProtocolClient } from "@open-e2ee/signal-protocol-sdk";
import { expoStore } from "@open-e2ee/signal-protocol-sdk/local/store/expo";
import { inMemoryRelay } from "@open-e2ee/signal-protocol-sdk/remote/relay/memory";
const relay = inMemoryRelay(); // Devices post and collect envelopes from the relay.
const alice = await createSignalProtocolClient({ // Alice's device.
identity: { userId: "alice" },
adapters: { storage: expoStore(), relay }, // Your keys stay in your store.
});
const bob = await createSignalProtocolClient({ // Bob's device. In an app, each runs its own.
identity: { userId: "bob" },
adapters: { storage: expoStore(), relay },
});
bob.registerHook("onMessageDecrypted", async (message) => { // Fires after the SDK decrypts.
console.log(message.content); // plaintext, only on Bob's device
});
bob.startRelaySubscription();
// Encrypted on Alice's device before the relay carries it.
await alice.send("bob", "Dinner at 7. I got us the table by the window.");import { createHostedSignalProtocolClient } from "@open-e2ee/signal-protocol-sdk";
import { expoStore } from "@open-e2ee/signal-protocol-sdk/local/store/expo";
const relayUrl = process.env.OPEN_E2EE_RELAY_URL!; // Devices post and collect envelopes from the relay.
// aliceSignIn and bobSignIn return each device's signed identity assertion from your identity provider.
const alice = await createHostedSignalProtocolClient({ // Alice's device.
hosted: { relayUrl, getIdentityAssertion: aliceSignIn },
adapters: { storage: expoStore() }, // Your keys stay in your store.
});
const bob = await createHostedSignalProtocolClient({ // Bob's device. In an app, each runs its own.
hosted: { relayUrl, getIdentityAssertion: bobSignIn },
adapters: { storage: expoStore() },
});
bob.registerHook("onMessageDecrypted", async (message) => { // Fires after the SDK decrypts.
console.log(message.content); // plaintext, only on Bob's device
});
bob.startRelaySubscription();
// Encrypted on Alice's device before the relay carries it.
await alice.send(bob.userId, "Dinner at 7. I got us the table by the window.");import { createSignalProtocolClient } from "@open-e2ee/signal-protocol-sdk";
import { indexedDbStore } from "@open-e2ee/signal-protocol-sdk/local/store/web";
import { inMemoryRelay } from "@open-e2ee/signal-protocol-sdk/remote/relay/memory";
const relay = inMemoryRelay(); // Devices post and collect envelopes from the relay.
const alice = await createSignalProtocolClient({ // Alice's device.
identity: { userId: "alice" },
adapters: { storage: await indexedDbStore(), relay }, // Your keys stay in your store.
});
const bob = await createSignalProtocolClient({ // Bob's device. In an app, each runs its own.
identity: { userId: "bob" },
adapters: { storage: await indexedDbStore(), relay },
});
bob.registerHook("onMessageDecrypted", async (message) => { // Fires after the SDK decrypts.
console.log(message.content); // plaintext, only on Bob's device
});
bob.startRelaySubscription();
// Encrypted on Alice's device before the relay carries it.
await alice.send("bob", "Dinner at 7. I got us the table by the window.");import { createHostedSignalProtocolClient } from "@open-e2ee/signal-protocol-sdk";
import { indexedDbStore } from "@open-e2ee/signal-protocol-sdk/local/store/web";
const relayUrl = process.env.OPEN_E2EE_RELAY_URL!; // Devices post and collect envelopes from the relay.
// aliceSignIn and bobSignIn return each device's signed identity assertion from your identity provider.
const alice = await createHostedSignalProtocolClient({ // Alice's device.
hosted: { relayUrl, getIdentityAssertion: aliceSignIn },
adapters: { storage: await indexedDbStore() }, // Your keys stay in your store.
});
const bob = await createHostedSignalProtocolClient({ // Bob's device. In an app, each runs its own.
hosted: { relayUrl, getIdentityAssertion: bobSignIn },
adapters: { storage: await indexedDbStore() },
});
bob.registerHook("onMessageDecrypted", async (message) => { // Fires after the SDK decrypts.
console.log(message.content); // plaintext, only on Bob's device
});
bob.startRelaySubscription();
// Encrypted on Alice's device before the relay carries it.
await alice.send(bob.userId, "Dinner at 7. I got us the table by the window.");import { createSignalProtocolClient } from "@open-e2ee/signal-protocol-sdk";
import { reactNativeStore } from "@open-e2ee/signal-protocol-sdk/local/store/react-native";
import { inMemoryRelay } from "@open-e2ee/signal-protocol-sdk/remote/relay/memory";
const relay = inMemoryRelay(); // Devices post and collect envelopes from the relay.
const alice = await createSignalProtocolClient({ // Alice's device.
identity: { userId: "alice" },
// storage is your own ReactNativeKeyValueStorage implementation.
adapters: { storage: await reactNativeStore({ storage }), relay }, // Your keys stay in your store.
});
const bob = await createSignalProtocolClient({ // Bob's device. In an app, each runs its own.
identity: { userId: "bob" },
adapters: { storage: await reactNativeStore({ storage }), relay },
});
bob.registerHook("onMessageDecrypted", async (message) => { // Fires after the SDK decrypts.
console.log(message.content); // plaintext, only on Bob's device
});
bob.startRelaySubscription();
// Encrypted on Alice's device before the relay carries it.
await alice.send("bob", "Dinner at 7. I got us the table by the window.");import { createHostedSignalProtocolClient } from "@open-e2ee/signal-protocol-sdk";
import { reactNativeStore } from "@open-e2ee/signal-protocol-sdk/local/store/react-native";
const relayUrl = process.env.OPEN_E2EE_RELAY_URL!; // Devices post and collect envelopes from the relay.
// aliceSignIn and bobSignIn return each device's signed identity assertion from your identity provider.
const alice = await createHostedSignalProtocolClient({ // Alice's device.
hosted: { relayUrl, getIdentityAssertion: aliceSignIn },
// storage is your own ReactNativeKeyValueStorage implementation.
adapters: { storage: await reactNativeStore({ storage }) }, // Your keys stay in your store.
});
const bob = await createHostedSignalProtocolClient({ // Bob's device. In an app, each runs its own.
hosted: { relayUrl, getIdentityAssertion: bobSignIn },
adapters: { storage: await reactNativeStore({ storage }) },
});
bob.registerHook("onMessageDecrypted", async (message) => { // Fires after the SDK decrypts.
console.log(message.content); // plaintext, only on Bob's device
});
bob.startRelaySubscription();
// Encrypted on Alice's device before the relay carries it.
await alice.send(bob.userId, "Dinner at 7. I got us the table by the window.");Encrypted delivery for your application
Relay is the delivery half of an end-to-end encrypted application. Every plan carries every Relay protocol feature. Plans differ by capacity, support, service terms, and approved operational controls.
Free
Production traffic at $0: 100 Relay MAU, 100,000 delivery units, and 1 GB storage, under hard caps with no automatic charge. Every project also opens a Development environment, which needs no card.
Start a Relay projectLive demo, in your browser
Type a sentence and the installed SDK encrypts itA scripted conversation plays through the installed SDK against the in-memory relay. Keys stay on the devices. The relay never needs message plaintext or device private keys.
Demo Settings
- key generationOne-time setup. This device generates its identity and the batches of single-use keys it publishes, before any message exists.
- —
- post-quantum shareThe ML-KEM part of the row above. It is already counted in key generation, not added to it.
- —
- key agreementAgreeing the first shared secrets with the other device, classical and post-quantum together.
- —
- encryptionSealing one message on this device, measured up to the hand-off to the relay.
- —
- decryptionOpening one arrived envelope, from arrival to plaintext. The first opening also performs this device’s share of the key agreement, so both rows show that one span until a later arrival.
- —
- key generationOne-time setup. This device generates its identity and the batches of single-use keys it publishes, before any message exists.
- —
- post-quantum shareThe ML-KEM part of the row above. It is already counted in key generation, not added to it.
- —
- key agreementAgreeing the first shared secrets with the other device, classical and post-quantum together.
- —
- encryptionSealing one message on this device, measured up to the hand-off to the relay.
- —
- decryptionOpening one arrived envelope, from arrival to plaintext. The first opening also performs this device’s share of the key agreement, so both rows show that one span until a later arrival.
- —
relay never needs plaintext
Alice's mailbox0
Bob's mailbox0
Alice's deviceoffline
private keys0
0 message keys derived
- key generation
- —
- encryption
- —
- decryption
- —
Bob's deviceoffline
private keys0
0 message keys derived
- key generation
- —
- encryption
- —
- decryption
- —
Nothing recorded yet.
Select a row to see the call that caused it.
ciphertext — 3,532 base64 characters, 2,648 bytes decoded, excerpt shown
and a relay-assigned envelope id, in whatever format your relay assigns
What ships in the box
Five things that are true the first time you install it.
Pure TypeScript, wherever your app runs
Expo, React Native, browsers, and Node from one package. The protocol code is pure TypeScript with no native crypto module to link.
Post-quantum
PQXDH with ML-KEM-1024 establishes every session, and the Triple Ratchet carries it forward: the Double Ratchet plus the ML-KEM Braid. On without configuration, and failing closed.
Batteries included
Multi-device, groups, sealed sender, safety numbers, encrypted attachments and files ship in the same package as the one-to-one case.
Published for anyone to review
The implementation, the protocol profile, and the threat model are public, with every deliberate divergence from the specifications documented.
Infrastructure you own
Storage, relay, and object storage are interfaces you fill. Each has a shipped adapter, or you write your own. Self-host them on your hardware, a VPS, or your cloud; the SDK itself does not phone home. Or use the Signal Protocol Relay for encrypted delivery. A self-hosted relay holds ciphertext and routing metadata: all your backend can leak and all it can produce for a legal request. Search, moderation, and restoring a user who has lost every device stay yours to design.
The Expo, Node, browser, and bare React Native stores implement the storage interface in full. What each store holds.
Open Source
The complete SDK is free under the MIT or Apache-2.0 license, at your option. Closed-source products need no commercial license. Every feature is in the package, and none of it is time-limited.
- read it
- run it
- share it
- build on it
SDK on GitHub (opens in a new tab)Licensing
The OSI logo trademark is the trademark of Open Source Initiative.