{"record":{"id":"64edf34e85c7faac","repo":"block/buzz","slug":"cannot-remove-the-last-owner","errorCode":null,"errorMessage":"cannot remove the last owner","messagePattern":"cannot remove the last owner","errorType":"exception","errorClass":"anyhow::Error","httpStatus":400,"severity":"error","filePath":"crates/buzz-relay/src/handlers/side_effects.rs","lineNumber":751,"sourceCode":"                    {\n                        return Ok(());\n                    }\n                    Err(anyhow::anyhow!(\"only owner can delete group\"))\n                }\n            }\n        }\n        9022 => {\n            // LEAVE_REQUEST: must be an active member, and cannot be the last owner.\n            let members = state.db.get_members(tenant.community(), channel_id).await?;\n            let actor_member = members.iter().find(|m| m.pubkey == actor_bytes);\n            match actor_member {\n                None => {\n                    return Err(anyhow::anyhow!(\"actor is not an active member\"));\n                }\n                Some(m) if m.role == \"owner\" => {\n                    let owner_count = members.iter().filter(|m| m.role == \"owner\").count();\n                    if owner_count <= 1 {\n                        return Err(anyhow::anyhow!(\"cannot remove the last owner\"));\n                    }\n                }\n                _ => {}\n            }\n            Ok(())\n        }\n        _ => Ok(()),\n    }\n}\n\n/// Emit a system message (kind 40099) signed by the relay keypair.\n///\n/// `idempotency_ts` is used as the event's `created_at`. Passing a stable\n/// timestamp (e.g. from the outbox row's `created_at`) makes re-tries produce\n/// the same Nostr event ID — the existing `ON CONFLICT DO NOTHING` in\n/// `insert_event` then provides DB-enforced delivery idempotency.\n///\n/// Returns `Err` if the event could not be durably inserted; fanout remains","sourceCodeStart":733,"sourceCodeEnd":769,"githubUrl":"https://github.com/block/buzz/blob/eed74bde2f4797714335ac10c56c0b0244c1def4/crates/buzz-relay/src/handlers/side_effects.rs#L733-L769","documentation":"Buzz-relay rejects a kind 9022 (LEAVE_REQUEST) when the actor is an owner and the channel has owner_count <= 1. The guard counts members with role == \"owner\" and refuses to let the last one walk away, preventing an unadministerable channel. The event is rejected during side-effect validation and nothing is mutated.","triggerScenarios":"The sole owner of a channel publishing kind 9022 against that channel. Happens when every other owner has already left, or the channel was provisioned with exactly one owner (solo community, demo channel).","commonSituations":"Single-admin communities where the creator tries to leave or delete their presence; account-cleanup automation that leaves all channels including owned ones; two-owner channels where one owner already left and the second now tries.","solutions":["Promote another active member to the owner role first (admin membership/role update event), wait for it to land, then publish the leave request","If the channel should not exist at all, delete it via kind 9008 (DELETE_GROUP) as owner instead of leaving it","Verify owner count via the member list (`buzz channels members`) before sending the leave"],"exampleFix":"// before: sole owner leaves directly\nrelay.publish(EventBuilder::leave_request(channel_id, keys)).await?;\n// -> rejected: cannot remove the last owner\n\n// after: check owner count and transfer first\nlet members = client.query_members(channel_id).await?;\nlet owner_count = members.iter().filter(|m| m.role == \"owner\").count();\nif owner_count <= 1 {\n    // promote another member to owner, or delete the channel (kind 9008)\n} else {\n    relay.publish(EventBuilder::leave_request(channel_id, keys)).await?;\n}","handlingStrategy":"validation","validationCode":"// Before publishing kind 9022 as an owner:\nlet members = client.query_members(channel_id).await?;\nlet owner_count = members.iter().filter(|m| m.role == \"owner\").count();\nlet actor_is_owner = members.iter().any(|m| m.pubkey == actor && m.role == \"owner\");\nif actor_is_owner && owner_count <= 1 {\n    // promote another member to owner first, or delete the channel (kind 9008)\n}","typeGuard":"fn would_remove_last_owner(members: &[Member], actor: &str) -> bool {\n    let actor_is_owner = members.iter().any(|m| m.pubkey == actor && m.role == \"owner\");\n    let owner_count = members.iter().filter(|m| m.role == \"owner\").count();\n    actor_is_owner && owner_count <= 1\n}","tryCatchPattern":"if err.to_string().contains(\"cannot remove the last owner\") {\n    // surface a prompt: transfer ownership or delete the channel — do not auto-retry\n}","preventionTips":["Provision channels with at least two owners if any owner may leave","Check owner_count from a live member query before sending a leave","Prefer channel deletion over last-owner leave when the channel is being decommissioned"],"tags":["nostr","channel-ownership","kind-9022","business-rule","buzz-relay"],"backgroundTag":"cannot-remove-last-owner","analyzedSha":"eed74bde2f4797714335ac10c56c0b0244c1def4","analyzedAt":"2026-08-20T04:38:24.874Z","contentChangedAt":"2026-08-20T04:38:24.874Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}