# How Buzz Handles Multi-Community Mode and Data Isolation for FTS Queries

> Learn how Buzz ensures data isolation for FTS queries in multi-community mode. Buzz scopes queries to specific community identifiers at the PostgreSQL level, preventing data leaks.

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

---

**Buzz enforces strict data isolation across communities by scoping every Full-Text Search (FTS) query to a specific community identifier at the PostgreSQL database level, ensuring that events and search indexes from one community never leak into another.**

Buzz is an open-source Nostr-based communication platform developed by **block/buzz** that implements **multi-community mode** by binding each community to a unique relay URL and its own isolated data slice. When users switch communities, the desktop, web, and mobile clients re-initialize their entire state to guarantee that only the active community’s events, personas, and FTS indexes remain visible. This architecture prevents cross-contamination of data between distinct community contexts.

## How Buzz Structures Multi-Community Mode

In Buzz, a **community** represents a distinct Nostr relay with its own PostgreSQL schema and file system paths. The system treats the relay URL as the canonical community identifier, passing it through every WebSocket connection, HTTP request, and database transaction. When a user executes a command like `buzz --community https://relay.example.com community switch`, the frontend hook [`useCommunityInit.ts`](https://github.com/block/buzz/blob/main/useCommunityInit.ts) and the backend `resetCommunityState()` function collaborate to purge all community-scoped caches—including agent runtimes, persona catalogs, message threads, and FTS result caches—before loading the new community’s data.

## Three Layers of Data Isolation

Buzz guarantees data isolation through three distinct architectural layers that intercept requests at different stages of the data lifecycle.

### Relay-Level Schema Scoping

At the ingress layer, the `buzz-relay` crate inspects every incoming WebSocket or HTTP request to extract the target relay URL. According to the source code in [`crates/buzz-relay/src/relay.rs`](https://github.com/block/buzz/blob/main/crates/buzz-relay/src/relay.rs) (lines 70-84), the relay uses this URL to invoke `core::db::community_schema`, which selects the appropriate PostgreSQL schema for that specific community before any query executes. This ensures that database connections are physically segregated by schema at the network boundary.

### Retention-Level DB Isolation

Durable storage isolation is handled by the `managed_agents::retention` module in the desktop backend. The function `scoped_retention_db_path` 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 483-516) constructs database file paths that embed the community identifier directly into the directory structure. This guarantees that events arriving from community A are physically written to a distinct path from community B, preventing filesystem-level collisions or misrouting of persisted data.

### FTS Query Isolation at the SQL Layer

The `buzz-search` crate builds a separate FTS index per community and enforces isolation at query time. When a `POST /search` request arrives, the handler defined in [`crates/buzz-search/src/lib.rs`](https://github.com/block/buzz/blob/main/crates/buzz-search/src/lib.rs) (lines 112-138) extracts the active community from the `RequestContext` and prepends a `WHERE community_id = $community` clause to the generated SQL. The query then delegates to PostgreSQL’s `tsvector` engine using `plainto_tsquery`, but only scans documents where the `community_id` column matches the request context.

```rust
pub async fn search_fts(
    ctx: &RequestContext,
    query: &str,
) -> Result<Vec<SearchResult>, SearchError> {
    let community_id = ctx.community_id();               // pulled from the request’s relay URL
    let sql = format!(
        "SELECT * FROM fts_index WHERE community_id = $1 AND document @@ plainto_tsquery($2)"
    );
    sqlx::query_as::<_, SearchResult>(&sql)
        .bind(community_id)
        .bind(query)
        .fetch_all(&ctx.db)
        .await
}

```

## State Reset During Community Switching

Community transitions trigger a coordinated cleanup process to eliminate stale data. The frontend orchestrator [`desktop/src/app/useCommunityInit.ts`](https://github.com/block/buzz/blob/main/desktop/src/app/useCommunityInit.ts) signals the Tauri backend to invoke `resetCommunityState()` 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-36). This function terminates active agent runtimes, closes file handles, and drops in-memory caches before re-initializing the connection pool against the new community’s schema. This reset ensures that indexed search data from the previous community cannot persist in memory or influence subsequent queries.

## Verifying Isolation with Integration Tests

The project validates cross-community isolation through dedicated integration tests located in [`crates/buzz-search/tests/fts_integration.rs`](https://github.com/block/buzz/blob/main/crates/buzz-search/tests/fts_integration.rs) (lines 25-64). These tests seed two separate communities with identical document content and verify that a `search_fts` call scoped to community A returns zero results from community B. The test suite uses `sqlx` to assert that the `community_id` filter effectively partitions the `tsvector` index, providing empirical proof that the isolation logic is impervious to content collisions.

## Practical CLI Examples

You can observe this isolation behavior directly through the Buzz CLI:

```bash

# Switch to a different community (desktop, web, or mobile)

buzz --community https://relay.example.com community switch

# Perform an FTS search limited to the active community

buzz --format compact search "project roadmap"

# The same query on a different community yields a distinct result set

buzz --community https://other-relay.example.com --format compact search "project roadmap"

```

Even when both communities contain identical content matching "project roadmap," the second command returns only the results indexed under `https://other-relay.example.com`, demonstrating the per-community scoping of the FTS engine.

## Summary

- Buzz implements **multi-community mode** by mapping each community to a unique Nostr relay URL and dedicated PostgreSQL schema.
- **Data isolation** is enforced at three layers: relay-level schema selection ([`buzz-relay/src/relay.rs`](https://github.com/block/buzz/blob/main/buzz-relay/src/relay.rs)), retention-level database path scoping (`scoped_retention_db_path`), and FTS-level SQL filtering ([`buzz-search/src/lib.rs`](https://github.com/block/buzz/blob/main/buzz-search/src/lib.rs)).
- Every **FTS query** automatically includes a `community_id` predicate that restricts PostgreSQL’s `tsvector` search to the active community’s index slice.
- Community switching triggers a complete state reset via `resetCommunityState()` and [`useCommunityInit.ts`](https://github.com/block/buzz/blob/main/useCommunityInit.ts), ensuring no cached data leaks between contexts.
- Integration tests in [`fts_integration.rs`](https://github.com/block/buzz/blob/main/fts_integration.rs) empirically verify that cross-community data leakage is impossible even with identical content.

## Frequently Asked Questions

### How does Buzz prevent cross-community data leaks during FTS searches?

Buzz prevents leaks by injecting a mandatory `WHERE community_id = $1` clause into every FTS SQL query before execution. As implemented in [`crates/buzz-search/src/lib.rs`](https://github.com/block/buzz/blob/main/crates/buzz-search/src/lib.rs), the `search_fts` function extracts the community identifier from the request context and binds it as a prepared statement parameter, ensuring PostgreSQL’s `tsvector` engine only scans documents belonging to that specific community.

### What happens to cached data when I switch communities in Buzz?

When switching communities, the UI immediately clears all community-scoped caches including the agent runtime, persona catalog, message threads, and FTS result buffers. The frontend hook [`useCommunityInit.ts`](https://github.com/block/buzz/blob/main/useCommunityInit.ts) and the backend function `resetCommunityState()` in [`desktop/src-tauri/src/managed_agents/retention.rs`](https://github.com/block/buzz/blob/main/desktop/src-tauri/src/managed_agents/retention.rs) orchestrate this purge to guarantee no stale indexes or events persist in memory.

### Where is the community identifier stored during an FTS query?

The community identifier is derived from the request’s relay URL and stored within the `RequestContext` object that accompanies every search request. According to the source code, `ctx.community_id()` retrieves this value in [`crates/buzz-search/src/lib.rs`](https://github.com/block/buzz/blob/main/crates/buzz-search/src/lib.rs), which the handler then binds to the SQL query as a scoping parameter.

### How does Buzz test FTS isolation between communities?

The project includes integration tests in [`crates/buzz-search/tests/fts_integration.rs`](https://github.com/block/buzz/blob/main/crates/buzz-search/tests/fts_integration.rs) that populate multiple communities with identical content and assert that queries remain strictly partitioned. These tests verify that the `community_id` filter in the generated SQL prevents results from one community appearing in another’s search results, even when the underlying text matches exactly.