1 Overview
NIP-EE is unrecommended. It is superseded by the Marmot Protocol, which continues this work outside the NIPs repository. Read this chapter to understand existing kind 443, 444, and 445 events and to follow the MLS-over-Nostr design. Do not build new features on it.
This NIP maps the MLS protocol (RFC 9420) onto Nostr. MLS, the Messaging Layer Security protocol, provides end-to-end encrypted (E2EE) group messaging. This NIP defines how Nostr relays and events perform the two services MLS needs: an Authentication Service and a Delivery Service. The MLS cryptography itself stays inside an MLS library, such as OpenMLS.
Earlier NIPs do not solve this problem. NIP-04 encrypts message content but leaks all metadata: anyone can see who talks to whom. NIP-44 improves the encryption scheme but defines no messaging scheme, so privacy does not improve. NIP-17 adds NIP-59 gift wraps and hides most metadata, but it gives no forward secrecy and no post-compromise security. If a key leaks, an attacker reads all past and future messages. None of these NIPs handles group messages.
This NIP has four goals. First, messages must be private: an observer cannot tell that Alice and Bob talk, or that Alice is in a group. Second, messages must be confidential: only the intended recipients can read them. Third, the scheme must give forward secrecy (a leaked key does not expose past messages) and post-compromise security (a leaked key does not expose future messages forever). Fourth, the scheme must scale to large groups and support multiple devices per user.
MLS meets these goals. It is an evolution of the Signal protocol. It reduces the cost of group encryption operations from linear to logarithmic. It works in federated environments, allows ciphersuite upgrades over time, and is agnostic to message content and transport. Here, the transport is WebSockets, Nostr events, and relays. The wider motivation: an E2EE messenger built on relays has no central server that a state-level actor can shut down.
2 Core MLS Concepts
MLS lets clients form groups and communicate securely inside them. A group can hold two members or hundreds of thousands. The group is represented as a tree. The members are the leaves. The tree makes it efficient to encrypt to subsets of members. The MLS protocol manages the cryptographic state of this tree: membership, keys, and the encryption, decryption, and authentication of messages.
- Group. Created by its first member, who invites one or more others. A group evolves in blocks called epochs. Members propose a new epoch with one or more
Proposalmessages. ACommitmessage commits a set of proposals and starts the new epoch. - Client / Member. One device-and-client pair, for example Primal on iOS. Each client is one
LeafNodein the tree, holding its identity, credentials, and capabilities. The terms client and member are interchangeable here. Group state cannot be shared across clients. A user who joins from two devices is two separate members. - Messages. Control messages (
Welcome,Proposal,Commit) evolve the group state.Applicationmessages carry the content members send to each other. - Framing. Every MLS message is wrapped in a structure that records the sender, the epoch, and the message index within the epoch. Framing lets members authenticate and decrypt messages even when they arrive out of order.
MLS is agnostic to message content and to transport. This NIP supplies the transport: Nostr events over relays. This NIP specifies four things: how clients create MLS groups, the required MLS Credential format for Nostr users, the KeyPackage events that let users be added to groups asynchronously, and the Group events that carry the group's evolution and its messages.
3 Creating Groups
Create each MLS group with a random 32-byte MLS group ID. This ID is effectively permanent. Treat it as private to the group. It MUST NOT be published to relays in any form. A separate, public Nostr group ID exists for relay traffic; see §5.
Before creating a group, check that your ciphersuite, capabilities, and extensions are compatible with the users you want to invite. Read their published KeyPackage events (§6) to check this.
When you create a new group, these MLS extensions MUST be used:
| Extension | Requirement | Purpose |
|---|---|---|
| required_capabilities | MUST | Declares the capabilities every member must support. |
| ratchet_tree | MUST | Carries the full ratchet tree so new members can join. |
| nostr_group_data | MUST | Binds Nostr-specific group data to the MLS group. See §5. |
| last_resort | SHOULD (highly recommended) | Marks KeyPackages as reusable to reduce invite race conditions. See §6. |
Changes to a group happen in two MLS steps. First create one or more Proposal events. Then commit a set of proposals with a Commit event. These are MLS objects, not Nostr events. But the group state only evolves when the other members see the Commit. So publish each Commit to relays inside a Group Event (§8).
4 MLS Credentials
A Credential in MLS asserts who a user is, coupled with a signing key. Clients MUST use the BasicCredential type. Set the identity value to the 32-byte hex-encoded public key of the user's Nostr identity key. Clients MUST NOT allow users to change the identity field. Clients MUST validate that no Proposal message tries to change the identity field of any credential in the group.
Each credential has an associated signing key. The initial signing key arrives in the user's KeyPackage event. The signing key MUST be different from the user's Nostr identity key. Rotate it over time; this improves post-compromise security (§6).
5 Nostr Group Data Extension
The nostr_group_data extension associates Nostr-specific data with an MLS group in a cryptographically secure and provable way. Include it as a required capability when you create a group. It stores five values:
| Field | Meaning |
|---|---|
| nostr_group_id | A 32-byte public group ID. Different from the private MLS group ID. It CAN change over time. It is the value in the h tag of Group Events (§8). |
| name | The name of the group. |
| description | A short description of the group. |
| admin_pubkeys | Hex-encoded public keys of the group admins. MLS itself has no admin concept. Clients MUST check this list before any change to the group data, to group membership (add or remove members), or to any other aspect of the group such as the ciphersuite. Exception: every member may send Proposal and Commit messages for their own credential, for example to rotate their own signing key. |
| relays | The Nostr relay URLs the group uses to publish and receive messages. |
Group admins can update all of these values over time with MLS Proposal and Commit events.
6 KeyPackage Event (kind 443) and Signing Keys
A user who wants to be reachable over MLS messaging MUST first publish at least one KeyPackage event, kind 443. Other users read it to authenticate the user, build the Credential, and add the user to a group asynchronously. A user can publish several KeyPackage events with different parameters, for example different ciphersuites or extensions.
Each KeyPackage includes a signing key used to sign MLS messages inside a group. This signing key MUST NOT equal the user's Nostr identity key. Clients MUST retain the private key material of the signing key for every group the user belongs to. In most cases the client, not the user, manages KeyPackage creation and rotation.
{
"id": <id>,
"kind": 443,
"created_at": <unix timestamp in seconds>,
"pubkey": <main identity pubkey>,
"content": "",
"tags": [
["mls_protocol_version", "1.0"],
["ciphersuite", <MLS CipherSuite ID value e.g. "0x0001">],
["extensions", <array of MLS Extension ID values e.g. "0x0001, 0x0002">],
["client", <client name>, <handler event id>, <optional relay url>],
["relays", <array of relay urls>],
["-"]
],
"sig": <signed with main identity key>
}
| Item | Required | Rule |
|---|---|---|
| content | yes | The hex-encoded serialized KeyPackageBundle from MLS. |
| mls_protocol_version | yes | The MLS protocol version in use. For now this is 1.0. |
| ciphersuite | yes | The MLS ciphersuite ID this KeyPackage supports. |
| extensions | yes | The MLS extension IDs this KeyPackage supports. |
| client | optional | Client name, handler event id, and optional relay URL. Helps other clients handle invites when the signing key lives elsewhere. |
| relays | yes | Every relay the client will publish this KeyPackage event to. Enables later deletion. |
| - | optional | Protected-event marker: only the authenticated author can publish this event. See NIP-70. |
The source spec contradicts itself here. The example event shows "content": "", but the field list says the content is required and holds the hex-encoded serialized KeyPackageBundle. Also note the name: in OpenMLS a KeyPackageBundle contains private key material; only the public KeyPackage part can safely be published. The spec text is reproduced as written.
Reuse and last-resort KeyPackages
Minimize KeyPackage reuse. In normal MLS use a KeyPackage is consumed when the user joins a group. But invites to several groups can race for the same KeyPackage. To reduce these race conditions, clients SHOULD publish "last resort" KeyPackages: include the last_resort extension in the KeyPackage's capabilities, the same as for the group. Reusing an initial signing key carries some risk. The risk is mitigated if the user rotates the signing key immediately after joining a group. That rotation also improves the forward secrecy of the whole group.
Deleting KeyPackage events
When a client successfully processes a group request event for a KeyPackage event, it SHOULD delete that KeyPackage event on all relays listed in its relays tag. It MAY publish a new KeyPackage event at the same time. If a client cannot process a Welcome message, for example because another client generated the signing key, it MUST NOT delete the KeyPackage event. It SHOULD show a human-understandable error to the user.
Rotating signing keys
Clients MUST rotate the user's signing key regularly in each group the user belongs to. More frequent rotation gives stronger post-compromise security. Rotation happens through Proposal and Commit events, broadcast to the group in a Group Event (§8).
KeyPackage Relays List event (kind 10051)
A kind 10051 event declares which relays a user publishes KeyPackage events to. It MUST include a list of relay tags with relay URIs. These relays SHOULD be readable by anyone the user wants to hear from.
{
"kind": 10051,
"tags": [
["relay", "wss://inbox.nostr.wine"],
["relay", "wss://myrelay.nostr1.com"]
],
"content": "",
// ...other fields
}
7 Welcome Event (kind 444)
A new user joins a group through an MLS Commit message. The member who sends that Commit is responsible for sending the new member a Welcome event, kind 444. The Welcome event gives the new member the context needed to join the group and start messaging. It travels as a NIP-59 gift-wrapped event.
Ordering matters. The sender SHOULD wait for relay acknowledgement that the Group Event carrying the Commit was received, and only then publish the Welcome event.
{
"id": <id>,
"kind": 444,
"created_at": <unix timestamp in seconds>,
"pubkey": <nostr identity pubkey of sender>,
"content": <serialized Welcome object>,
"tags": [
["e", <ID of the KeyPackage event used to add the user>],
["relays", <array of relay urls>]
],
"sig": <NOT SIGNED>
}
| Item | Required | Rule |
|---|---|---|
| content | yes | A serialized MLSMessage object that contains the MLS Welcome object. |
| e | yes | The ID of the KeyPackage event used to add the user to the group. |
| relays | yes | The relays the new member should query for Group Events. |
| sig | — | Empty. Kind 444 events MUST never be signed. |
Seal and gift-wrap every Welcome event as defined in NIP-59 before publishing. Like all sealed and gift-wrapped events, kind 444 events MUST never be signed. An unsigned event cannot be published to relays if it ever leaks.
Large groups
Above roughly 150 participants, the Welcome message grows past the maximum event size Nostr allows. MLS work on "light" client welcomes, which omit the full ratchet tree, is underway. The source spec defers recommendations for large groups to a future update.
8 Group Events (kind 445)
A Group Event, kind 445, carries every message sent within a group. That includes the control messages that evolve the shared group state (Proposal, Commit) and the Application messages members send each other.
Publish each Group Event from an ephemeral Nostr keypair. This obfuscates the number and identity of the group participants. Clients MUST use a new keypair for every Group Event they publish.
{
"id": <id>,
"kind": 445,
"created_at": <unix timestamp in seconds>,
"pubkey": <ephemeral sender pubkey>,
"content": <NIP-44 encrypted serialized MLSMessage object>,
"tags": [
["h", <group id>]
],
"sig": <signed with ephemeral sender key>
}
The content field is built in two encryption layers:
- MLS serializes and protects the message as a TLS-style
MLSMessageobject, using the group's keys. - The client then encrypts that serialized object with NIP-44. But it does not derive the
conversation_keyfrom sender and receiver keys. Instead it uses the MLSexporter_secret. Use the hex-encodedexporter_secretvalue as a Nostr private key (the sender key). Compute its public key (the receiver key). Then run the standard NIP-44 scheme with that pair.
Generate the exporter_secret with a 32-byte length and the label nostr. The secret rotates on every new epoch: generate a new 32-byte value each time you process a valid Commit. The pubkey field holds the hex-encoded ephemeral sender key. The h tag holds the Nostr group ID from the nostr_group_data extension (§5).
Application messages
Application messages are the member-to-member messages inside the MLSMessage object. Format them as unsigned Nostr events of the appropriate kind. For normal DMs and group messages, clients SHOULD use kind 9 chat message events (NIP-C7). A reaction is a kind 7 event (NIP-25), and so on. After decryption and deserialization, clients can store these events and treat them like any other Nostr events. The group becomes a private Nostr feed with all the features of Nostr.
Three rules protect this inner layer:
- The inner event's
pubkeyMUST be the member's Nostr identity key. Clients MUST check that the sending member's identity matches this pubkey. - Inner events MUST remain unsigned. If they leak, they cannot be published to relays.
- Inner events MUST NOT include an
htag or any other tag that identifies the group.
Commit message race conditions
MLS tolerates almost any message arriving out of order. Commit messages are the exception: their order moves the group from one epoch to the next. On a decentralized network, two or more Commits can race for the same epoch. Two rules resolve the race:
- Clients sending a Commit MUST wait for acknowledgement from at least one relay that the Group Event carrying the Commit was received, before applying the Commit to their own group state.
- If a client receives two or more Commits for the same epoch, it MUST apply exactly one. Pick the kind
445event with the lowestcreated_at. On a timestamp tie, pick the lowestid. Discard the others.
Clients SHOULD retain previous group state for a short period, to recover from forked group state.
9 Security Considerations
This NIP strives to fully maintain the MLS security guarantees. The main trade-offs:
- Forward secrecy and post-compromise security. As per the MLS spec, keys are deleted as soon as they encrypt or decrypt a message. The MLS library usually handles this. Clients must still take care not to store secrets, especially the
exporter_secret, longer than absolutely necessary. The MLS guarantees carry over unchanged. - Identity key leakage. This NIP does not depend on the user's Nostr identity key for any part of the MLS messaging. A compromised Nostr identity key gives no access to past or future group messages.
- Metadata. The only group-specific metadata on relays is the Nostr group ID in the
htag of kind445events. Admins can change this ID over the group's lifetime. The trade-off: participants and group size stay obfuscated, while message fan-out stays efficient. The content is encrypted twice (NIP-44 plus MLS), so only members with current group state can read it. - KeyPackage reuse. A KeyPackage event can be used one or more times to add its owner to groups. Reuse of the initial signing key carries some risk. Rotating the signing key immediately after joining removes most of it and improves the group's forward secrecy.
Device compromise
Store all data securely on the device: encryption at rest, biometric authentication, and similar protections. Treat full device compromise as a catastrophic event. Consider every group that the device belonged to as compromised until the group removes that member and updates its state. Suggested mitigations:
- Support and encourage self-destructing messages, so full transcript history does not live on a device forever.
- Regularly suggest that group admins remove inactive users.
- Regularly suggest, or automate, rotation of the user's signing key in each group.
- Encrypt group state and keys on the device with a secret that is not part of the group state and not the Nostr identity key.
- Use secure enclave storage where possible.
Two loose ends in the source spec. First, it says kind 445 events "are published ephemerally", but 445 is outside the ephemeral kind range (20000-29999) defined in NIP-01; relays following NIP-01 will store them as regular events. Second, the large-groups section (§7) is an unfinished placeholder: it promises future recommendations that never landed before deprecation.
10 Worked Example
Bob wants to be reachable. Alice starts a two-person group and adds him. First, Bob publishes his relays list and a last-resort KeyPackage.
→ ["EVENT", {"kind":10051,"pubkey":"bob11aa…","content":"", "tags":[["relay","wss://inbox.example.com"]], …}] ← ["OK", "77ac31…", true, ""] → ["EVENT", {"id":"c4f1e2…","kind":443,"pubkey":"bob11aa…","content":"a1b2c3…", "tags":[["mls_protocol_version","1.0"],["ciphersuite","0x0001"], ["extensions","0x0003, 0x000a, 0xf2ee"], ["client","ManualChat","31990e…","wss://inbox.example.com"], ["relays","wss://inbox.example.com"],["-"]], "sig":"9de1aa…"}] ← ["OK", "c4f1e2…", true, ""]
Alice fetches Bob's KeyPackage, creates the group locally with the required extensions, and commits an Add proposal. She publishes the Commit as a Group Event from a fresh ephemeral key, and waits for the relay's OK.
→ ["REQ", "kp", {"kinds":[443], "authors":["bob11aa…"]}] ← ["EVENT", "kp", {"id":"c4f1e2…", "kind":443, …}] ← ["EOSE", "kp"] -- Alice: MLS create group, Add(Bob), Commit → epoch 1 -- → ["EVENT", {"id":"e09b77…","kind":445,"pubkey":"eph001…", "content":"<NIP-44(exporter_secret keypair, MLSMessage{Commit})>", "tags":[["h","7fa33c…"]],"sig":"<ephemeral key sig>"}] ← ["OK", "e09b77…", true, ""] -- ack received: safe to apply + welcome
Only after the OK does Alice apply the Commit to her own state and send the Welcome. The kind 444 event stays unsigned and rides inside a NIP-59 gift wrap addressed to Bob.
-- inner, unsigned, never published bare -- {"kind":444,"pubkey":"alice99…", "content":"<serialized MLSMessage{Welcome}>", "tags":[["e","c4f1e2…"],["relays","wss://groups.example.com"]], "sig":""} → ["EVENT", {"kind":1059, "pubkey":"rnd042…", "tags":[["p","bob11aa…"]], "content":"<gift-wrapped seal>", …}] ← ["OK", "ab12cd…", true, ""]
Bob unwraps the Welcome, joins at epoch 1, deletes his consumed KeyPackage event from the listed relays, and immediately rotates his signing key with a self-update Commit. Then chat flows as kind 445 events. Each one carries an unsigned inner kind 9 event.
-- inner, after both MLS + NIP-44 layers are removed -- {"kind":9,"pubkey":"alice99…","created_at":1756300000, "content":"welcome to the group, bob","tags":[],"sig":""} -- on the wire it is only this: -- → ["EVENT", {"kind":445,"pubkey":"eph002…","content":"<ciphertext>", "tags":[["h","7fa33c…"]], …}] ← ["OK", "f31c09…", true, ""]
Every kind 445 publish uses a brand-new ephemeral keypair. An observer sees random senders posting opaque blobs to a rotating group ID. Membership, size, and content stay hidden.
11 Version History
NIP-EE had a short life in the NIPs repository. It arrived in 2025 after a long review that started in 2024, and left again within four months when its development moved to a dedicated protocol. Newest first:
- 20252025-12Deprecated. The spec text was replaced with a pointer to the Marmot Protocol, which supersedes this NIP and continues the MLS-over-Nostr work.
- 20252025-09 - 2025-12Typo fixes and header formatting cleanup.
- 20252025-08Fixed the
nostr_group_dataextension link and clarified how to use theexporter_secretas a NIP-44 conversation key. - Origin2025-08Jeff Gardner added NIP-EE after more than a year of iteration in PR #1427.
Original Proposal - Credits
| Author | Jeff Gardner |
| First commit | 2025-08-27 — “Add NIP-EE: E2EE messaging using MLS” (PR #1427) |
| Status | final - unrecommended - optional — superseded by the Marmot Protocol |
| Source | EE.md on GitHub - full change history |
12 See Also
- NIP-01 — the base protocol: events, kinds, tags, and relay messages used throughout this chapter.
- NIP-44 — the encryption scheme applied as the outer layer of kind 445 content.
- NIP-59 — gift wrap; the transport for kind 444 Welcome events.
- NIP-17 — private DMs without forward secrecy; the scheme this NIP set out to improve on.
- NIP-04 — the deprecated original DM scheme and its metadata leaks.
- NIP-70 — the protected-event
-tag used on KeyPackage events. - NIP-C7 — kind 9 chat messages, the recommended inner application message.
- NIP-25 — kind 7 reactions, usable as inner application messages.