# How Buzz Achieves Multi-Community Tenant Isolation: Database, Relay, and Client Architecture

> Discover how Buzz ensures multi community tenant isolation through database, relay, and client architecture. Learn how community_id guarantees data separation across all layers.

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

---

**Buzz enforces strict multi-community tenant isolation by binding every database row, WebSocket connection, and in-memory cache to a non-null `community_id`, ensuring complete data separation between tenants at the database, relay, and application layers.**

Buzz is an open-source Nostr relay and client stack developed by Block that implements rigorous multi-community tenant isolation to prevent cross-tenant data leakage. According to the `block/buzz` source code, the architecture utilizes a pervasive `community_id` identifier that namespaces every persistent row, network request, and runtime state machine. This comprehensive design guarantees that events, rate limits, and agent jobs belonging to one community cannot be accessed, modified, or even observed by another.

## Database-Level Isolation via community_id Schema Design

At the persistence layer, Buzz implements hard tenant boundaries through strict schema constraints. Every tenant-scoped table in [`schema/schema.sql`](https://github.com/block/buzz/blob/main/schema/schema.sql) carries a non-null `community_id` column that forms part of the primary key, ensuring that rows from different communities can never collide or be queried together accidentally.

The schema explicitly requires `community_id NOT NULL` on all tenant-scoped tables, with keys prefixed by this identifier to maintain physical separation at the storage level. As defined in the schema comments at lines 5–17, this constraint is enforced at the database level, not merely in application logic.

Channel entities enforce additional immutability guarantees. Once a channel is created with a specific `community_id` at lines 125–131 of the schema definition, that association becomes permanent. This prevents "re-tenenting" attacks where a channel might be migrated to a different community after creation, which could potentially leak historical messages.

To prevent schema drift, Buzz includes a migration lint harness documented in [`docs/multi-tenant-conformance.md`](https://github.com/block/buzz/blob/main/docs/multi-tenant-conformance.md). This tooling automatically verifies that every new migration maintains the tenant-scoped contract, rejecting any table definition that lacks the required `community_id` column or permits null values where tenant isolation is required.

## Relay-Level Tenant Isolation and Request Context

When the Buzz relay accepts a WebSocket or HTTP connection, it immediately resolves the tenant context before processing any requests. The relay looks up the target community using the `communities.host` field, as demonstrated in [`scripts/start-relay-for-tests.sh`](https://github.com/block/buzz/blob/main/scripts/start-relay-for-tests.sh) at line 112, binding that connection exclusively to a single tenant for its entire lifetime.

This resolution creates a `RequestContext` struct that carries the `community_id` through every subsequent operation:

```rust
// Resolve the community from the host the client connects to.
let community = db::store::community::find_by_host(&state.pool, &host)
    .await?
    .ok_or(Error::CommunityNotFound)?;

let ctx = RequestContext {
    community_id: community.id,
    // other per-request state …
};

```

Rate-limiting logic respects these boundaries strictly. In [`desktop/src-tauri/src/relay_admission.rs`](https://github.com/block/buzz/blob/main/desktop/src-tauri/src/relay_admission.rs) at lines 21–35, rate-limit state is scoped to the active `community_id`. This architectural choice ensures that a 429 rate-limit response triggered by excessive requests from community A will never block legitimate traffic from community B, even when both connect through the same relay instance.

Side effects and published events maintain similar isolation. NIP-43 membership snapshots and other community-specific events, as implemented in [`crates/buzz-relay/src/handlers/community_provisioning.rs`](https://github.com/block/buzz/blob/main/crates/buzz-relay/src/handlers/community_provisioning.rs) at lines 210–221, are published only for the owning community. The relay filters all outgoing messages against the active `RequestContext`, preventing accidental broadcast to wrong tenants.

## In-Memory and Agent Runtime Isolation

Both the desktop Tauri client and managed background agents maintain strictly partitioned memory spaces per community. The retention store in [`desktop/src-tauri/src/managed_agents/retention.rs`](https://github.com/block/buzz/blob/main/desktop/src-tauri/src/managed_agents/retention.rs) (lines 20–38) writes durable events to filesystem paths that incorporate the community's relay URL. This guarantees that events retrieved while community A is active are persisted to a separate directory tree than those from community B, eliminating cross-community persistence leaks.

Agent orchestration enforces isolation at the runtime level. The harness in [`desktop/src-tauri/src/managed_agents/runtime.rs`](https://github.com/block/buzz/blob/main/desktop/src-tauri/src/managed_agents/runtime.rs) (lines 145–156) spawns separate async jobs for each `(agent, community)` pair:

```rust
for community in communities {
    jobs.push((agent_record.clone(), community.relay_url.clone()));
}
runtime.spawn_jobs(jobs);

```

Because each combination runs in isolation, a panic or failure in community A's agent job does not terminate or affect agents running in community B. The runtime maintains independent failure domains per tenant.

The UI layer reinforces these boundaries during community switches. When users change communities in the Tauri desktop application, the React tree remounts with a new `communityKey`, forcing all component state to reset. Module-level caches are explicitly cleared via `resetCommunityState()` calls, as referenced in the community view transition logic.

## Community Provisioning and API Controls

Creating or updating a community occurs through an authenticated HTTP endpoint at `POST /operator/communities`. The provisioning handler in [`crates/buzz-relay/src/handlers/community_provisioning.rs`](https://github.com/block/buzz/blob/main/crates/buzz-relay/src/handlers/community_provisioning.rs) defines the request struct at lines 43–53, which includes the target host and optional initial owner pubkey.

The implementation validates ownership strictly. At lines 274–283, the handler verifies the initial owner's hex pubkey format and writes the community record atomically, ensuring that the `community_id` is established correctly from inception. All subsequent database operations triggered by this community must include the assigned identifier, enforced by the schema constraints described earlier.

## Conformance Testing and Migration Lints

Buzz maintains tenant isolation through automated verification. The [`docs/multi-tenant-conformance.md`](https://github.com/block/buzz/blob/main/docs/multi-tenant-conformance.md) specification defines lint rules that execute against every database migration, verifying that `community_id` columns remain present and immutable where required.

Integration tests simulate hostile cross-tenant scenarios. In [`desktop/src-tauri/src/managed_agents/runtime/tests.rs`](https://github.com/block/buzz/blob/main/desktop/src-tauri/src/managed_agents/runtime/tests.rs) (lines 974–987), the test suite verifies that actions performed in one community do not affect the caches, rate limits, or event stores of another. These tests catch regressions that might otherwise allow isolation boundaries to degrade over time.

## Summary

- **Database schema hardening**: Every tenant-scoped table requires a non-null `community_id` as part of the primary key, enforced by migration lints in [`schema/schema.sql`](https://github.com/block/buzz/blob/main/schema/schema.sql).
- **Request context binding**: The relay resolves tenants via `communities.host` and binds all operations to a `RequestContext` containing the immutable `community_id`.
- **Per-community rate limiting**: Rate-limit gates in [`relay_admission.rs`](https://github.com/block/buzz/blob/main/relay_admission.rs) scope state to the active community, preventing one tenant from exhausting shared relay resources.
- **Agent job isolation**: The runtime spawns separate async jobs for each `(agent, community)` pair, ensuring failures remain contained within a single tenant boundary.
- **Client-side cache separation**: The desktop client uses community-specific storage paths and UI remounting to clear in-memory state when switching contexts.

## Frequently Asked Questions

### How does Buzz resolve which community a connection belongs to?

When a WebSocket or HTTP connection arrives, the relay extracts the host header and queries the `communities.host` column to find the matching tenant. This lookup, shown in [`scripts/start-relay-for-tests.sh`](https://github.com/block/buzz/blob/main/scripts/start-relay-for-tests.sh), returns the `community_id` that populates the `RequestContext` for the entire connection lifecycle. If no host matches, the connection is rejected before any application logic executes.

### What prevents a malicious client from writing events to the wrong community?

The database schema enforces this at the storage layer. Every `INSERT` operation into the `events` table must include a `community_id` that matches the connection's resolved tenant. Because the `community_id` is part of the primary key and marked `NOT NULL`, the database rejects any write attempt that lacks this identifier or attempts to use a different community's ID. Additionally, the relay middleware validates that the `"h"` tag in Nostr events matches the active `community_id` before persisting.

### Do managed agents share state between different communities?

No. The agent runtime in [`managed_agents/runtime.rs`](https://github.com/block/buzz/blob/main/managed_agents/runtime.rs) maintains complete isolation by spawning distinct jobs for each community. Each job receives its own database pool connection scoped to the specific `community_id`, and the retention store writes to community-specific file paths. A crash or resource exhaustion in community A's agents does not affect community B's operations, as they run in separate async tasks with independent error boundaries.

### How does Buzz handle schema migrations without breaking tenant isolation?

The project uses a conformance linting system documented in [`multi-tenant-conformance.md`](https://github.com/block/buzz/blob/main/multi-tenant-conformance.md) that runs against every proposed migration. These lints verify that all new tables include the required `community_id` column with appropriate constraints and that existing tenant-scoped tables remain compliant. Any migration that would allow null values or omit the community identifier is rejected by the CI pipeline before deployment.