7.8 KiB
7.8 KiB
NIP-29 alignment: coop vs nostord
Comparison of our NIP-29 implementation (crates/nip29, crates/nip29_ui,
crates/workspace) against nostord
(main @ bfd933e), checked against
NIP-29.
The two clients are architecturally similar: relay-hosted groups keyed by
(relay, id), relay-generated 3900x state events, user moderation events
9000-9022, and a NIP-51 kind:10009 group list. They made different
trade-offs; each has real gaps the other closed.
TL;DR
- We follow the spec more literally where nostord deviates:
previoustimeline refs, membership-from-9000/9001 (the spec's "implementation quirks" section), and fork/migration detection via admins'kind:10009. nostord implements none of those three. - nostord is ahead on product surface: invite code listing/revocation, full
LiveKit AV (token endpoint +
kind:39004), threads UI, subgroup hierarchy management, member rosters as a membership source. - Spec bugs in ours worth fixing: no
39002/39001fallback for membership. (Since this comparison:duplicate:join rejection, 9008 delete-group and reaction aggregation were implemented.)
1. Protocol compliance vs NIP-29
| Spec item | coop | nostord |
|---|---|---|
h tag on all user→group events |
yes, every builder (crates/nip29/src/protocol.rs) |
yes |
| Moderation kinds 9000/9001/9002/9005/9007/9009 | yes, all | yes, all |
| 9008 delete-group | yes, plus confirm dialog | yes |
| 9010 update-pin-list + 39005 | send + parse + UI | stub ("disabled until the pinning backend exists") |
previous tag (last 50 seen, excl. self, ≥3 × 8 chars) |
TimelineWindow (crates/nip29/src/protocol.rs) |
not built, not validated |
| Membership = latest of own 9000/9001 | Membership::from_events |
derived from 39001/39002 rosters + local markers instead |
Join request code tag / naddr1…?invite= |
GroupReference::parse |
yes, plus legacy formats (wss://host'id, ?relay=&group=) |
duplicate: 9021 rejection = already member |
treated as success (mark_member) |
matched by wording, treated as success |
39000 metadata: all flags + supported_kinds + parent/child |
yes, plus banner and content-JSON fallback | yes, minus banner; tags only |
| 39001/39002/39003 state | parsed, admins used for actions | richer use (roster as membership source) |
| 39004 livekit participants + token endpoint (NIP-98 kind 27235) | flag parsed/editable only | full flow incl. /.well-known support probe |
| Kind 10009 list | NIP-44 private section, public-wins, opaque-carry | plus foreign-tag preservation and relay-consensus freshness |
| Fork/migration detection (admins' 10009) | watch_forks + "may have moved" banner |
none |
| Metadata author check (relay-signed 3900x) | parsed from any author | same |
Both clients ignore "clients should also check [previous refs] to keep
relays honest" — fine, it is a SHOULD. NIP-42 auth: we configure
SignerAuthenticator on the SDK client (crates/state/src/lib.rs), nostord
waits for AUTH explicitly before REQs; both work on auth-gating relays.
2. Where coop is ahead (keep these)
- Fork/migration detection. The spec says clients SHOULD (MUST when the
primary relay is down) watch admins'
kind:10009.GroupsRegistry::watch_forks/evaluate_forksdoes this on a 15-minute cycle with a "may have moved" banner and a rejoin button. nostord has nothing here. Known limitation (seedocs/TODO.md): the fork-detection subscription is unscoped, so NIP-29 group relays refuse it by policy; it currently works via generic and gossip relays only. - Membership per the spec quirk. Latest own 9000/9001 decides, with
Refused/Removedstates nostord lacks entirely. - Pinning. Full 39005/9010 round-trip, pin chips, admin toggle.
- Spec-shaped
previousrefs and a metadata parser that tolerates content-JSON variants. - Discovery primitives. Contact-list
10009sweep plus per-relay browse (Discovery,GroupsRegistry::browse). nostord deliberately has no relay browsing (join by address only). The browse core exists even though the UI only wires contact-based discovery.
3. Gaps, prioritized
P1 — spec correctness / interop
done. Aduplicate:join rejectionduplicate:-prefixed publish failure on 9021 now marks the group membership asMember(GroupsRegistry::join_key→Group::mark_member).9008 delete-groupdone.GroupId::delete_groupbuilder,Group::delete_group,GroupsRegistry::delete_group(forgets the group on accept), and an admin-only "Delete group" button with a confirm dialog innip29_ui::actions.Reactions render as message bubbles.done. Kind-7 events no longer become rows:Group::absorbroutes them topush_reaction, which aggregates them per target (Group::reactions);nip29_uirenders a grouped reaction bar under the target message. Deleting an event drops its reactions.
P2 — client-parity features
- Invite management. We create a 9009 code and copy the link, but never subscribe to 9009, so there is no list of active codes and no revoke (nostord: active codes = 9009 minus 9005 revocations, per-code copy and revoke). Natural fit: add 9009 to the group's state subscription and an "Invites" section in the details dialog.
- Membership fallback to rosters. We only derive membership from
9000/9001. If a relay prunes moderation history (or the user was added
without us seeing the 9000), membership shows
Unknownwhile 39002 lists us. Consider nostord's "external add" adoption: own pubkey in 39001/39002 → treat as member (or invite card), without weakening the 9000/9001 override. - LiveKit. The flag is parsed and editable, but joining an AV group does
nothing. At minimum: the
/.well-known/nip29/livekitprobe (204) to gate the toggle, and the token flow (/.well-known/nip29/livekit/<id>with NIP-98 kind 27235 plus a 39004 participant subscription) if AV is on the roadmap. - Subgroup management UI.
parentis set at creation (CreateChannel), andedit_metadataalready carriesparent/childtags, but the edit dialog cannot reparent or reorder children (the spec's 9002 child-list replace semantics). nostord has a full Hierarchy tab (move under / make root / reorder). Nothing reads NIP-11nip29.subgroupsto gate the UI. - No reply/thread UI.
send_messageacceptsrepliesbutGroupPanel::sendalways passes empty; kind 11/1111 render inline only. nostord has reply chips, a threads pane, and NIP-22 tag nesting.
P3 — polish
- 10009 foreign tags. We rebuild only
group/rtags on publish (GroupList::public_tags), dropping anything other clients stored in the event; nostord preserves unknown tags verbatim. Cheap fix, avoids clobbering. - No
previousvalidation on incoming events (spec SHOULD) and no client-side late-publication guard — mostly relay-side concerns. - Self-demotion guard. Member roster UI lives in the sidebar
(
crates/workspace/src/sidebar/group_view.rs: promote/demote/remove) at parity with nostord minus roles beyondadmin(both clients only assign one role). nostord additionally blocks self-demotion/self-removal — worth copying to avoid locking yourself out.
4. Suggested order of work
Fixdone.duplicate:-prefix handling on 9021Exclude/aggregate kind-7 reactions in group rowsdone.Add 9008 delete-group + confirm dialogdone.- Subscribe 9009 → invite list + revoke via 9005 (medium).
- 39002/39001 membership fallback (medium).
- Subgroup reparent/reorder in the edit dialog (medium, uses the existing 9002 path).
- LiveKit probe + token flow (larger; only if AV is in scope).