# Redis Pub/Sub System for Presence and Typing Indicators in Buzz: Architecture and Implementation

> Discover how Buzz uses Redis Pub/Sub for real-time presence and typing indicators. Learn about the architecture and implementation of this ephemeral notification system.

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

---

**Buzz implements real-time presence and typing notifications using a dedicated Redis Pub/Sub layer in the `buzz-pubsub` crate, treating these signals as ephemeral Nostr events that bypass PostgreSQL persistence and are broadcast instantly to WebSocket subscribers.**

The `block/buzz` repository uses this system to power online status badges and "user is typing" hints across community workspaces. By isolating presence data in Redis and leveraging Pub/Sub channels, the relay achieves low-latency updates without burdening the core event store.

## Architecture of the buzz-pubsub Crate

The `buzz-pubsub` crate encapsulates all Redis interactions for transient state. It lives at `crates/buzz-pubsub/` and exposes three primary concerns: key generation, publishing, and subscribing.

### Ephemeral Event Design

Presence updates in Buzz are Nostr events of **kind 200001** (presence) and similar ephemeral kinds for typing. Unlike standard Nostr events, these are intentionally dropped by the relay's ingestion pipeline before they reach PostgreSQL. In [`crates/buzz-relay/src/handlers/event.rs`](https://github.com/block/buzz/blob/main/crates/buzz-relay/src/handlers/event.rs), the handler detects these kinds and routes them to `set_presence` or `clear_presence` on the connection state, ensuring they never trigger database writes.

## Redis Key Design and Namespacing

All presence and typing keys follow a strict multitenant pattern to prevent cross-community leakage.

### Presence Key Generation

The `presence_key` function in [`crates/buzz-pubsub/src/presence.rs`](https://github.com/block/buzz/blob/main/crates/buzz-pubsub/src/presence.rs) generates keys using the format:

```

buzz:{community_id}:presence:{author_pubkey}

```

This concatenation ensures that presence state is scoped to a specific community ID, allowing the same public key to have distinct status values across different workspaces.

### Typing Indicators

Typing events use a parallel namespace (`buzz:{community_id}:typing:{author_pubkey}`). Both prefixes are computed at runtime when the relay processes an incoming event, guaranteeing that temporary typing state cannot collide with presence state.

### Community Isolation

The integration test in [`crates/buzz-test-client/tests/conformance_multitenant.rs`](https://github.com/block/buzz/blob/main/crates/buzz-test-client/tests/conformance_multitenant.rs) explicitly verifies that presence updates published in one community are invisible to subscribers in another, confirming the safety of the Redis key prefixing strategy.

## Publishing and Subscribing to Presence Events

### Creating Presence Events with the SDK

Client applications construct presence updates using the SDK builder defined in [`crates/buzz-sdk/src/builders.rs`](https://github.com/block/buzz/blob/main/crates/buzz-sdk/src/builders.rs). The `build_presence_update` function assembles a Nostr event with kind 20001 and a content field set to `"online"`, `"away"`, or `"offline"`.

```rust
use buzz_sdk::builders::build_presence_update;
use buzz_sdk::signer::sign;

// Construct and sign a presence update
let presence_event = sign(build_presence_update("online")?)?;
client.send_event(presence_event).await?;

```

### Relay Handling and Redis Updates

When the relay receives this event, [`crates/buzz-relay/src/handlers/event.rs`](https://github.com/block/buzz/blob/main/crates/buzz-relay/src/handlers/event.rs) extracts the community ID and author public key, then invokes the presence module. The `set_presence` helper writes the status string to Redis using the namespaced key generated by `presence_key`.

### The Publish Flow

[`crates/buzz-pubsub/src/publisher.rs`](https://github.com/block/buzz/blob/main/crates/buzz-pubsub/src/publisher.rs) exposes the `publish` method, which writes the serialized event to the Redis key and simultaneously pushes a message to the `presence` or `typing` Pub/Sub channel. This dual write ensures that new subscribers querying Redis directly and existing subscribers listening to the channel both receive consistent state.

### Real-Time Delivery to Clients

[`crates/buzz-pubsub/src/subscriber.rs`](https://github.com/block/buzz/blob/main/crates/buzz-pubsub/src/subscriber.rs) maintains long-lived Redis subscriptions. When a message arrives on the `presence` channel, the subscriber forwards it to all WebSocket connections associated with that community, allowing UIs to update badges instantly without polling.

## Querying Presence Without Database Hits

### Synthesized Responses via Redis

When a client executes a `/query` request for presence data, the relay bypasses PostgreSQL entirely. In [`api/bridge.rs`](https://github.com/block/buzz/blob/main/api/bridge.rs), the `synthesize_presence` function calls `buzz_pubsub::get_presence_bulk`, which performs a Redis MGET operation on the precomputed keys for the requested authors.

```rust
// Inside api/bridge.rs
let keys: Vec<String> = authors.iter()
    .map(|pk| presence_key(community_id, pk))
    .collect();
let statuses = buzz_pubsub::get_presence_bulk(&redis_conn, &keys).await?;

```

The bridge then constructs synthetic Nostr events on the fly, returning them to the client as if they were stored in the database, even though they exist only in Redis memory.

### Performance Benefits

By serving presence queries from Redis, Buzz eliminates disk I/O and JSON parsing overhead for high-frequency status checks. This design keeps the PostgreSQL connection pool available for permanent event storage while presence remains a lightweight, in-memory concern.

## Code Examples

**Publishing a typing indicator via internal API:**

```rust
use buzz_pubsub::publisher::Publisher;

let publisher = Publisher::new(redis_pool);
publisher.typing_set(
    community_id,
    author_pubkey,
    "User is typing...",
    Duration::from_secs(5)
).await?;

```

**Subscribing to presence updates in a WebSocket handler:**

```rust
use buzz_pubsub::subscriber::Subscriber;

let mut sub = Subscriber::new(redis_conn);
sub.subscribe(&format!("buzz:{}:presence", community_id)).await?;

while let Some(msg) = sub.next().await {
    let update: PresenceEvent = serde_json::from_str(&msg)?;
    websocket.send(update).await?;
}

```

## Summary

- Buzz isolates presence and typing data in the `buzz-pubsub` crate, using Redis as the sole storage medium for these ephemeral signals.
- Redis keys follow the pattern `buzz:{community_id}:presence/{typing}:{pubkey}`, enforced by the `presence_key` function in [`presence.rs`](https://github.com/block/buzz/blob/main/presence.rs).
- Presence events (kind 20001) are handled in [`event.rs`](https://github.com/block/buzz/blob/main/event.rs) and never touch PostgreSQL; they are written to Redis and published via [`publisher.rs`](https://github.com/block/buzz/blob/main/publisher.rs).
- Subscribers in [`subscriber.rs`](https://github.com/block/buzz/blob/main/subscriber.rs) listen to Redis channels and fan out updates to WebSocket clients in real time.
- Query requests are satisfied by `get_presence_bulk` in [`bridge.rs`](https://github.com/block/buzz/blob/main/bridge.rs), synthesizing Nostr events directly from Redis without database queries.
- Community isolation is guaranteed by key prefixing and verified by [`conformance_multitenant.rs`](https://github.com/block/buzz/blob/main/conformance_multitenant.rs).

## Frequently Asked Questions

### How does Buzz prevent presence data from persisting in the database?

The relay's event handler in [`crates/buzz-relay/src/handlers/event.rs`](https://github.com/block/buzz/blob/main/crates/buzz-relay/src/handlers/event.rs) detects Nostr kind 20001 (presence) and similar ephemeral kinds before they reach the storage pipeline. Instead of inserting them into PostgreSQL, the handler calls `set_presence` on the connection state, which routes the data to Redis via the `buzz-pubsub` crate, ensuring the database remains free of temporary status updates.

### What Redis key pattern does Buzz use for tracking presence?

Buzz uses the format `buzz:{community_id}:presence:{author_pubkey}`, generated by the `presence_key` function in [`crates/buzz-pubsub/src/presence.rs`](https://github.com/block/buzz/blob/main/crates/buzz-pubsub/src/presence.rs). This namespacing ensures that presence states are isolated per community, preventing status leaks between different workspaces even when the same public key participates in multiple communities.

### How does the relay handle presence queries without querying PostgreSQL?

When a client queries for presence, [`api/bridge.rs`](https://github.com/block/buzz/blob/main/api/bridge.rs) invokes `get_presence_bulk` from `buzz-pubsub`, which performs a Redis MGET on the preassembled keys. The bridge then constructs synthetic Nostr events containing the current status strings and returns them directly, bypassing the database entirely and reducing latency to a single round-trip to Redis.

### Is presence data isolated between different communities in Buzz?

Yes. The [`conformance_multitenant.rs`](https://github.com/block/buzz/blob/main/conformance_multitenant.rs) integration test explicitly validates that presence updates published to one community's Redis channel are not visible to subscribers in other communities. This isolation is enforced by the community ID prefix in all Redis keys and the subscription logic that filters channels by community scope.