Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1cc3ha7z055mu0rwwu7806t2wt8mj3pvu0uv5mfp2c50dahaqhczshdalg6 <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1jmc9dt2lyvzu3h0kxlwxt5zg4fxp9476awyxw6gwxn72g6cw7exqs64whm <96f056ad5f2305c8ddf637dc65d048aa4c12d7daeb8867690e34fca46b0ef64c@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
15 KiB
Community-Admin Moderation Plan
Goal (Tyler, 2026-07-01): let community admins moderate their own communities.
Co-authored by Eva + Wren. Grounded against buzz-1321-review @ 86d6388 and
RESEARCH/NOSTR_CONTENT_REPORTING_MODERATION.md. Re-verified against fresh
REPOS/buzz-moderation-ref @ main 1f5ba5b (2026-07-07).
0. DECISIONS LOCKED (Tyler, 2026-07-07, #buzz-moderation thread e2a91af6)
All open calls below are now decided. The UX was refined by Eva + Wren to a
co-signed 9/10+ (Wren's line-cited corner-check: event 295db04e; final
presentation: event 4273365f).
- Roles: owner + admin only. No Moderator tier for now. (Tyler, event
559f838f.) - Reporter identities visible to mods in the queue (never to the reported author).
- Relay-key resolution DM (Tyler, event
34de7dac): on report resolution the relay key authors a regular nostr message stored in the DB. Container (Wren-verified): relay/moderation key creates/reuses a two-party DM channel{mod key, user}via the participant-hash-idempotent DM model (buzz-db/src/dm.rs), emits 39000 discovery (hidden,t=dm,ptags), inserts relay-signed kind-9 withh=<dm_channel_id>. One DM thread per user per community; relay publishes kind-0 "{Community} Moderation" identity.moderation_reportspersistsreporter_pubkey. Same primitive carries actioned-author notices and timeout/ban issuance. Non-replyable v1; replies (v2) route to mod queue as appeals. - Ban/timeout gate at the auth seam (Tyler, event
34de7dac): evaluated after NIP-42 verify, before pubkey allowlist /enforce_relay_membership(handlers/auth.rs— gate goes immediately after ~L91). Banned ⇒OK false "blocked: you are banned from this community"+ immediate WS close, zero further processing (blocked:is the NIP-01 standardized prefix; there is nobanned:prefix; AUTH must be answered withOKper NIP-42). Live ban kill: cluster-wide disconnect-by-pubkey over Redis pub-sub;CLOSED "blocked: ..."per active sub, then socket close (needs a close-by-pubkey API onConnectionManager). NIP-OA: gate checks the authenticating pubkey always + owner pubkey when present — owner ban cascades to agents; agent ban is agent-only; audit records matched principal, client never learns which. Timeout is a write-block, not a connection block:OK false "restricted: you are timed out until <ts>", desktop disables composer with countdown chip. No silent write-drops.
1. Where this sits — two moderation layers, not one
Discord's published model (safety/our-approach-to-content-moderation) is explicitly two-layer, and Buzz should mirror it:
| Layer | Discord | Buzz | Owner | This plan |
|---|---|---|---|---|
| Platform safety | Trust & Safety team: CSAM image-hashing → NCMEC, ML network detection, human investigations | Relay operator: sha-match in S3, NIP-86 ban/takedown, NIP-62 vanish | Block / relay operator | Adjacent — Fizz's MODERATION_SAFETY_SKETCH.md owns it. We cite, don't duplicate. |
| Community moderation | Server owners + volunteer mods: AutoMod keyword/spam filters, Warning System | Community admins/mods: NIP-29 admin actions, report queue, in-community bans | Community admins | THIS PLAN. |
The severe-safety class (CSAM) is never delegated to community admins — it's a platform-level hard-removal + legal-report path. Community moderation is the subjective, per-community rule enforcement layer on top.
2. What PR #1321 already gives us (the foundation)
#1321 is not a moderation PR — it's the multi-tenant substrate that makes per-community moderation safe:
- Every scoped row carries a server-resolved
community_id(never caller-supplied). TenantContextis minted only on the host-resolution path and threaded through every scoped DB read + Redis publish — a proven cross-community isolation fence.
Implication: community-admin moderation authority is naturally tenant-scoped, but
only if target resolution also stays inside the fence. An admin of community A can
never reach community B's content when the query lists/deletes/bans with
for_community(A). The dangerous edge is a new moderation signal whose target is a
bare event id, address, pubkey, or blob hash. Therefore every new moderation table gets
community_id, every new action takes &TenantContext, and every report/label target
is resolved under tenant.community() before it enters a queue or action row. No new
isolation primitive needed; reuse #1321's — but do not add any global target lookup in
the report/label path.
3. What already exists (build-on, don't rebuild)
From crates/buzz-relay/src/handlers/side_effects.rs + buzz-core:
- NIP-29 admin kinds, authorized in
validate_admin_event:9000put-user (add / assign role),9001remove-user (kick),9002edit-metadata,9005delete-event,9008delete-group,9021/9022join/leave request.
- Role model (
buzz-core/src/channel.rs):Owner > Admin > Member > Guest, plusBot.is_elevated()= Owner|Admin gates elevated-role grants. Agent-owner delegation lets the owning human act. - 39000-series group-state mirrors (metadata/admins/members/roles).
- NIP-51 mute list (kind:10000) constant exists — user-level, client-advisory.
4. The gaps (what this plan adds)
Gap A — No distinct Moderator role
Today the only elevated tier is Admin (can manage members + settings). Discord's
model leans on a volunteer-moderator tier that can act on content/users but
cannot reconfigure the community or manage other mods.
Recommendation after corner-check: ship v1 Admin-only for community-wide
moderation, with an explicit capability helper, and defer a distinct Moderator
role until product needs delegated volunteer moderation.
Why not add Moderator in v1:
- Community-wide authority lives in the tenant-scoped
relay_memberstable, not in channelMemberRole. AddingModeratortoMemberRolewould migrate channel-local DB enum/API/UI/projection surfaces without actually creating tenant-wide authority. - Adding
moderatortorelay_members.roleis possible, but still touches admin commands, membership announcements, UI, downgrade paths, and tests. That is extra surface before we have the basic report/delete/ban loop working. - NIP-29 interop is fine either way because roles are relay-defined/arbitrary, but
clients that only reconstruct channel
39001admins will not understand a community-wide moderator unless the relay explains/enforces it. A relay-signed tombstone is clearer than silently adding every community admin/mod to every group. - A capability helper now makes v2 cheap: later
moderatoris a role-to-capability mapping, not an authorization rewrite.
V1 capability grid:
| Capability | Community owner | Community admin | Channel owner/admin | Member |
|---|---|---|---|---|
| Delete any message in community (9005) | ✅ | ✅ | channel only | own only |
| Remove/kick user (9001) | ✅ | ✅ | channel only | self |
| Ban user from community | ✅ | ✅ | ❌ | ❌ |
| Timeout/mute user in-community | ✅ | ✅ | ❌ | ❌ |
| Resolve reports (queue actions) | ✅ | ✅ | optional channel-view | ❌ |
| Assign/revoke community Admin | ✅ | ❌ | ❌ | ❌ |
| Edit community-level settings | ✅ | ✅ | ❌ | ❌ |
| Delete community / hard-delete channel (9008) | ✅ | ❌ | channel owner only where already allowed | ❌ |
Open call for Tyler: is a distinct Moderator tier required in v1? My reviewed lean
is no: ship Admin-only, but structure authorization around capabilities so adding
moderator later is a narrow extension. If Tyler wants Discord-like volunteer mods on
day one, add moderator first to relay_members.role (tenant-level), advertise it in
39003, and only then decide whether channel MemberRole also needs a Moderator.
Gap B — No NIP-56 reports (the report button)
The single biggest missing primitive. kind:1984 is referenced only as a CLI filter example; there is no ingest, queue, or UI. Without it, mods have nothing to act on — they can only delete what they personally see.
Pipeline (all tenant-scoped via #1321's fence; target resolution is the sharp edge):
client "Report" → kind:1984 event (p + e/x tags, report-type, free-text)
│ ingest: validate_report_event (new, in side_effects.rs)
▼
moderation_reports table (community_id, target_event/pubkey/blob,
reporter, type, reason, created_at, status=Open)
│ aggregate by target; weight trusted reporters (mods > members)
▼
per-community moderation queue (buzz-cli / desktop admin surface)
│ mod resolves: dismiss | delete (9005) | remove (9001) |
│ ban | mute | escalate-to-platform
▼
moderation_actions table (audit) + relay-signed tombstone (see Gap D)
Target-resolution rules to keep the tenant fence closed:
etarget: look up the event only withtenant.community(); inferchannel_idfrom that row. If missing in this tenant, reject or store as an unresolved report — never search other tenants by event id.xblob target: resolve through tenant-scoped media references, e.g.(community_id, sha256)or(community_id, event_id, sha256). A bare SHA-256 can be shared across tenants and must not grant cross-tenant visibility/action.p-only target: treat as a community-local report about that pubkey in the current tenant. It cannot imply a platform/global ban.- NIP-32 labels (
1985) in v2 follow the same rule: labels are advisory inputs until their target is resolved under the currentTenantContext.
Key policy (from NIP-56 + the RESEARCH doc): relays should not auto-moderate on random-user reports — they're gameable. Reports are a triage inbox, not an auto-takedown trigger. Exception: trusted-admin reports of CSAM/illegal class can fast-path to platform quarantine (Fizz's layer), not to community-admin discretion.
Gap C — No persistent ban / in-community mute
9001 remove-user is a kick — nothing stops re-join on an open channel. Add:
community_banstable (community_id, pubkey, actor, reason, expires_at NULL=perm).- Enforce at join (
9021/9000self-add) and at event ingest for the community. - Mute/timeout = time-boxed write-block (can read, cannot post) — a softer tool
than ban, matches Discord's timeout. Same table,
muted_untilcolumn.
Gap D — No "Warning System" (why-was-this-removed)
Discord tells users why content was actioned. Buzz already has the right primitive:
handle_delete_event_side_effect soft-deletes the target, then emits a relay-signed
kind 40099 system message with type: "message_deleted", actor, and
target_event_id. Extend that rather than inventing a new 48000-range event for v1.
Recommended shape:
- Public/in-context tombstone: relay-signed kind
40099, rendered as "Removed by a community moderator" without exposing removed content. Add safe fields:action_id,target_event_id,target_author,reason_code, optional sanitizedpublic_reason, and maybeactorif moderator identity is intentionally visible. - Private author notice: separate p-gated/DM notification to the actioned user with the reason, community rule, and appeal/restore path. This is the closer Discord Warning System analog.
- Internal action/audit row: full moderator, report ids, reporter identities, evidence, and unsafe details stay admin-only.
- NIP-32 label: useful in v2 as an advisory/scanner signal, not the authoritative enforcement record. Labels can be consumed by clients; tombstones explain relay actions.
5. Standards to adopt vs. skip (from RESEARCH doc, cited to nostr-protocol/nips)
| NIP | Adopt? | Role in this plan |
|---|---|---|
| NIP-29 groups (9000–9022) | Yes — already have it. Extend authorization for community Admin + ban. | Relay-enforced community roles + admin actions. |
| NIP-56 reports (1984) | Yes — new. Gap B. | User report button; interop-standard so other clients work. |
| NIP-32 labels (1985) | v2. | Distributed/automated moderation output; feeds queue as a signal. Client-advisory blur/hide. |
| NIP-51 mute (10000) | Yes (exists). | User-level self-mute — client-advisory, orthogonal to community moderation. Keep distinct from Gap C's community mute. |
| NIP-86 relay-admin API | Platform layer (Fizz). | Operator ban/takedown at event/pubkey/IP grain. Not community-admin scope. |
| NIP-62 vanish (kind:62) | Platform layer. | GDPR right-to-erasure; MUST fully delete + block rebroadcast. Legal, not community. |
| NIP-72 moderated communities | Skip. | Deprecated upstream in favor of NIP-29 (README marks it unrecommended). |
| NIP-09 deletion (kind:5) | Keep (exists). | Author self-delete only; advisory. Not a moderation tool for others' content. |
6. Phased rollout
Phase 1 — community-admin MVP (this plan's core):
- Add a community moderation capability helper and extend
validate_admin_eventfor communityowner|adminauthority (Admin-only v1 unless Tyler explicitly wantsmoderator). - NIP-56 kind:1984 ingest → tenant-scoped
moderation_reports, with target resolution underTenantContext. moderation_actionsaudit table + extended relay-signed kind40099tombstone for removals.community_bans(+ mute/timeout column); enforce at join + ingest.buzz-cli moderationqueue commands (list/resolve/ban/mute) for admins.
Phase 2 — UX + trust weighting: 6. Desktop/mobile Report button + moderation queue surface. 7. Trusted-reporter weighting; report aggregation by target. 8. Warning-System notifications to actioned users.
Phase 3 — distributed signals: 9. NIP-32 labels (1985) as a queue input + client-advisory hide. 10. Cross-community shared blocklists (opt-in), platform-layer coordination.
7. Open calls for Tyler — ALL DECIDED 2026-07-07, see §0
- Moderator role in v1, or Admin-only first? (§4 Gap A — reviewed lean: Admin-only v1 with capability seams.)
- Ban grain: per-community only, or should a community ban be able to request a platform-level ban for severe cases? (Escalation path to Fizz's layer.)
- Report visibility: are reports mod-only, or does a reporter see resolution?
- Warning System shape: reviewed lean is relay-signed kind
40099tombstone + private author notice; NIP-32 label is v2/advisory (§4 Gap D).
Sources
buzz-1321-review @ 86d6388:crates/buzz-core/src/kind.rs,crates/buzz-core/src/channel.rs,crates/buzz-relay/src/handlers/side_effects.rs.- PR #1321 description (multi-tenant
community_id/TenantContext). RESEARCH/NOSTR_CONTENT_REPORTING_MODERATION.md(cited to nostr-protocol/nips).- Discord: safety/our-approach-to-content-moderation (2024-03-15).
- Fizz's
docs/MODERATION_SAFETY_SKETCH.md(platform media-safety layer, adjacent). - Wren corner-check:
PLANS/COMMUNITY_MODERATION_CORNER_CHECK_WREN.md.