Skip to content

NIP-29: add subgroups spec#2319

Merged
fiatjaf merged 11 commits into
nostr-protocol:masterfrom
Anderson-Juhasc:nip29-subgroups
Jul 16, 2026
Merged

NIP-29: add subgroups spec#2319
fiatjaf merged 11 commits into
nostr-protocol:masterfrom
Anderson-Juhasc:nip29-subgroups

Conversation

@Anderson-Juhasc

@Anderson-Juhasc Anderson-Juhasc commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Adds a Subgroups section to NIP-29, letting groups be organized hierarchically. No new kinds: it reuses kind:39000/kind:9002 and adds two tag names, parent and child.

  • A parent tag on kind:39000 points at the parent's d identifier; no parent (or an empty one) means the group is a root. Clients build the tree locally from kind:39000 events.
  • Parenting, re-parenting and promotion to root all happen via kind:9002 (edit-metadata). The relay updates the group's kind:39000 and keeps both sides in sync: it adds/removes the matching child tag on the parent (and on the previous parent when moving).
  • The parent's kind:39000 MUST list every child as an ordered ["child", "<id>"] tag; relays reject metadata edits that drop existing children. Order is the tag position; admins can rearrange it.
  • Relationships are relay-enforced, not client-negotiated: a kind:9002 setting ["parent", "X"] is rejected unless the author is an admin of X as well (per X's kind:39001). Editing the child already requires admin on the child, so both sides must consent.
  • Relays MUST reject cycles (including self-reference) and reject a parent pointing at a group that doesn't exist on the relay. When a parent is deleted (kind:9008), its remaining children automatically become roots.
  • Membership and admin roles are fully independent per group: nothing is inherited across the parent-child link. A relay MAY emit its own kind:9000/kind:9001 to cascade members, but clients MUST NOT assume it.
  • The tree is scoped to a single relay; across relays, the kind:39000 from the relay serving the group is authoritative. Relays advertise subgroup support via NIP-11 (nip29: { subgroups: true }).
  • Migration is incremental: an existing group becomes a subgroup just by an admin editing its metadata to add a parent; historical events under its h tag stay valid.

@staab staab left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deletion behavior is going to be confusing for users who use a client that doesn't have subgroups (because they don't see the hierarchy), and won't be actionable for them. I would prefer orphan sub groups to this (and cascading, yeesh). If parent is invalid, the group is not a sub-group. This is a case clients will have to cover anyway, because they might not have a complete list of group events at any given time.

I also don't think implicit membership inheritance is a good idea. I'm working on this here at a relay level, which has the same problem. Policy inheritance should be explicit — since the relay is the one enforcing the policy, if it wants to cascade membership it should update sub-groups to match parent groups instead of expecting clients to infer it (which clients that don't support sub-groups won't).

I think this PR is too complex (just because of the nature of the problem), and something more laissez faire would be better. For example:

39000 events MAY include a `parent` tag. Clients MAY display groups in a hierarchy. Relays MAY implement whatever policy they want for nested groups.

@Anderson-Juhasc

Copy link
Copy Markdown
Contributor Author

That makes sense @staab, I'll simplify it.

@Anderson-Juhasc

Copy link
Copy Markdown
Contributor Author

@staab I think it's following your recommendations now, but one point I'm a little unsure about is how to identify if the relay supports subgroups, maybe using something like this in nip-11:
"nip29": { "subgroups": true }
Any ideas?

@fiatjaf

fiatjaf commented Apr 17, 2026

Copy link
Copy Markdown
Member

I didn't read the original proposal, but I like the way it is now.

@Anderson-Juhasc

Copy link
Copy Markdown
Contributor Author

Another point is how to prevent groups from becoming children without the parent group's permission. Any ideas, @staab, @fiatjaf?

Something like the following might solve the problem, what do you think?

To require explicit approval from the parent: the parent admin issues a kind:9002 event on the parent with a tag of type ["allow-child", ""] or a new event of type kind:9010 adopt-child. Relay only accepts parent: Y in X if there is prior consent from Y.

@staab staab left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much better.

how to identify if the relay supports subgroups

I don't know that it should. NIP 29 in the supported_nips field should be enough; relays that don't support sub-groups should ignore instead of reject parent tags. Required logic should be handled by clients; relays can augment them with additional policy. This is the only way to get full compatibility. Which means this NIP has to fully define parent tag semantics when relays don't support it at all.

Another point is how to prevent groups from becoming children without the parent group's permission.

You're right, this is annoying. Probably leave this up to the relay. I could see this being handled in a few ways:

  • The relay auto-updates the child group's permissions/ownership to match the parent. The original sub-group admin would be basically donating its group to the parent's admin.
  • Leave permissions alone, and just live with weird hierarchies. Probably bad.
  • Don't allow moving groups; force delete/re-create. Only group admins can create child groups.

None of these is very satisfying to me.

Comment thread 29.md Outdated

A group without a `parent` tag is a root group. To build the tree, clients fetch `{"kinds":[39000]}` and assemble it locally: events without a `parent` tag are roots; the rest are grouped under the `d` referenced by their `parent` tag.

Subgroups are self-advertising for readers: if a relay serves `kind:39000` events with `parent` tags, it supports them; otherwise it doesn't, and there is nothing to render hierarchically. Admins creating or re-parenting subgroups probe by attempting the `kind:9002` event — a relay that does not implement subgroups rejects it via the `OK` response.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Admins creating or re-parenting subgroups probe by attempting the kind:9002 event — a relay that does not implement subgroups rejects it via the OK response.

I don't think you can rely on relays not supporting this part of the spec to detect/reject this.

Comment thread 29.md Outdated
### Rules

- A `kind:39000` MUST have at most one `parent` tag. An empty value (`["parent"]` or `["parent", ""]`) is equivalent to the tag being absent and indicates a root group.
- Relays MUST reject any `kind:39000` whose `parent` tag would create a cycle, including self-reference.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably SHOULD, current implementations won't do this.

@1amKhush

Copy link
Copy Markdown

@staab @Anderson-Juhasc Was reading through the thread, had a few suggestions;

  • Imo parent consent and ownership transfer should be stated as relay policy, not a protocol-level guarantee.
  • Also client fallback should be explicit, so if a parent is missing, unknown, or cyclic in the client’s current view - render that group as a root until the graph becomes consistent.
  • Support detection should come from observed 39000 parent metadata, not from expecting a relay to reject 9002 edits, since some relays may ignore unsupported subgroup behavior (though i think this one was pointed out earlier).

@luthen-seas luthen-seas left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spec text looks sound and backwards-compat is clean — zero new kinds, four new tag names (parent, child, closed-children, open-children) all currently unused elsewhere in the registry. The v2 simplification (orphan handling delegated to clients, support self-advertised via observed parent tags) is the right call.

Three small non-blocking suggestions:

1. Replacement-on-touch on child is a real footgun. "If any child tag is present, the relay SHOULD replace the parent's full child list" — a client adding one child must re-send every existing one or silently drop them. This is materially different from how clients usually treat tags. Worth an explicit implementation note plus a worked "add-one-child" example so tooling authors don't get bitten.

2. ["closed-children"] with no child tags = "accepts no children" is correct but easy to misread. A reader can plausibly parse it as "the list is sealed but existing entries remain legitimate," which is the opposite of the intended semantics. A short worked example of a parent that refuses all children would remove the ambiguity.

3. Cross-relay tiebreaker is circular. "When aggregating across relays … the kind:39000 from the relay currently serving the group's events is authoritative" — but a client in discovery mode usually doesn't know which relay is "currently serving." Suggest a deterministic fallback: prefer the most recent created_at for the same d, ties broken by the user's relay preference order from kind:10002.

One nit: parent is a generic token in a global tag namespace. NIP-29's h-scoping contains the collision risk, but a more specific name (e.g. g or parent-h) would future-proof against another NIP reaching for parent in a different domain. Acceptable as-is.

On interop: the subgroups feature is unimplemented today since it's brand new, and broader NIP-29 support outside of pippellia-btc/relay29 is still thin across the libraries and relays I've looked at. Worth tracking adoption alongside merge.

@Anderson-Juhasc

Anderson-Juhasc commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

@luthen-seas I implemented it in this relay: fiatjaf/relay29#21 and in this client https://web.nostrord.com/ behind an experimental toggle.

@luthen-seas luthen-seas left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Anderson-Juhasc — that updates my interop point. The relay29 PR's integration suite khatru29/subgroups_test.go covers the parts I'd most want a reference implementation to nail down: cycle rejection at both RejectEvent and on startup replay against DB-planted rogue events, the replacement-on-touch child semantics (including the bare ["child"] clear marker), admin attestation round-trip, and parent-deletion → child promotion. With that plus the nostrord client behind a flag, the "≥2 client + ≥1 relay" framing is on much firmer ground than I gave it credit for.

Will keep an eye on the thread.

Comment thread 29.md Outdated
Comment thread 29.md Outdated
Comment thread 29.md Outdated
Comment thread 29.md
Comment thread 29.md
Comment thread 29.md Outdated
Comment thread 29.md Outdated
@fiatjaf

fiatjaf commented Jun 4, 2026

Copy link
Copy Markdown
Member

Removed some text, there was too much. Rewrote some of the AI output to be more succinct. And made the "child" tags mandatory and enforced by the relay.

Looks good to me now.

@Anderson-Juhasc

Copy link
Copy Markdown
Contributor Author

Excellent, well-crafted @fiatjaf!

Introduces hierarchical subgroups via a `parent` tag on `kind:39000`,
a `kind:39005` parent manifest for discovery, opt-in membership
inheritance, cascade deletion, and reconciliation rules. Also clarifies
that group existence is authoritative via `kind:39000` only, since
moderation events persist in history after deletion.
- Behavioral relay support detection via probing (kind:9002 with parent tag)
- Bilateral declaration: parent may list accepted children via ["child", "<id>"] tags, with optional ["closed-children"] flag for strict acceptance
- Admin attestation: relay may copy kind:9002 author's pubkey into the parent tag's 3rd element; clients verify it against the parent's kind:39001 admins (Matrix-inspired power-level pattern)
- Client classification: confirmed / unverified / invalid
- Cross-relay scope, attestation revocation, and role inheritance clarified
- Optional polish on child tag: <order> (sibling ordering) and "suggested" flag
…rship independence)

- advertise subgroup support via nip29.subgroups in NIP-11, drop the
  write-based probe; keep passive detection via observed parent tags
- move at-most-one-parent and cycle checks onto the kind:9002 (the
  user-published event) instead of the relay-emitted kind:39000
- reference kind:9008 for parent deletion
- state re-parenting needs no consent from the previous parent
- document the child list as a top-down traversal hint (parent is not
  filterable) and not an authoritative list
- make membership strictly independent: no cascade, no inheritance
@staab

staab commented Jun 8, 2026

Copy link
Copy Markdown
Member

I'm happy with this.

@fiatjaf

fiatjaf commented Jun 16, 2026

Copy link
Copy Markdown
Member

I really don't understand why you use the word "reparenting" here so much. Is this a term an LLM came up with? I don't know what it means. Is it for when you have a child but then you don't have anymore and then you adopt that child again? I don't see why this should be in the spec. "adopting" would be enough and more generic.

Also this last commit breaks things again. If you want to remove a parent you should just send an "edit-metadata" without any parent (since the "edit-metadata" overwrites all the group metadata), the ["parent", ""] syntax is unnecessary and ambiguous.

@Anderson-Juhasc

Copy link
Copy Markdown
Contributor Author

I understood. “Reparenting” was a term suggested by LLM. I was trying to avoid sending all the tags to make the edits. But since kind:9002 includes all tags it doesn't make sense.

@fiatjaf

fiatjaf commented Jun 16, 2026

Copy link
Copy Markdown
Member

Yeah, initially kind:9002 was supposed to only send the deltas, but @staab decided that it shouldn't be like that so we have to follow him. It is simpler this way.

edit-metadata overwrites all group metadata, so removing a parent is
just an edit-metadata that omits the parent tag. Replace the ambiguous
["parent", ""] in the example, and re-send name to show the overwrite.
Reword the "re-parent" comment to "adopt".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Anderson-Juhasc added a commit to nostrord/nostrord that referenced this pull request Jun 16, 2026
…pported

Per nostr-protocol/nips#2319 a relay advertises subgroup support with a top-level
nip29:{subgroups:true} in its NIP-11 document. Parse it into
Nip11RelayInfo.supportsSubgroups (null/false = unsupported). The group sidebar now
shows the subgroups section + add button only when the hosting relay supports
them; otherwise it shows "This relay doesn't support subgroups." (web + Compose).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pablof7z pushed a commit to pablof7z/nostr-multi-platform that referenced this pull request Jun 23, 2026
Add hierarchical subgroups per nostr-protocol/nips#2319. No new event kinds;
the spec adds parent/child tag names on kind:39000 and a NIP-11
nip29.subgroups capability flag. NMP is a client/protocol crate, so this
implements the read side (projections) + publish side (actions) + NIP-11
detection; relay-enforced rules (cycle rejection, bilateral consent,
child-list auto-sync) remain the relay's job.

Read side:
- DiscoveredGroup + JoinedGroup gain parent: Option<String> and
  children: Vec<String> (ordered), folded from kind:39000 via a shared
  kinds::tags helper (one canonical parent/child extractor).
- discovered_groups.fbs + joined_groups.fbs bump schema_version 1->2;
  Rust + Swift FlatBuffers bindings regenerated (flatc 25.12.19).
- PROJECTION_CONTRACT versions bumped 1->2; producer consts regenerated.

Publish side:
- CreatePublicGroupInput gains optional parent (emits ["parent", id]
  on the kind:9002 metadata edit).
- New SetParent action (nmp.nip29.set_parent) for adopt/detach, emitting
  a kind:9002 with only the parent tag (relay retains prior metadata).
- Shared metadata_tags::metadata_edit_tags builder is the single
  canonical kind:9002 authoring path (no fragmentation between create and
  set_parent).
- Client-side self-reference guard (relays MUST reject cycles; fail early).
- New set_parent_action.fbs (N29S) + wire codec; create_public_group.fbs
  gains optional parent (additive, SCHEMA_VERSION stays 1).

NIP-11 detection:
- RelayInfoDoc gains a generic feature_flags: BTreeMap<String, bool>
  carrier (D0-safe: no NIP noun in nmp-core; the "nip29.subgroups" key
  string is authored in nmp-nip11, not the substrate).
- nmp-nip11 parse.rs fills feature_flags from the nip29.subgroups bool.

Verified: cargo test -p nmp-nip29 (195 lib + integration) / nmp-nip11 (22)
  / nmp-core --lib substrate (121) all pass; cargo build --workspace clean;
  doctrine-lint D0 clean on nmp-core + nmp-nip29; all 4 codegen drift
  checks green (producer-consts, swift-flatc, KernelTypes, typed-decoders).
pablof7z added a commit to pablof7z/nostr-multi-platform that referenced this pull request Jun 23, 2026
…etParent action, NIP-11 capability detection

* feat(nmp-nip29): NIP-29 subgroups support (nips PR #2319)

Add hierarchical subgroups per nostr-protocol/nips#2319. No new event kinds;
the spec adds parent/child tag names on kind:39000 and a NIP-11
nip29.subgroups capability flag. NMP is a client/protocol crate, so this
implements the read side (projections) + publish side (actions) + NIP-11
detection; relay-enforced rules (cycle rejection, bilateral consent,
child-list auto-sync) remain the relay's job.

Read side:
- DiscoveredGroup + JoinedGroup gain parent: Option<String> and
  children: Vec<String> (ordered), folded from kind:39000 via a shared
  kinds::tags helper (one canonical parent/child extractor).
- discovered_groups.fbs + joined_groups.fbs bump schema_version 1->2;
  Rust + Swift FlatBuffers bindings regenerated (flatc 25.12.19).
- PROJECTION_CONTRACT versions bumped 1->2; producer consts regenerated.

Publish side:
- CreatePublicGroupInput gains optional parent (emits ["parent", id]
  on the kind:9002 metadata edit).
- New SetParent action (nmp.nip29.set_parent) for adopt/detach, emitting
  a kind:9002 with only the parent tag (relay retains prior metadata).
- Shared metadata_tags::metadata_edit_tags builder is the single
  canonical kind:9002 authoring path (no fragmentation between create and
  set_parent).
- Client-side self-reference guard (relays MUST reject cycles; fail early).
- New set_parent_action.fbs (N29S) + wire codec; create_public_group.fbs
  gains optional parent (additive, SCHEMA_VERSION stays 1).

NIP-11 detection:
- RelayInfoDoc gains a generic feature_flags: BTreeMap<String, bool>
  carrier (D0-safe: no NIP noun in nmp-core; the "nip29.subgroups" key
  string is authored in nmp-nip11, not the substrate).
- nmp-nip11 parse.rs fills feature_flags from the nip29.subgroups bool.

Verified: cargo test -p nmp-nip29 (195 lib + integration) / nmp-nip11 (22)
  / nmp-core --lib substrate (121) all pass; cargo build --workspace clean;
  doctrine-lint D0 clean on nmp-core + nmp-nip29; all 4 codegen drift
  checks green (producer-consts, swift-flatc, KernelTypes, typed-decoders).

* fix: regenerate nip29 flatc bindings with pinned flatc 25.12.19

Regenerates discovered_groups_generated.rs and joined_groups_generated.rs
using the canonical ci/regenerate-flatbuffers.sh script to resolve Rust
flatc drift CI failure on PR #1899.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: add SetParentInput to fail-closed test suite + trailing newlines

Add SetParentInput to both assert_wrong_fid_rejected! and assert_bad_version!
macros in tests_fail_closed.rs, and add the missing import. Fix missing
trailing newlines on 4 files: set_parent_action.fbs, metadata_tags.rs,
set_parent.rs, and subgroups.rs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(nmp-nip29): update ActorCommand import path after 3b-iv seal

Commit 0bd6fdb removed the top-level `pub use actor::ActorCommand`
re-export from `nmp_core`. Update the one remaining old-path import in
`set_parent.rs` to use `nmp_core::actor::ActorCommand`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: pablof7z <marinadiazc@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
@fiatjaf

fiatjaf commented Jul 15, 2026

Copy link
Copy Markdown
Member

I was going to merge this and tried to fix the conflicts using the GitHub interface but I think I broke everything. Can you check, @Anderson-Juhasc?

@fiatjaf
fiatjaf merged commit 223ddb3 into nostr-protocol:master Jul 16, 2026
@Anderson-Juhasc

Copy link
Copy Markdown
Contributor Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants