Add end-to-end encrypted messaging to your TypeScript app.
The SDK encrypts on each device. OpenE2EE 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 { createSignalProtocolClient } from "@open-e2ee/signal-protocol-sdk";
import { inMemoryStore } from "@open-e2ee/signal-protocol-sdk/local/store/memory";
import { convexRelay } from "@open-e2ee/signal-protocol-sdk/remote/relay/convex";
import { ConvexReactClient } from "convex/react";
import { api } from "../convex/_generated/api";
const convex = new ConvexReactClient(process.env.CONVEX_URL!);
// Devices post and collect envelopes from the relay.
const relay = convexRelay({ convex, api: api.signal });
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 { 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 { createSignalProtocolClient } from "@open-e2ee/signal-protocol-sdk";
import { nodeStore } from "@open-e2ee/signal-protocol-sdk/local/store/node";
import { convexRelay } from "@open-e2ee/signal-protocol-sdk/remote/relay/convex";
import { ConvexReactClient } from "convex/react";
import { api } from "../convex/_generated/api";
const convex = new ConvexReactClient(process.env.CONVEX_URL!);
// Devices post and collect envelopes from the relay.
const relay = convexRelay({ convex, api: api.signal });
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 { 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 }), 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 }), 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 { createSignalProtocolClient } from "@open-e2ee/signal-protocol-sdk";
import { expoStore } from "@open-e2ee/signal-protocol-sdk/local/store/expo";
import { convexRelay } from "@open-e2ee/signal-protocol-sdk/remote/relay/convex";
import { ConvexReactClient } from "convex/react";
import { api } from "../convex/_generated/api";
const convex = new ConvexReactClient(process.env.CONVEX_URL!);
// Devices post and collect envelopes from the relay.
const relay = convexRelay({ convex, api: api.signal });
const alice = await createSignalProtocolClient({ // Alice's device.
identity: { userId: "alice" },
adapters: { storage: expoStore({ relay }), 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 }), 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 { 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 { createSignalProtocolClient } from "@open-e2ee/signal-protocol-sdk";
import { indexedDbStore } from "@open-e2ee/signal-protocol-sdk/local/store/web";
import { convexRelay } from "@open-e2ee/signal-protocol-sdk/remote/relay/convex";
import { ConvexReactClient } from "convex/react";
import { api } from "../convex/_generated/api";
const convex = new ConvexReactClient(process.env.CONVEX_URL!);
// Devices post and collect envelopes from the relay.
const relay = convexRelay({ convex, api: api.signal });
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 { 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 { createSignalProtocolClient } from "@open-e2ee/signal-protocol-sdk";
import { reactNativeStore } from "@open-e2ee/signal-protocol-sdk/local/store/react-native";
import { convexRelay } from "@open-e2ee/signal-protocol-sdk/remote/relay/convex";
import { ConvexReactClient } from "convex/react";
import { api } from "../convex/_generated/api";
const convex = new ConvexReactClient(process.env.CONVEX_URL!);
// Devices post and collect envelopes from the relay.
const relay = convexRelay({ convex, api: api.signal });
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.");Managed encrypted delivery
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
Production traffic at $0: 100 Relay MAU, 100,000 delivery units, and 1 GB exact live storage, under hard caps with no automatic charge. Every project also opens a Development environment, which needs no card.
Start a Relay project (opens in a new tab)Live 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 OpenE2EE Relay for managed 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 AGPLv3, and a commercial license covers closed-source products. The build is the same either way. 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)LicensingPricing
The OSI logo trademark is the trademark of Open Source Initiative.