Chapter 29 - Groups
29
Nostr Implementation Possibility - Hex № 29

Relay-based Groups

Closed groups that live on a relay: the relay enforces membership, admins moderate with events, and the whole group state is rebuilt from the moderation log.

1 Overview

This NIP defines groups that only a closed set of users can write to. A group can be public for reading by external users, or private. The rules are not enforced by cryptography. They are enforced by a relay: the relay accepts or rejects each event based on the group's membership and settings.

A group is identified by a random string of any length. This string is the group id. There is no protocol-level act of group creation. A relay, most likely when asked by users, creates rules around a specific id. From then on, events sent to that id are subject to those rules, and the id serves as an actual group.

Members publish ordinary events into the group. A tag named h carries the group id. Admins publish moderation events (kinds 9000 to 9020) to add users, remove users, and change settings. The relay itself publishes metadata events (kinds 39000 to 39005) that describe the current group state. Users ask to join with kind 9021 and to leave with kind 9022.

Normally a group belongs to one specific relay. The community may move the group to another relay. It may even fork the group, so the same id exists in different forms across different relays.

Hint!

Coming from NIP-28 public chat channels? Think of NIP-29 as the managed upgrade. Channels have no gatekeeper; here the relay is the gatekeeper, and the admins hold the keys.

Tech Note

The source spec states "there is no way to create a group", yet the moderation table defines kind 9007 as create-group. Read the first statement as: no protocol object creates a group; a relay decides to honor an id, and kind 9007 is one way to ask it to.

2 Group Identity, Migration, and Forks

Two things hold a group together: the group id, and the relay that currently enforces access rules over that id on behalf of one or more owners or admins. Enforcement is tied to one relay. The group is only as durable as that relay's willingness and ability to keep hosting it.

Moving a group to another relay

The hosting relay can become unresponsive, go offline permanently, or become untrustworthy. Then the group can migrate. Copy the existing events to a different relay. The new relay must agree to receive the old group, to continue enforcing the same rules, and to acknowledge the same admins.

Forking a group

Members of a group can disagree. A dissident faction can take the group's history to a different relay that agrees to change the rules and assign admin powers to a different set of users. This is a group fork. The new relay keeps the same id but enforces different moderation policies and a different admin roster.

Forks are not bugs. It is a feature of this NIP that one id can identify different communities at the same time. Each fork lives on its own relay, with its own kind:39000 metadata event, its own kind:39001 admin list, and its own message history.

id: "pizza" Relay A (original) 39000 metadata - 39001 admins: X, Y history: events 1..n rules: closed, private Relay B (fork) 39000 metadata' - 39001 admins: Z history: events 1..k + new rules: open reads fork / migrate
Fig. 29-1 — Same id, two communities. A fork copies the history to a new relay, then diverges in rules, admins, and events.
Tech Note

The fork section of the source opens with "a situation in which it might be desirable to move a relay to a different group". The wording is reversed; the intent, per the rest of the section, is moving a group to a different relay.

Detecting migrations and forks

Clients SHOULD periodically check for migrations and forks. They MUST check when their primary relay for a group is offline or unreachable. To check, read the kind:10009 group-list events of the group's admins and of trusted friends. Cache the admin pubkeys of your groups locally, so the check works while the original relay is down.

A kind:10009 entry can point to a different relay hint than the one the client was using. Then the client SHOULD notify the user that the group may have moved or forked. It SHOULD offer to fetch the group's events from the new relay. If the user agrees, it SHOULD migrate the user's participation and update the user's own kind:10009.

Replicating a group for resilience

A group that must not lose history SHOULD keep a live backup replica of all its events on a second relay. The replica relay does not need to enforce any moderation rules. It exists only to preserve history. If the primary relay disappears, the community picks a new authoritative relay and copies the historical events over from the replica.

3 Referencing a Group

Relays generate the events that describe group metadata and group admins. These are addressable events (see NIP-01) signed directly by the relay keypair, with the group id as the d tag.

Identify a group with a NIP-19 naddr1... code to its kind:39000 metadata event. Set the public key to the relay's self public key, the d identifier to the group id, the kind to 39000, and add a relay hint to the hosting relay.

An invite code may be appended to the group identifier:

Group identifier with invite code
naddr1...?invite=<code>

Clients should place this code in the code tag of the kind:9021 join request. The bech32 charset does not include ?. So everything before the ? is still a valid identifier on its own. Clients that do not understand the suffix can ignore it.

4 The h Tag and Timeline References

Every event a user sends to a group — chat messages, text notes, moderation events, and so on — MUST carry an h tag with the value set to the group id.

The two group tags
TagUsed inMeaning
huser-created events, kinds 9000-9022Group id. Marks the event as sent to the group.
drelay-generated kinds 39000-39005Group id as the addressable-event identifier. Marks the event as about the group.

The previous tag

Group events may reference previous events seen on the same relay, in a previous tag. This stops events from being reused out of context. The client picks which previous events to cite. Each reference is the first 8 characters (4 bytes) of the id of any event among the last 50 events the user saw on the relay, excluding the user's own events. Zero or more references are allowed. Clients are recommended to include at least 3, and relays are recommended to enforce that.

Example - previous tag
["previous", "eb96c864", "2db75638", "b5d1065f"]

This is a hack. It prevents messages from being broadcast to external relays that host out-of-context forks of the group. Relays are expected to reject any event whose timeline references are not found in their own database. Clients should also check the references, to keep relays honest.

Late publication

Relays should prevent late publication: messages published now with a timestamp from hours or days ago. The one exception is a relay that is open to receive a group forked or moved from another relay.

5 Roles and Group Management

A group can have any number of users with elevated access. Role labels identify these users. Relays define the labels arbitrarily (see kind:39003 in §8). This NIP does not define what each role can do. That is relay policy, and it can vary.

A user with the right capability assigns roles by publishing a kind:9000 event. Put the target user's pubkey in a p tag, and the roles after it. This works even when the target is already a member: issue the kind:9000 again and the relay must simply update the user's roles.

The roles that carry a real privilege should be listed in the relay's kind:39003 event. The relay may also accept other role names, arbitrarily defined by clients, and just do nothing with them. Any user whose roles carry any privilege counts as an admin in the broad sense, and may be listed in the group's kind:39001 event.

6 User Events: Join and Leave

A group accepts normal user-created events of any kind: chats, threads, long-form articles, calendar events, livestreams, market announcements, and so on. Each keeps the format from its own NIP, plus the h tag. Two special kinds let a user manage their own membership. Both require the h tag.

Join request (kind 9021)

Any user can send a kind:9021 event to the relay to request admission. The relay MUST reject the request while the user has not been added to the group. The error message SHOULD say whether the rejection is final, whether the request is pending review, or whether special handling applies (for example, payment is required). If the user is already a member, the relay MUST reject the event with duplicate: as the error message prefix.

Wire format - kind 9021 join request
{
  "kind": 9021,
  "content": "optional reason",
  "tags": [
    ["h", <group-id>],
    ["code", <optional-invite-code>]
  ]
}

The optional code tag lets the relay preauthorize acceptance. It pairs with the kind:9009 create-invite moderation event.

Leave request (kind 9022)

Any user can send a kind:9022 event to be removed from the group automatically. The relay answers by issuing a kind:9001 event that removes the user.

Wire format - kind 9022 leave request
{
  "kind": 9022,
  "content": "optional reason",
  "tags": [
    ["h", <group-id>]
  ]
}
User Group relay kind 9021 ["h","pizza"] ["code","XYZ"] ["OK", ..., true, ""] kind 9000 put-user (relay key) kind 39002 member list regenerated kind 9 chat ["h","pizza"] ["previous", ...] Without the invite code, the relay may defer to an admin instead.
Fig. 29-2 — Join lifecycle. The user asks; the relay admits with kind:9000 and mirrors the new state in kind:39002.

7 Moderation Events (Kinds 9000-9020)

Moderation events change group state. They are optional. The relay master key or a group admin sends them, and relays should reject them when the author is not an authorized admin. They require the h tag. The relay must check that the sending pubkey can perform the given action, based on its role and the relay's internal policy (see kind:39003).

Wire format - moderation event shape
{
  "kind": 90xx,
  "content": "optional reason",
  "tags": [
    ["h", <group-id>],
    ["previous", ...]
  ]
}

Each action uses a different kind. Each takes its arguments as tags:

Moderation actions
KindNameArgument tags
9000put-userp with pubkey hex, then optional roles. Adds a member or updates roles.
9001remove-userp with pubkey hex.
9002edit-metadataAll the fields of the group-metadata event (kind:39000).
9005delete-evente with event id hex.
9007create-groupnone.
9008delete-groupnone.
9009create-invitearbitrary code.
9010update-pin-listZero or more e with event id hex or a with event address.

The group state must be fully reconstructable from the canonical sequence of these events: who is a member, who is an admin and with which permission, the group name and picture, and so on.

Tech Note

The range is stated as kinds 9000-9020, but only eight kinds are defined. Kinds 9003, 9004, 9006, and 9011-9020 are reserved gaps with no meaning; 9006 (edit-group-status) existed historically and was removed in 2024. Do not assign these numbers new meanings inside groups.

8 Relay-Generated Metadata (Kinds 39000-39005)

These events carry the group id in a d tag, not an h tag. Only the relay master key MUST create them — the key that the NIP-11 document states as the "self" pubkey. For each group, a single instance of each kind (or none) should exist at all times. The events are informative only, but they should reflect the latest group state as changed by the moderation events over time.

Group metadata (kind 39000, optional)

Defines how clients should display the group. The relay where the event is found must generate and sign it. Relays should not accept a kind:39000 signed by anyone else. If the group is forked across relays, each relay carries its own version of this event.

Wire format - kind 39000 group metadata
{
  "kind": 39000,
  "content": "",
  "tags": [
    ["d", <group-id>],
    ["name", "Pizza Lovers"],
    ["picture", "https://pizza.com/pizza.png"],
    ["banner", "https://pizza.com/banner.png"],
    ["about", "a group for people who love pizza"],
    ["private"],
    ["closed"],
    ["supported_kinds", "9", "11"]
  ]
  // other fields...
}
Group metadata tags
TagMeaning when presentMeaning when omitted
nameDisplay name of the group.
pictureGroup picture URL.
bannerGroup banner image URL.
aboutShort group description.
privateOnly members can read group messages.Anyone can read group messages.
restrictedOnly members can write messages to the group.Anyone can send messages to the group.
hiddenRelays should hide group metadata from non-members.Anyone can request the group metadata events.
closedJoin requests are ignored.Users can expect join requests to be honored.
livekitThe group supports LiveKit-powered media rooms (live audio/video). See §9.No AV support advertised.
supported_kindsList of stringified kind numbers the group supports. An empty list means no kinds are supported (an AV-only group, for example).All kinds are assumed supported.
parentThe group is a subgroup of the referenced group. See §10.The group is a root group.
childOrdered list entries naming the group's subgroups. See §10.The group has no subgroups.

Group admins (kind 39001, optional)

Lists each admin along with one or more roles. The roles SHOULD correspond to the roles the relay supports, as advertised by the kind:39003 event.

Wire format - kind 39001 group admins
{
  "kind": 39001,
  "content": "list of admins for the pizza lovers group",
  "tags": [
    ["d", <group-id>],
    ["p", <pubkey1-as-hex>, "ceo"],
    ["p", <pubkey2-as-hex>, "secretary", "gardener"]
    // other pubkeys...
  ]
}

Group members (kind 39002, optional)

A list of member pubkeys, each in a p tag. Relays may choose not to publish this information, to restrict which pubkeys can fetch it, or to display only a subset of the members. Clients should not assume this event is always present, or that it contains the full member list.

Wire format - kind 39002 group members
{
  "kind": 39002,
  "content": "list of members for the pizza lovers group",
  "tags": [
    ["d", <group-id>],
    ["p", <admin1>],
    ["p", <member-pubkey1>],
    ["p", <member-pubkey2>]
    // other pubkeys...
  ]
}

Group roles (kind 39003, optional)

The relay MAY publish this event to tell users and clients which roles it supports in its internal logic. Each role is a role tag with a role name and an optional description.

Example: a relay supports the roles "admin" and "moderator". The "admin" can edit group metadata, delete messages, and remove users. The "moderator" can only delete messages. The relay could just as well call these roles "ceo" and "secretary"; the exact name is not relevant. How the relay decides its roles and how it handles moderation events based on them is specific to each relay and not specified here.

Wire format - kind 39003 group roles
{
  "kind": 39003,
  "content": "list of roles supported by this group",
  "tags": [
    ["d", <group-id>],
    ["role", <role-name>, <optional-description>],
    ["role", <role-name>, <optional-description>]
    // other roles...
  ]
}

LiveKit participants (kind 39004, optional)

The relay MAY publish this event to say who is live in an AV room. The relay should update it whenever a user joins or leaves. Clients are expected to keep an active subscription to it. The event MUST contain zero or more participant tags, each with a participant's public key as lowercase hex.

Wire format - kind 39004 livekit participants
{
  "kind": 39004,
  "content": "",
  "tags": [
    ["d", <group-id>],
    ["participant", <pubkey>]
    // other participants...
  ]
}

Group pinned events (kind 39005, optional)

A list of events pinned in the group, in display order. The kind:9010 moderation event carries the full list: e tags for regular events and a tags for addressable events. Pinning, unpinning, reordering, and clearing pins all work the same way — submit a new full list. When the relay accepts a kind:9010, it regenerates this event to mirror it. The relay may limit the number of pins. Clients may display the pinned events at the top of the group view.

Wire format - kind 39005 pinned events
{
  "kind": 39005,
  "content": "",
  "tags": [
    ["d", <group-id>],
    ["e", <event-id1-as-hex>],
    ["a", <kind>:<pubkey>:<d-identifier>],
    ["e", <event-id2-as-hex>]
    // other event references...
  ]
}

9 Live Audio/Video Spaces

A group may support live audio and/or video chat through a LiveKit server. It signals this with the livekit tag in the group's kind:39000 announcement event. A group that supports only live AV chat, and no text messages, should use an empty supported_kinds tag. This allows both Discord-style setups (independent AV rooms and text rooms) and Telegram-style setups (one room with both text and AV).

To join the AV space after seeing a livekit tag, the client calls the token endpoint at the relay path /.well-known/nip29/livekit/<group-id>. The call carries a NIP-98-style Authorization header. The relay returns the LiveKit JWT and the URL of the LiveKit server. The client then proceeds with the standard LiveKit flow.

The Authorization event is a kind:27235 event. It must contain the tag ["u", "https://relay.tld/.well-known/nip29/livekit/<group-id>"].

  • Relays should enforce access control at JWT creation time, according to the group settings.
  • Relays MUST set the JWT sub property (called "identity" in standard LiveKit libraries) so that its first 64 characters are the user's lowercase hex public key. One user can hold several JWTs and join the same room several times, so relays are expected to append a random identifier after those 64 characters.
  • To advertise AV support (so clients can offer the option when users create or edit groups), a relay should serve HTTP status 204 at https://relay.tld/.well-known/nip29/livekit.

10 Subgroups

Groups MAY form a hierarchy. A subgroup is an ordinary NIP-29 group: same kind:39000, same moderation events, same h tag mechanics. The only difference: its metadata carries a parent tag that points at the parent group's d identifier. A group without a parent tag is a root group.

Example - a three-level tree in kind 39000 events
// root group
{ "kind": 39000, "tags": [["d", "tech"], ["name", "Tech"], ["child", "nostr"]] }

// subgroup
{ "kind": 39000, "tags": [["d", "nostr"], ["name", "Nostr"], ["parent", "tech"], ["child", "nip29"]] }

// sub-subgroup
{ "kind": 39000, "tags": [["d", "nip29"], ["name", "NIP-29"], ["parent", "nostr"]] }

To build the tree, fetch {"kinds":[39000]} and assemble locally. Events without a parent tag are roots. Group the rest under the d value their parent tag references. The tree is scoped to a single relay. When aggregating across relays, a client MAY see conflicting parent values for the same d. Then the kind:39000 from the relay currently serving the group's events is authoritative.

d: "tech" root (no parent) d: "nostr" parent: tech d: "nip29" parent: nostr child #1 child #1 membership and admin roles do NOT cascade
Fig. 29-3 — Subgroup tree. Each node is a normal group. Only the parent and child tags link them.

Relay support detection

A relay that supports subgroups SHOULD advertise it in its NIP-11 relay information document, under a nip29 object:

NIP-11 fragment
{
  "supported_nips": [29],
  "nip29": { "subgroups": true }
}

Subgroup rules

  • Parenting, re-parenting, and promotion to root all go through a kind:9002 (edit-metadata) event from an authorized admin. Include the desired parent value, or omit the parent tag to detach. The relay then updates the group's kind:39000.
  • The relay also updates the parent's kind:39000 to include the new child as a child tag, and vice-versa. When a group switches parents, the relay updates the previous parent as well.
  • Historical events under the group's h tag stay valid across these transitions.
  • A kind:9002 MAY carry at most one parent tag. The resulting kind:39000 therefore also carries at most one parent tag.
  • Absence of the parent tag makes the group a root.
  • Relays MUST reject a kind:9002 whose parent value would create a cycle, including self-reference.
  • If the declared parent does not exist on the relay, the relay MUST reject the kind:9002.
  • When a parent is deleted with kind:9008, its remaining children automatically become roots.
  • Relays MUST reject any kind:9002 carrying ["parent", "X"] unless the event author is also an admin of X, per X's kind:39001.
  • A parent's kind:39000 MUST include ["child", "<id>"] tags as an ordered list of its children. The position of each tag in the array is the order. By default the relay appends new children as they are created. The parent's admin can rearrange the list with a kind:9002 carrying the desired child tags in order; each edit replaces the previous list. Relays MUST reject a kind:9002 metadata edit that does not include all the children as child tags.
Lifecycle example - re-parenting and promotion
// "social" adopts "nostr" (was under "tech")
→ { "kind": 9002, "tags": [["h", "nostr"], ["name", "Nostr"], ["parent", "social"]] }
// Relay emits:
← { "kind": 39000, "tags": [["d", "nostr"], ["name", "Nostr"], ["parent", "social"]] }
← { "kind": 39000, "tags": [["d", "social"], ["child", "nostr"]] }

// Admin promotes "nostr" to root: edit-metadata without a parent tag
→ { "kind": 9002, "tags": [["h", "nostr"], ["name", "Nostr"]] }
// Relay emits:
← { "kind": 39000, "tags": [["d", "nostr"], ["name", "Nostr"]] }
← { "kind": 39000, "tags": [["d", "social"]] }

Subgroup membership

Each subgroup has its own independent membership. Users join through kind:9021 and kind:9000, like any other group. Nothing is inherited across the parent-child link. Membership does not cascade: parent membership grants no membership in a child. Each group's member list is defined only by its own kind:9000 and kind:9001 events. Admin roles do not inherit either. Each group's kind:39001 is authoritative for its own scope. A parent admin has no authority over a child unless the child's own kind:39001 lists the same pubkey.

This spec defines no automatic propagation of membership between parent and child. A relay that wants such behavior MAY emit the corresponding kind:9000/kind:9001 moderation events itself. Clients MUST NOT assume it: no kind:9000 in a child means the user is not a member of that child, regardless of parent membership.

11 Implementation Notes

Checking your own membership

Look at the latest of either kind:9000 or kind:9001 events for your pubkey in a group. The latest one tells you whether you are currently a member or were removed. If neither exists, assume you are not a member.

Adding yourself to a group

Anyone can send a kind:9021 join request. The relay may generate a kind:9000 immediately, or defer the decision to an administrator. If a group is closed, join requests are not honored unless they include an invite code.

Storing your list of groups

NIP-51 defines kind:10009: a list where clients store the groups a user wants to remember being in. It is also the anchor for fork and migration detection (§2).

12 Worked Example

Alice joins the group pizza on wss://groups.example.com with an invite code, posts a chat message, and later leaves. Orange lines go to the relay. Blue lines come from the relay.

Session transcript - wss://groups.example.com
-- Alice requests to join with the invite code from naddr1...?invite=BEACH --
→ ["EVENT", {"kind":9021,"pubkey":"a1ec3f77…","content":"pineapple tolerated?",
     "tags":[["h","pizza"],["code","BEACH"]], …}]
← ["OK", "8c41d0aa…", true, ""]

-- the relay admits her and mirrors the new state --
← ["EVENT", "sub1", {"kind":9000,"pubkey":"<relay-self-key>",
     "tags":[["h","pizza"],["p","a1ec3f77…","member"],["previous","eb96c864","2db75638","b5d1065f"]], …}]
← ["EVENT", "sub1", {"kind":39002,"pubkey":"<relay-self-key>",
     "tags":[["d","pizza"],["p","f7a2bd4c…"],["p","a1ec3f77…"]], …}]

-- Alice posts a chat message with timeline references --
→ ["EVENT", {"kind":9,"content":"margherita night!",
     "tags":[["h","pizza"],["previous","2db75638","b5d1065f","8c41d0aa"]], …}]
← ["OK", "77b0c2e1…", true, ""]

-- a second join attempt is rejected --
→ ["EVENT", {"kind":9021,"tags":[["h","pizza"]], …}]
← ["OK", "91aa07f3…", false, "duplicate: already a member"]

-- Alice leaves; the relay auto-issues the removal --
→ ["EVENT", {"kind":9022,"content":"diet","tags":[["h","pizza"]], …}]
← ["OK", "cd80413b…", true, ""]
← ["EVENT", "sub1", {"kind":9001,"pubkey":"<relay-self-key>",
     "tags":[["h","pizza"],["p","a1ec3f77…"]], …}]

13 Version History

NIP-29 has been reworked heavily since it landed. Notable changes, newest first:

  • 20262026-08Added the previous tag example and clarified group migration, fork detection via kind:10009, and replica relays.
  • 20262026-07Big feature wave: subgroups with parent/child tags, message pinning (kind:9010 + kind:39005, with a tags), the ?invite= identifier suffix, and the banner metadata tag.
  • 20262026-05Added LiveKit audio/video spaces: the livekit tag, the token endpoint, JWT rules, and kind:39004 participants.
  • 20262026-01Deleted the "unmanaged groups" concept, clarified the relay self key, and dropped a poorly-thought group identifier recommendation.
  • 20252025-12Clarified closed vs private, added the hidden metadata tag.
  • 20242024-11Chat and thread kinds were broken out into their own NIPs; kind:9000 was renamed put-user; duplicate joins now get a duplicate: rejection.
  • 20242024-09Fine-grained permissions were reworked into relay-defined roles; invites simplified into the code tag with kind:9009.
  • 20242024-07Added kind:9007 create-group, kind:9008 delete-group, and kind:9022 leave requests.
  • 20242024-02fiatjaf landed "Simple Groups" as NIP-29 (PR #566), replacing an earlier draft in this slot.
  • Origin2023-11Jon Staab created the file at this number for an image-metadata draft; that content later moved to NIP-92.

Original Proposal - Credits

Authorfiatjaf — PR #566, "NIP-29: Simple Groups" (2024-02-28)
First commit2023-11-27 — "Add imeta tag" (Jon Staab; the slot then held an unrelated draft that moved to NIP-92)
Statusdraft - optional - relay feature — the relay does the enforcement
Source29.md on GitHub - full change history

14 See Also

  • NIP-01 — the event format, addressable events, and the OK rejection prefixes used here.
  • NIP-11 — the relay information document; source of the relay self pubkey and the nip29 subgroups flag.
  • NIP-19 — the naddr encoding used to reference a group.
  • NIP-51 — defines kind:10009, the user's list of groups.
  • NIP-98 — HTTP auth with kind:27235, used for the LiveKit token endpoint.
  • NIP-28 — the older, unmanaged public chat channels this NIP supersedes in practice.