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

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, 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 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 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. The build_presence_update function assembles a Nostr event with kind 20001 and a content field set to "online", "away", or "offline".

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 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 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 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, the synthesize_presence function calls buzz_pubsub::get_presence_bulk, which performs a Redis MGET operation on the precomputed keys for the requested authors.

// 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:

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:

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.
  • Presence events (kind 20001) are handled in event.rs and never touch PostgreSQL; they are written to Redis and published via publisher.rs.
  • Subscribers in 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, synthesizing Nostr events directly from Redis without database queries.
  • Community isolation is guaranteed by key prefixing and verified by 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 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. 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 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 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →