How Buzz Prevents Private Channel Leaks Through Strict Channel Access Control

Buzz prevents private channel leaks by enforcing tenant-scoped membership checks via the ChannelAccessChecker trait, requiring explicit community context for every operation, and validating membership at the relay, database, and cache layers before any content is emitted.

The block/buzz repository implements a multi-layered security architecture that ensures private channels remain isolated to their explicit members. Every read and write operation passes through strict access control gates that validate both OAuth scopes and channel membership within the correct tenant context. This design guarantees that private channel content is never exposed to unauthorized users, even in scenarios involving UUID collisions or malicious API requests.

The Core Channel Access Control Architecture

At the heart of Buzz's privacy guarantees sits the ChannelAccessChecker trait, which defines the contract for all channel access decisions.

The ChannelAccessChecker Trait

The foundational interface lives in [crates/buzz-auth/src/access.rs](https://github.com/block/buzz/blob/main/crates/buzz-auth/src/access.rs). This trait exposes two critical methods:

  • accessible_channel_ids – Returns the set of channels a user may access
  • can_access – Validates membership for a specific channel

Both methods require a TenantContext parameter that supplies the community ID. This requirement ensures that channel lookups are always scoped to the correct community boundary, preventing a user from accessing a channel that belongs to another community even if the UUID collides.

Helper Functions for Read and Write Gates

The same file implements check_read_access and check_write_access, which serve as the primary entry points for API enforcement. Each helper performs a two-step validation:

  1. OAuth scope validation – Verifies the caller possesses Scope::MessagesRead or Scope::MessagesWrite
  2. Membership validation – Invokes checker.can_access to confirm the user is a member of the target channel

If the caller is not a member of a private channel, these functions immediately return AuthError::ChannelAccessDenied. These helpers are invoked by every public API surface that returns channel content, including the WebSocket POST /events endpoint, the HTTP POST /query route, and the internal RPC handlers that feed the desktop UI.

Relay-Side Enforcement for Private Channels

The relay acts as the final gatekeeper, ensuring that no private content enters or leaves the system without proper authorization.

Event Ingest Validation

In [crates/buzz-relay/src/handlers/ingest.rs](https://github.com/block/buzz/blob/main/crates/buzz-relay/src/handlers/ingest.rs) (around line 735), the ingest path checks membership before accepting any ephemeral or media-related events. The handler calls checker.can_access via the same ChannelAccessChecker implementation backed by the database to confirm the author’s membership status.

For non-member requests, the relay short-circuits the event pipeline entirely, returning a 403-style error without ever processing or storing the event payload.

Visibility Tags and Event Filtering

Events carry a visibility tag with values "private" or "public". The conversion logic in nostr_convert.rs interprets this tag, treating any event lacking the tag as public by default. Private events are only emitted to clients that have passed the membership check for that specific channel, ensuring that subscription broadcasts never leak sensitive content.

Multi-Layered Defense Mechanisms

Buzz combines several defensive strategies to eliminate race conditions and cross-tenant leakage vectors.

Tenant Scoping and Community Isolation

As documented in comments at lines 24-31 of the access control file, ChannelAccessChecker requires the community ID for every query. Because channel UUIDs are not globally unique, this tenant scoping guarantees that a lookup never crosses community boundaries, even when identifiers collide across different organizations.

Database-Level Enforcement with Advisory Locks

The database layer in [crates/buzz-db/src/store/channel_members.rs](https://github.com/block/buzz/blob/main/crates/buzz-db/src/store/channel_members.rs) implements per-channel advisory locks and restrictive SQL that restricts reads to members:

WHERE community_id = $1 AND pubkey = $2 AND channel_id = $3

This lock guarantees atomicity for membership updates and read checks, eliminating race conditions that could otherwise expose private content during membership state transitions.

Strict Cache Invalidation Strategy

The relay caches membership checks for 10 seconds in [crates/buzz-relay/src/state.rs](https://github.com/block/buzz/blob/main/crates/buzz-relay/src/state.rs) at line 974. The cache key uses the tuple (community_id, pubkey, channel_id), ensuring that a cached "allowed" entry can never be reused for a different community. When a user's membership changes (such as when they are removed from a channel), the cache is flushed immediately, preventing stale permissions from persisting.

Implementation Examples

The following Rust example demonstrates how a handler verifies read access before returning channel data:

use buzz_auth::access::{check_read_access, ChannelAccessChecker};
use buzz_core::TenantContext;
use nostr::PublicKey;
use uuid::Uuid;

async fn handle_get_channel(
    checker: impl ChannelAccessChecker,
    ctx: &TenantContext,
    pubkey: &PublicKey,
    channel_id: Uuid,
) -> Result<ChannelInfo, AuthError> {
    // Ensure the caller has the MessagesRead scope and is a member
    check_read_access(&checker, ctx, pubkey, channel_id, &[Scope::MessagesRead]).await?;
    // … fetch and return the channel data …
}

On the client side, the desktop application performs pre-flight checks before requesting messages:

import { checkChannelAccess } from '@/api/channel';

async function loadChannelMessages(channelId: string) {
  const ok = await checkChannelAccess(channelId, 'read');
  if (!ok) {
    throw new Error('You are not a member of this private channel');
  }
  // fetch messages via WebSocket / HTTP …
}

Summary

  • Trait-based enforcement: The ChannelAccessChecker trait in buzz-auth requires TenantContext for every lookup, preventing cross-community access
  • Relay gatekeeping: The ingest handler in buzz-relay validates membership before accepting any events, returning 403 errors for non-members
  • Database locks: Per-channel advisory locks and scoped SQL queries in buzz-db ensure atomic membership checks
  • Strict caching: A 10-second cache keyed by (community_id, pubkey, channel_id) prevents permission reuse across tenants while maintaining performance
  • Visibility metadata: Private channels carry explicit visibility tags that the relay uses to filter broadcast emissions

Frequently Asked Questions

How does Buzz prevent UUID collisions from causing private channel leaks?

Buzz requires a TenantContext containing the community ID for every ChannelAccessChecker operation. Because the database queries and cache keys all incorporate community_id, a channel UUID collision across different communities cannot result in unauthorized access. The system treats (community_id, channel_id) as the unique identifier for access control purposes.

What happens when a user is removed from a private channel?

When membership is revoked, the database layer updates the channel membership record and triggers a cache invalidation. The relay's membership cache (defined in buzz-relay/src/state.rs) is flushed for that specific (community_id, pubkey, channel_id) tuple. Subsequent requests by the removed user will fail the can_access check and return AuthError::ChannelAccessDenied.

Where does Buzz cache channel membership checks?

The relay caches membership decisions for 10 seconds in buzz-relay/src/state.rs at line 974. The cache is keyed by the tuple (community_id, pubkey, channel_id), ensuring that cached permissions are strictly isolated to the correct tenant and cannot be applied to different communities or channels.

How does the relay handle events without visibility tags?

According to the conversion logic in the relay's nostr_convert.rs, any event lacking an explicit visibility tag is treated as public by default. Only events explicitly tagged with "private" are subject to the membership checks described above, ensuring backward compatibility while maintaining strict privacy controls for sensitive channels.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →