draft optional client
This NIP defines a private group messaging scheme built on top of NIP-17 in which members prove posting rights for the current epoch by presenting an epoch ticket. A ticketed group is represented by a stable group identity keypair and a rotating epoch keypair shared by the current members.
Ticketed groups are designed for conversations in which posting requires a valid epoch ticket and one party (the group owner) controls membership.
- group identity: a dedicated keypair that represents the group as a Nostr account.
- group owner: the party that controls the group identity private key.
- epoch number: a monotonically increasing non-negative integer that identifies a group epoch.
- group epoch: the current membership version of the group, identified by an epoch number and an epoch keypair.
- epoch keypair: a keypair shared by the current members of a group epoch. Its public key is the recipient of NIP-17 gift wraps for that epoch.
- epoch ticket: a
kind:1014invitation that conveys the current epoch number and epoch private key to one member. - current epoch ticket: the valid
kind:1014invitation a client uses for itself for a given group identity, selected according to the rules below.
Control of the group identity private key is the only authority required by this NIP. How the owner proves a relationship between the group identity and any other Nostr account is out of scope.
Each ticketed group has exactly one group identity and exactly one current group epoch.
The group identity:
- MAY publish
kind:0metadata like any other account. - MAY publish
kind:10050to advertise preferred relays for receiving group messages. - MUST be kept secret by the group owner.
This NIP suggests that NIP-24 could define a group boolean metadata field for kind:0 profiles, analogous to the existing bot field, so a group identity can explicitly declare itself to be a group.
The epoch keypair:
- MUST be generated by the group owner.
- MUST be distributed only to current members.
- MUST NOT be used as an author identity.
- MUST be rotated whenever a member is removed.
- SHOULD be rotated whenever a member is added if the owner wants to prevent that new member from reading earlier messages still stored for the old epoch.
Clients MUST ignore any message whose effective sender pubkey is equal to a known epoch public key.
kind:1014 is a regular event signed by the group identity key. Each epoch ticket targets exactly one member and conveys the current epoch number and epoch private key.
The event has the following form:
The kind:1014 event:
- MUST contain exactly one
ptag naming the invited member. - MUST contain exactly one
epochtag whose value is a non-negative decimal integer. - MUST contain the current epoch private key in
content, encoded as 32-byte lowercase hex. - MUST be signed by the group identity private key.
- MUST be delivered to the invited member inside a NIP-59 gift wrap.
- All valid
kind:1014tickets for the same group epoch MUST use the sameepochtag value and the same epoch private key. - When creating a new epoch keypair, the group owner MUST use an epoch number greater than any previously used epoch number for that group identity.
- The initial epoch number SHOULD be
0.
Clients SHOULD treat the pubkey of a valid kind:1014 event as the stable identity of the group and MAY display the group as a regular chat thread.
For a given group identity, the current epoch ticket for the local user is the valid kind:1014 addressed to that user with the highest epoch number. If multiple tickets share that highest epoch number and the same epoch private key, clients SHOULD prefer the one with the greatest created_at. If they still tie, clients SHOULD prefer the lexicographically lowest event id.
If two valid tickets for the same group identity and epoch number contain different epoch private keys, clients SHOULD treat that epoch as inconsistent and SHOULD NOT use it for sending or receiving.
A current member MAY send messages to the group using NIP-17.
By convention, kind:14, kind:15, and kind:7 rumors may be used, following NIP-17. The message MUST be addressed to the current epoch public key, not to the group identity public key.
The inner rumor event:
- MUST be unsigned, as required by NIP-59.
- MUST contain exactly one
ptag naming the current epoch public key. - MUST contain exactly one
htag whose value is the group identity public key. - MUST contain exactly one
epochtag whose value is the sender's current epoch number. - MUST contain exactly one
invited_attag whose value is thecreated_atof the authors's latest epoch ticket. - MUST contain exactly one
invitation_prooftag whose value is thesigof the authors's latest epoch ticket.
Example rumor tags:
[
["p", "<epoch-pubkey>"],
["h", "<group-pubkey>"],
["epoch", "<epoch from sender kind:1014>"],
["invited_at", "<created_at from sender kind:1014>"],
["invitation_proof", "<sig from sender kind:1014>"]
]The wrapping rules are the same as in NIP-17 and NIP-59, with these constraints:
- the
kind:13seal MUST keeptagsempty. - the
kind:1059gift wrap MUST have aptag naming the current epoch public key. - clients SHOULD publish the gift wrap to relays advertised by the group identity's
kind:10050event, when available.
To receive messages for a group, clients MUST listen for NIP-59 gift wraps addressed to the epoch public key from the highest valid current epoch ticket they have for that group.
After unwrapping a message, clients MUST:
- verify the NIP-17 sender binding by checking that the
pubkeyon thekind:13seal matches thepubkeyon the inner rumor. - if the sender pubkey equals the group identity public key, validate and interpret the message according to the Announcements section.
- otherwise, verify that the rumor contains exactly one
p,h,epoch,invited_at, andinvitation_prooftag as defined above. - verify that the rumor
ptag equals the current epoch public key for the group. - verify that the rumor
htag equals the group identity public key for that group. - verify that the rumor
epochtag equals the current epoch number for the group. - rebuild the sender's epoch ticket using the current epoch number and epoch private key known locally:
{
"id": "<computed according to NIP-01>",
"pubkey": "<group-pubkey>",
"created_at": "<invited_at>",
"kind": 1014,
"tags": [
["p", "<sender-pubkey>"],
["epoch", "<current epoch number>"]
],
"content": "<current epoch-private-key hex>",
"sig": "<invitation_proof>"
}- verify the rebuilt
kind:1014signature against the group identity public key.
If that signature is valid, the sender has possession of a valid epoch ticket for the current epoch and is therefore a current member of the group. If validation fails, the event MUST be dropped.
Messages from the group identity public key that do not match the Announcements section SHOULD be ignored.
Clients MUST use only the epoch public key from the highest valid epoch number for receiving new messages. Messages addressed to older epoch public keys SHOULD be treated as historical data from earlier epochs and SHOULD NOT be mixed into the current writable epoch.
The group identity MAY send a NIP-17 kind:14 message to announce joins or leaves.
An announcement message MUST:
- contain exactly one
ptag naming the current epoch public key. - contain one or more
membertags naming affected member public keys. - contain exactly one
htag whose value is the group identity public key. - contain exactly one
epochtag whose value is the current epoch number. - use content exactly equal to
+or-. - SHOULD NOT contain
invited_atorinvitation_prooftags.
Content + means the member pubkeys joined the group. Content - means the member pubkeys left the group.
Announcements are non-authoritative. They do not grant membership, revoke membership, or prove current access by themselves. Membership is determined by the current epoch ticket and epoch rotation rules.
Clients MAY display these announcements however they see fit.
The group owner creates a group by generating a dedicated group identity keypair and an initial epoch keypair, then issuing kind:1014 epoch tickets for the initial epoch to the initial members.
When removing a member, the owner:
- MUST generate a fresh epoch keypair.
- MUST choose an epoch number greater than any previously used epoch number for that group identity.
- MUST issue a new
kind:1014epoch ticket to each remaining member. - MUST stop using the previous epoch public key for new messages.
Removing a member does not revoke messages already delivered to that member, nor does it prevent that member from revealing them later.
If compromise of the current epoch key is suspected, the owner SHOULD immediately create a new epoch and redistribute epoch tickets to the intended members.
Users MAY send NIP-17 direct messages to the group identity public key to request membership changes.
A join request:
- MUST be sent to the group identity public key, not to the epoch public key.
- MUST use NIP-17.
- MUST use content exactly equal to
+.
A leave request:
- MUST be sent to the group identity public key, not to the epoch public key.
- MUST use NIP-17.
- MUST use content exactly equal to
-.
These requests are advisory only:
- a join request does not grant membership by itself.
- a leave request does not remove the sender by itself.
- only the group owner changes membership, by issuing or withholding a
kind:1014epoch ticket and by rotating the epoch when removing a member.
Clients MAY present these requests as explicit join or leave actions instead of ordinary chat messages.
A client MAY store a group identity private key for backup or cross-device restore using NIP-78.
When doing so, the client:
- SHOULD use one
kind:30078event per group. - MUST encrypt
contentwith NIP-44 to the local user's own public key. - MUST NOT publish the group identity private key in plaintext.
- SHOULD use
["d", "<group-pubkey>"]. - SHOULD use
["t", "group"].
One recommended form is:
{
"kind": 30078,
"tags": [
["d", "<group-pubkey>"],
["t", "group"]
],
"content": "<NIP-44 encrypted JSON payload>"
}The decrypted payload SHOULD be:
{
"version": 1,
"group_privkey": "<group-private-key-hex>"
}Clients SHOULD verify after decryption that group_privkey corresponds to the group identity public key stored in the event's d tag. If validation fails, the stored secret MUST be ignored.
This NIP does not require new relay-side semantics beyond NIP-17 and NIP-59.
Relays that already protect access to kind:1059 events SHOULD apply the same protections to gift wraps addressed to epoch public keys.
- Ticketed groups are intentionally centralized around a single owner.
- Any current member can delegate live read access to outsiders by leaking their current epoch ticket. An outsider with that ticket can decrypt group messages until the epoch is rotated.
- Any member can reveal messages they were able to decrypt while they were a member.
- Membership changes only affect future epochs after rotation.
- If the owner adds a member without rotating the epoch, that member may be able to read older messages still available on relays for that epoch.
- Multi-owner groups and delegation of membership control are out of scope for this NIP.
{ "id": "<computed according to NIP-01>", "pubkey": "<group-pubkey>", "created_at": "<unix timestamp>", "kind": 1014, "tags": [ ["p", "<member-pubkey>"], ["epoch", "<non-negative decimal integer>"] ], "content": "<32-byte lowercase hex epoch-private-key>", "sig": "<signature by the group private key>" }