# Using Third-Party Nostr Clients with Buzz Buzz is a Nostr relay that speaks NIP-29 (relay-based groups) natively. Third-party Nostr clients connect directly to `buzz-relay` using NIP-29 and NIP-42 authentication. The old NIP-28 compatibility proxy has been removed. ## Community scope Buzz treats the relay URL/domain as authoritative for the community. Today's single-relay deployment has exactly one community behind that URL, so existing NIP-29 clients keep using the same WebSocket URL, event kinds, tags, and HTTP/media/git paths. In a multi-community deployment, each community is reached by its own domain or subdomain; the backend resolves the community from the host before handling AUTH, EVENT, REQ, REST, media, git, search, or workflow traffic. The Nostr wire format does not grow a tenant tag. Client-supplied `#h` tags still name channels/groups and are checked against the host-derived community. Events without `#h` — profiles, gift-wrapped DMs, membership notifications, lists, status, long-form notes, workflow/system events, and other "global" streams — are global only inside the connected community. A pubkey can join multiple communities and repost its profile in each one; DMs and profiles do not inherit across community domains. --- ## NIP-29 Direct Connect any NIP-29 client straight to the relay. ### Quick Start ```bash # 1. (Optional) Enable pubkey allowlist — must be set BEFORE relay startup export BUZZ_PUBKEY_ALLOWLIST=true # 2. Start the relay (auto-starts Docker services and runs migrations) just relay & # relay on :3000 # 3. Add a pubkey to the allowlist (if enabled) # Insert directly — there is no CLI command for this yet. PGPASSWORD=buzz_dev psql -h localhost -U buzz -d buzz -c \ "INSERT INTO pubkey_allowlist (pubkey) VALUES (decode('<64-char-hex-pubkey>', 'hex'))" # 4. Connect any NIP-29 + NIP-42 client to ws://localhost:3000 ``` ### What Works | Feature | Status | Notes | |---------|:------:|-------| | **Group chat (kind:9)** | ✅ | Send/receive messages with `#h ` tag | | **Reactions (kind:7)** | ✅ | Standard NIP-25; channel derived from target event's `#e` tag (client `#h` ignored) | | **Deletions (kind:5)** | ✅ | Standard NIP-09; self-authored only. `#h` optional, `#e` required | | **User profiles (kind:0)** | ✅ | NIP-01 metadata; synced to users table (display_name, avatar, about, NIP-05). NIP-05 handles must canonicalize to this relay's domain — off-domain or invalid handles are silently cleared. If a NIP-05 handle collides with another user's (UNIQUE constraint), the handle is skipped but other profile fields (display_name, avatar, about) are still synced. | | **Group creation (kind:9007)** | ✅ | NIP-29; include `name` tag, optional `visibility`, `channel_type` and label `t` tags | | **Add user (kind:9000)** | ✅ | Open: any user, subject to target's `channel_add_policy` (`owner_only`/`nobody` can block). Private: owner/admin only. Self-add bypasses agent policy but not private-channel auth. | | **Remove user (kind:9001)** | ✅ | Self-remove allowed (with last-owner guard). Removing others: owner/admin only. | | **Edit group metadata (kind:9002)** | ✅ | `name`/`about`/label `t` tags: owner/admin. `topic`/`purpose` tags: any member. | | **Admin delete event (kind:9005)** | ✅ | Event author can always delete own. Otherwise owner/admin required. Target must be in same channel. | | **Group deletion (kind:9008)** | ✅ | Owner only. | | **Leave group (kind:9022)** | ✅ | Any member. Last-owner guard prevents orphaned groups. | | **Group metadata (kind:39000)** | ✅ | Relay-signed; always `d`, `name`, `closed`, `["t", ]`, `["P", ]` and `["created_at", ]` tags; `about` only if description non-empty; `private` if applicable; `hidden` for DM channels and private system channels | | **Group admins (kind:39001)** | ✅ | Relay-signed; `d`, channel type `t` and creator `P` tags + `p` tags with roles (`owner`, `admin`) | | **Group members (kind:39002)** | ✅ | Relay-signed; `d`, channel type `t` and creator `P` tags + `p` tags for all members | | **Membership notifications** | ✅ | kind:44100 (added) / kind:44101 (removed); relay-signed, community-global scope (`channel_id=None` inside the connected community) | | **Presence (kind:20001)** | ✅ | Ephemeral; arbitrary status string (truncated to 128 chars); writes to Redis (`set_presence`/`clear_presence` on `"offline"`), then fan-out to local subscribers. In multi-community mode presence is scoped to the connected community. | | **Agent presence (kind:20003)** | ✅ | Ephemeral; an owner's presence for the agents it owns, named by `p` tags (at most 256); `online`/`away` set and `offline` clears each agent's presence as its own kind:20001 would. Not fanned out. | | **Typing indicators (kind:20002)** | ✅ | Ephemeral, not stored; published via Redis pub/sub (multi-node capable unlike presence fan-out) | | **NIP-42 authentication** | ✅ | Proactive challenge; optional pubkey allowlist | | **NIP-11 relay info** | ✅ | `GET /` with `Accept: application/nostr+json` | | **Blossom media** | ✅ | `PUT /media/upload` (BUD-02), `GET /media/{sha256}.{ext}` (BUD-01) | | **NIP-50 search** | ✅ | One-shot search REQs: `{"search":"query","kinds":[9],"#h":[""]}` → relevance-sorted results → EOSE. Not registered as persistent subscriptions. HTTP `POST /query` also accepts `search_sort: "newest"` (advertised as `buzz-search-sort` in NIP-11 `supported_extensions`) for newest-first results, paged with `page`. | | **NIP-10 threads** | ✅ | WS-submitted replies with `["e","","","reply"]` tags create `thread_metadata` atomically. Visible in REST thread queries. Unknown parents rejected. | | **NIP-17 DMs (gift wrap)** | ✅ | kind:1059 accepted with ephemeral signing keys. Stored community-globally (`channel_id=None` inside the connected community). Delivered via `#p`-filtered subscriptions. Not indexed in search. | | **DM discovery** | ✅ | DM creation emits kind:39000 (with `hidden` tag) + kind:44100 membership notifications. NIP-29 clients discover DMs via standard group discovery flow. | | **Join request (kind:9021)** | ✅ | Open channels only. Adds member, emits system message + group discovery events + kind:44100 membership notification. Private channels rejected at ingest. | | **Edits (kind:40003)** | ⚠️ | Works on the wire but Buzz-only — no standard NIP-29 client renders these | | **Rich content (kind:40002)** | ⚠️ | Works on the wire but Buzz-only — no standard NIP-29 client renders these | ### What Doesn't Work | Feature | Status | Why | |---------|:------:|-----| | **Create invite (kind:9009)** | ⚠️ | Accepted and stored, but side-effect handler is deferred (no-op with warning log) | | **Group roles (kind:39003)** | ❌ | Defined in kind registry but not emitted by the relay | | **DMs** | ⚠️ | NIP-17 gift wraps supported; NIP-04/NIP-44 not implemented. kind:10050 (DM relay list) deferred. | ### 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 open the relay to specific external Nostr identities without granting full access. - Users with valid **API tokens** bypass the allowlist. - **Fail-closed:** if the DB lookup fails, the connection is denied. - Default: `false` (all authenticated pubkeys accepted). - Auth failure returns generic `auth-required: verification failed` (no allowlist-specific message). - Manage the allowlist via direct SQL (no CLI command yet): ```sql INSERT INTO pubkey_allowlist (pubkey) VALUES (decode('<64-char-hex-pubkey>', 'hex')); DELETE FROM pubkey_allowlist WHERE pubkey = decode('<64-char-hex-pubkey>', 'hex'); SELECT encode(pubkey, 'hex'), added_at, note FROM pubkey_allowlist; ``` ### Group Discovery The relay emits NIP-29 group state events when channels are created, updated, or membership changes. [NIP-CL](docs/nips/NIP-CL.md) defines the channel identity tags on these events and how clients use them to find channels. All discovery events include a `d` tag set to the channel UUID (NIP-29 addressable event convention): | Kind | Tags | Content | |------|------|---------| | **39000** | `d=`, `name`, `closed`, `t=`, `P=`, `created_at=` (always); `about` (if description non-empty); `private` (if applicable); `hidden` (DM channels and private system channels) | Group metadata. **Note:** `closed` is always emitted per NIP-29 convention (Buzz channels require explicit membership), but open channels are still readable/writable by non-members at runtime. The tag reflects the membership model, not access enforcement. | | **39001** | `d=`, `t=`, `P=`, `p` tags with role label (`owner`, `admin`) | Admin list | | **39002** | `d=`, `t=`, `P=`, `p` tags for all members | Member list | On all three kinds the channel type `t` tag comes first, then one `t` tag per channel label, then `P`. Clients read the first `t` value as the channel type. `P` is the public key that signed the channel's kind:9007. The relay writes it from the database, and it does not change when ownership moves. Uppercase `P` follows NIP-22, NIP-34 and NIP-72, where it names the author of the root object; lowercase `p` already lists members and DM participants on these kinds. kind:39000 also carries `["created_at", ]`: the time the channel was created. The event's own `created_at` is the time of the last edit, so use the tag to find the older of two channels. **Channel labels.** A label groups channels across channel types, for example `["t", "workspace"]`. Set labels with `["t",