Use end-to-end encryption in your app
Encrypt messages and records on people's devices with keys only they hold, using MLS for groups, so the server can't read them.
Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
You will add e2e to your app, share one note with a group, remove a member and recover keys with a passkey. It takes about 15 minutes. It runs on the local development driver, which is a fake and not for real data. The real MLS driver, @mainsdk/e2e-mls, is built but its audit is pending, so it is not for production yet: use local to develop and test, and plan to switch to mls without changing your code.
You need
- A MainSDK app with a
main.yaml(see Thirty-minute path). - Two routes on your server that store and return opaque bytes: key packages, handshake mailboxes and a recovery blob. The mailbox must keep one global order per group. In tests,
memoryE2eRelay()from@mainsdk/testingstands in.
1. Declare it
e2e:
driver: local # development only; mls (@mainsdk/e2e-mls) has the same contract, audit pending
recovery: passkey
fields:
notes: [body]2. Join and make a group
Each member's device publishes its key package once, then anyone can add that member.
import { createE2e } from "mainsdk/experimental/e2e";
import { localE2eDriver, memoryE2eRelay } from "@mainsdk/testing";
const relay = memoryE2eRelay();
const alice = createE2e({ self: "alice", driver: localE2eDriver, relay });
const bob = createE2e({ self: "bob", driver: localE2eDriver, relay });
await bob.join();
const group = await alice.group.create({ members: ["bob"] });3. Encrypt and decrypt
import { type E2eApi, type E2eGroup, toWire } from "mainsdk/experimental/e2e";
declare const alice: E2eApi; // the devices and the group from step 2
declare const bob: E2eApi;
declare const group: E2eGroup;
const note = toWire(await alice.encrypt(group.id, "meet at six")); // store this string in your database
const text = new TextDecoder().decode(await bob.decrypt(group.id, note));4. Remove a member
import type { E2eApi, E2eGroup } from "mainsdk/experimental/e2e";
declare const alice: E2eApi;
declare const group: E2eGroup;
await alice.group.remove(group.id, "bob"); // a new epoch: bob can't read what alice seals next5. Recover keys
Ask the passkey for its PRF output with WebAuthn, pass it as a function, and enrol once per member. On a new device, restore with the same passkey.
Check it worked
Run npx mainsdk check. It names the trade-off for any encrypted field that search, moderation or analytics would read (MAIN-E2E-0008) and refuses driver: local in production (MAIN-E2E-0009).
Troubleshooting
MAIN-E2E-0003: a member has no key package; have them calle2e.join()first.MAIN-E2E-0004: this device holds no key for the message's epoch, because the member was removed or added later.MAIN-E2E-0012: two members committed for the same epoch; retry the call.MAIN-E2E-0009: the driver is not for production. The MLS driver stays non-production until its external audit signs off, so do not ship end-to-end encryption for real data yet.
See the e2e interface.
Was this page helpful?