# How Buzz Prevents Private Channel Leaks Through Strict Channel Access Control

> Buzz prevents private channel leaks with strict channel access control. Learn how tenant-scoped checks and multi-layer validation ensure your data stays secure.

- Repository: [Block Open Source/buzz](https://github.com/block/buzz)
- Tags: deep-dive
- Published: 2026-08-29

---

**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)](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)](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`](https://github.com/block/buzz/blob/main/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)](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:

```sql
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)](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:

```rust
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:

```typescript
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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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.