Repository navigation
Description
Describe the bug
get_members() in crates/buzz-db/src/channel.rs carries a hidden LIMIT 1000. The relay uses this one function to build the relay-signed kind 39002 (NIP-29 group members) snapshot and to resolve membership/roles in every NIP-29 admin handler, so any channel that grows past 1,000 members silently loses everyone who joined after the first 1,000 rows (ordered by joined_at).
What users actually experience once a channel crosses 1,000 members:
- Member Refine app loading skeletons #1001+ never discovers the channel. Desktop and mobile clients find their channels via
{kinds:[39002], "#p":[me]}. A truncated snapshot means noptag and noevent_mentionsrow for late joiners — the channel simply never appears for them. - Wrong member counts and rosters in every client — the 39002 snapshot is what client member lists render.
- Admin actions fail for late-joining owners/admins. Validators for kinds 9000/9001/9002/9005/9008/9022 resolve the actor's role from the truncated list, so an owner past the cutoff reads as a non-member ("actor not authorized").
- The last-owner guard fails open. If the sole owner sits past row 1,000, the guard preventing removal of a channel's last owner is skipped and the channel can be orphaned with zero owners.
- Silent demotion. A bare kind 9000 PUT_USER for a target past row 1,000 cannot see the target's existing role and rewrites owner/admin down to member.
- Members past the cutoff are evicted as "non-members" when an open channel flips to private.
- Mention/group expansion drops members — workflow messages and agent mentions expand against the roster and cannot see them, and any future channel-wide mention feature would under-notify.
Note: NIP-29 explicitly permits partial 39002 rosters, so this is not a spec violation — it is a Buzz product bug, because Buzz's own clients treat the roster as authoritative.
Steps to reproduce
- Create a channel and insert 1,500 active members (e.g.
generate_seriesintochannel_members). - Call
get_members()or fetch the channel's kind 39002 event. - Only 1,000 members come back; members 1,001–1,500 are missing. As a member past the cutoff, the channel never appears in your client; as a late-joining owner, NIP-29 admin events are rejected.
Expected behavior
The relay roster reflects every active member: complete 39002 snapshots, correct member counts, and role checks that work regardless of join order.
Version and platform
- Buzz version: main (
18eef633d) - OS: n/a (relay/db)
Logs / additional context
- A regression test with a 1,501-member roster confirms the truncation:
assertion failed: left: 1000, right: 1501. - Prior art: closed-unmerged Add channel-wide @everyone and @here mentions #2138 identified this exact bug ("hidden 1,000-row database roster limit") and removed the limit in its branch, but bundled it inside a larger
@everyone/@herefeature that never landed. Duplicate search today: no open issue or PR covers it. - Adjacent ceiling uncovered while fixing:
insert_mentions()binds 6 parameters per row in a single statement and hard-fails past ~10.9k p-tags (Postgres 65,535 bind-parameter cap), with the error swallowed as awarn!— past that size the whole channel's#pdiscovery index silently stops updating. - Known follow-ups surfaced by review, out of scope for the fix but worth tracking:
- 39002 events cross the NIP-11-advertised
max_message_length(512 KiB default) at roughly 6k members. Outbound frames are not size-checked so clients still receive them, but the advertised limit becomes inaccurate. emit_group_discovery_eventsruns on every membership change, so populating an n-member channel emits O(n²) roster tags; a per-channel coalescing/debounce layer would help very large channels.- The post-unarchive resubscribe loop emits one notification per member sequentially; batching would bound unarchive latency on very large channels.
channels.max_membersexists in the schema but is never enforced anywhere.
- 39002 events cross the NIP-11-advertised
Activity
fix(db): return complete channel rosters from get_members
LordMelkor commentedon Jul 27, 2026
🤖 Two corrections to the follow-up list above, after verifying against sources:
The NIP-11
max_message_lengthconcern is a non-issue — drop it. NIP-11 defines it as "the maximum number of bytes for incoming JSON that the relay will attempt to decode and act upon." It says nothing about outbound deliveries, so large relay-signed kind 39002 events don't violate the advertisement at any roster size.The O(n²) emission concern is slightly worse than stated: superseded snapshots orphan their mention rows.
replace_addressable_eventsoft-deletes the previous 39002 (UPDATE events SET deleted_at = NOW()) but never removes itsevent_mentionsrows — theDELETE FROM event_mentionscleanup exists only on the hard-deletion path (crates/buzz-db/src/lib.rs:3793). Since a new snapshot is emitted on every membership change, a channel that grows member-by-member permanently accumulates ~n²/2 orphaned mention rows. Reads stay correct (feed queries join throughe.deleted_at IS NULL), so this is table/index bloat and write amplification, not corruption. Minimal fix: delete the superseded event's mention rows inside the same replacement transaction.fix(db): return complete channel rosters from get_members
fix(channels): return complete member rosters (block#5765)
Sync fork main with upstream block/buzz main (#61)
fix(channels): return complete member rosters (block#5765)
fix(channels): return complete member rosters (block#5765)
fix(channels): return complete member rosters (block#5765)
fix(channels): return complete member rosters (block#5765)