How Buzz Achieves Multi-Community Tenant Isolation: Database, Relay, and Client Architecture
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 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. 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 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:
// 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 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 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 (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 (lines 145–156) spawns separate async jobs for each (agent, community) pair:
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 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 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 (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_idas part of the primary key, enforced by migration lints inschema/schema.sql. - Request context binding: The relay resolves tenants via
communities.hostand binds all operations to aRequestContextcontaining the immutablecommunity_id. - Per-community rate limiting: Rate-limit gates in
relay_admission.rsscope 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, 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 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 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →