Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/block/buzz/llms.txt

Use this file to discover all available pages before exploring further.

A community is the tenant boundary in Buzz: one workspace, one URL, one isolated world of channels, members, profiles, direct messages, repositories, and search. When you deploy Buzz at myproject.com, that domain is the community. Everything inside — every message, every channel, every agent job — belongs to that community and cannot be observed by any other.

The URL is the Community

Buzz treats the relay URL/domain as authoritative for the community. This is not a configuration option — it is enforced at the protocol level before any request handler runs.
req.community = resolve_host(connection.host)
This resolution happens at connection establishment, before AUTH, EVENT, REQ, REST, media, git, search, workflow, or pub/sub handling. An unknown or unmapped host is rejected fail-closed and never falls through to a default tenant. Client-supplied #h tags identify channels inside the host-resolved community; they never select or override which community a connection belongs to.
The community resolver is defined in crates/buzz-core/src/tenant.rs. TenantContext has no Default, no Deserialize, and no way to be parsed from client input — there is no accidental path by which a client can claim a community.

What a Community Owns

A community is a durable row in the communities table. Creating one is an INSERT, never DDL. It owns:

Channels

All stream, forum, DM, and workflow channels. A channel’s community_id is set at creation and immutable — channels can never be re-tenanted.

Membership

The set of admitted pubkeys with their roles (owner, admin, member, guest, bot). Managed via NIP-43 admin events or the buzz-admin CLI.

Signing Keypair

In multi-community deployments, each community has its own signing keypair used for relay-signed events (group metadata, membership notifications, audit entries).

API Tokens

A scoped token namespace for programmatic and guest access. Tokens carry a community stamp; a token from community A is rejected if presented to community B.

Workflows

YAML-as-code automation definitions and execution records, scoped to the community. Triggers only evaluate events within the same community.

Audit Chain

A per-community SHA-256 hash chain of audit entries. Tamper-evident and independently verifiable per community — one chain cannot corrupt another.

Messages

All stored Nostr events in the community-keyed append-only log. Channel-scoped events carry a channel_id; channel-less events (profiles, DMs, long-form) carry only the community_id.

Search Index

The Postgres full-text search index is shared infrastructure, but every query carries community_id — the FTS predicate is BitmapAnd-ed with community-leading btree filters. A search never crosses community boundaries.

Single-Community vs. Multi-Community

The default self-hosted deployment: one host, one relay process, one implicit community. Every connection binds to that community automatically. No behavioral difference from operating without community concepts at all — the community level adds nothing observable at N=1.
# Default startup — one community at localhost:3000
just relay &
Redis keys use the simple unprefixed form:
  • buzz:channel:{uuid} — event fan-out
  • buzz:presence:{pubkey_hex} — presence TTL (180s)
  • buzz:typing:{channel_uuid} — typing sorted set

Access Model

The relay enforces all access control. Channel membership is the only gate.
TypeVisibilityJoinWho Can Create
Open channelsSearchable by all community membersSelf-join (kind:9021)Any member
Private channelsHidden, invite-onlyInvited by owner/admin onlyAny member
DMsParticipants onlyN/A (up to 9 participants)Any member
GuestsScoped to specific channelsInvitedN/A

Pubkey Allowlist

When BUZZ_PUBKEY_ALLOWLIST=true, NIP-42 connections that authenticate with only a pubkey (no API token) are checked against the pubkey_allowlist table. This lets you accept specific external Nostr identities without opening the relay to everyone.
export BUZZ_PUBKEY_ALLOWLIST=true
just relay &

# Add a pubkey to the allowlist (direct SQL — no CLI command yet)
PGPASSWORD=buzz_dev psql -h localhost -U buzz -d buzz -c \
  "INSERT INTO pubkey_allowlist (pubkey) VALUES (decode('<64-char-hex-pubkey>', 'hex'))"
The allowlist is fail-closed: if the database lookup fails, the connection is denied. API token holders bypass the allowlist entirely.

API Tokens and Guest Access

API tokens let guests, investors, partners, or automated systems access specific channels without a Nostr keypair. A token carries a community stamp and an explicit set of channel claims and scopes. Token hash lookup, channel claims, scopes, and revocation are all evaluated under (community_id, token_hash).

Identity: Portable Keys, Per-Community Profiles

Your Nostr keypair is yours across every community you join. Your public key is the same on every relay. What is not shared:
  • Profiles — a kind:0 profile event is channel-less content stored under the community it was submitted to. You can publish the same profile to multiple communities, but profiles do not inherit across community domains.
  • DMs — NIP-17 gift-wrap DMs (kind:1059) are stored community-globally within the connected community (channel_id = NULL). They do not cross communities.
  • Channel memberships — membership in community A’s channels says nothing about community B.
This is the portable-identity / per-community-profile model. Your keypair is self-sovereign and travels with you. What you do within a community stays within that community.

Formal Isolation Proof

Community isolation in Buzz is proven, not just asserted. The docs/multi-tenant-relay.md specification mechanizes isolation in two complementary frameworks:
The TLA+ model encodes tenant isolation as a label-flow invariant: every state element (message row, membership, projection cell, in-flight query, emitted error, audit entry) carries the community label it originated from. The single-run safety invariant is that no high-labeled value ever flows into a low-labeled observation.Five isolation theorems are mechanized and mutation-tested non-vacuous:
TheoremGuarantee
NI (master)Non-interference: B-scoped observations are functions only of B-labeled state
I1 Read confinementEvery row a Serve returns is ctx.community-labeled
I2 Resolution fenceThe h tag never selects the community — only the host does
I3 Write non-lossAccepted appends commit under the resolved label and no other
I4 Fail-closed backstopA dropped application predicate yields ∅ under RLS, not all rows
I5 Admission fenceMembership and channel-less read capability exist only for admitted actors
The Tamarin model proves authorization soundness under a full Dolev-Yao adversary — an attacker who controls the network and can intercept, replay, and forge messages freely.Key theorems:
TheoremGuarantee
S1 Token confinementA token stamped for community A never authorizes in community B
S2 Mint integrityA token only exists as output of a NIP-98 mint by the holder of owner_pubkey
S3 Signing-key non-confusionA community-B-signed system event is never accepted as community A’s
S4 Audit chain integrityPer-community chains are independently verifiable; compromising A’s key doesn’t affect B
S7 NIP-43 admission confinementB’s signing key can never admit a pubkey into A
Every TLA+ invariant and every Tamarin lemma is mutation-tested to confirm it is non-vacuous. Key mutations that are confirmed to go red:
  • M3 — Replacing the composite (community_id, id) uniqueness constraint with a global UNIQUE (id) breaks the existence-oracle closure (I1/C2.1).
  • M9 — Replacing IsAdmitted(community, actor) with AdmittedInAnyCommunity(actor) breaks the admission fence (I5), allowing an actor admitted in A to act in B.
  • M10–M13 — Mutations to the open-community auth, channel-create stamp, feed-read gate, and aux-read gate each go red on their respective traces.

Relay Membership (NIP-43)

When BUZZ_REQUIRE_RELAY_MEMBERSHIP=true, every authenticated connection is checked against the relay_members table for the host-resolved community.
# Manage relay membership via buzz-admin
./run.sh add-member npub1abc...
./run.sh add-member npub1abc... --role admin
./run.sh remove-member npub1abc...
./run.sh list-members

# Or via WebSocket (NIP-43 admin events — owner/admin only)
nak event -k 9030 \
  --tag "p=<target-hex-pubkey>" \
  --tag "role=member" \
  --auth --sec <owner-privkey> \
  ws://localhost:3000
After each membership change the relay publishes a kind:13534 membership list snapshot (relay-signed, NIP-70 protected). Clients subscribe to it for live roster updates:
nak req -k 13534 --auth --sec <privkey> ws://localhost:3000
The relay owner is bootstrapped automatically from RELAY_OWNER_PUBKEY on startup. Use buzz-admin generate-key to generate a keypair for bootstrapping.

Build docs developers (and LLMs) love