Files
buzz/NOSTR.md
Logan Johnson 25bcb791b4 fix(relay): show only legacy channel types to reads that name no channel (#8469)
🤖
## Summary
Every Buzz channel has a **type**, such as an ordinary chat channel
(`stream`), a forum, a DM or a workflow. A client app can only show the
types it was built to understand. This PR makes the relay (the Buzz
server) follow one rule everywhere: **a client sees a channel only if it
knows that channel's type.** #8406 applied this rule to membership
notices. This PR applies it to the reads that list channels without
naming one: the channel list, "channels I belong to", the sidebar's
unread list and artifact lists.

Before, these reads left out one type by name: system channels. Now they
return only the four types that every client has always known: `stream`,
`forum`, `dm` and `workflow`. Today the result is the same for every
user, because `system` is the only other type.

### Backward compatibility: old clients stay safe
- A client that was released before a new channel type exists will never
receive channels of that type in its lists. It cannot show such a
channel correctly, so it does not get one.
- Requests that old clients send today return exactly what they returned
before. Nothing changes for them now or when a type is added later.

### Forward compatibility: new types can be added safely
- Before, the rule was a deny list: "everything except `system`". Each
new type would appear in every deployed client by default, unless
someone remembered to update five separate checks in the relay. A missed
check would show the new type to clients that cannot handle it.
- Now the rule is an allow list that is frozen: the four original types.
A new type is hidden by default, with no relay change needed. Clients
that understand it can request it on purpose, the same way current
clients request system channels today. Each new type gets its own
opt-in, so adding a type cannot break clients that are already in use.

## Details
- One frozen list, `buzz_core::channel::LEGACY_CHANNEL_TYPES`.
Membership notices (#8406) now use the same constant, so channel reads
and notices cannot drift apart. The duplicate
`LEGACY_NOTICE_CHANNEL_TYPES` is removed.
- One SQL helper, `legacy_channel_type_sql`, builds `channel_type IN
('stream', 'forum', 'dm', 'workflow')` from that list. It replaces the
five `<> 'system'` checks:
- `get_accessible_channel_ids` (both halves): the implicit channel scope
of any filter without `#h`, for example kind 39000 channel metadata,
REQ, COUNT and search;
  - `get_accessible_channels`: the channel list;
  - the personal-read sidebar roster;
  - artifact queries.
- A filter that names a channel (`#h`, or `#d` on kinds 39000–39003)
reads it under the ordinary rules, whatever its type: the reader is a
member, or the channel is open. The uncached named-channel lookup,
`get_readable_named_only_channels`, now returns every readable
non-legacy channel with its type, not only system channels. Before, a
non-member could not read an open later-type channel, and `#d` read
nothing. Workflow run reads and thread windows use the same lookup.
- The relay keeps each named channel in the scope of the filter that
names it only. This also holds when it re-checks membership for a named
channel that the cache lacks.
- The opt-in to system channels (`#t:["system"]` or `#P` on kinds
39000–39003) adds readable **system** channels only. It never adds a
channel of another type, even one that a sibling filter in the same
request names. The relay scope type `NamedOnlyChannels` keeps the type
with each channel, so the opt-in can tell them apart.
- NIP-CL and NOSTR.md state the rule.

### Related issue
Follows #8406 (the same rule for membership notices).

### Testing
New Postgres tests use a scratch database with a `future_type` value
added to `channel_type`, as a later migration would add one:
- `reads_that_name_no_channel_leave_out_a_type_added_later`: the channel
scope, the channel list and the sidebar roster leave out the later type
and keep the legacy types.
- `artifacts_of_a_later_channel_type_are_read_only_by_name`: artifact
current and history queries and counts without `#h` leave out the later
type. With `#h` naming the channel, they return it.
- `channels_of_a_later_type_are_read_only_by_name`: production read
paths (WS REQ, WS COUNT, HTTP `/query` and `/count`). A member reads a
private later-type channel and anyone reads an open one, by `#h` and by
`#d`; an outsider cannot read the private one; a read that names no
channel returns neither. In one request, a filter that names the channel
plus a `#P` or `#t:["system", <label>]` sibling: the sibling does not
get it.
- `a_named_channel_of_a_later_type_stays_in_its_named_filter`: the
membership re-check for a channel joined mid-request, with an unnamed
sibling and a `#P` sibling. Only the naming filter gets the channel.

Each test fails when its predicate goes back to `<> 'system'`, when the
named lookup loads only `system`, or when the opt-in adds every
named-only channel. The #8406 notice tests and the system channel
Postgres tests pass on this branch.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-10-20 11:04:29 -04:00

23 KiB
Raw Blame History

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

# 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 <channel-uuid> 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", <channel_type>], ["P", <creator>] and ["created_at", <unix seconds>] 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":["<uuid>"]} → 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","<root>","","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):
    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 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=<uuid>, name, closed, t=<channel_type>, P=<creator>, created_at=<unix seconds> (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=<uuid>, t=<channel_type>, P=<creator>, p tags with role label (owner, admin) Admin list
39002 d=<uuid>, t=<channel_type>, P=<creator>, 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", <unix seconds>]: 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", <label>] tags on kind:9007. On kind:9002 the t tags replace the whole set, and ["t", ""] with no other t tag clears it; a kind:9002 without t tags leaves the labels as they are. Only an owner or admin may change labels. The relay refuses a label set with more than 8 distinct labels, a label that is not 1-64 characters from a-z, 0-9, ., : and -, or a label equal to a channel type name (stream, forum, dm, workflow, system).

System channels. A channel with channel_type system exists only for access control, for example to hold config that only an agent and its owner can read. It is an ordinary channel in every other way, but queries leave it out unless they ask for it:

  • A filter that does not name it, for example a REQ with no #h, a COUNT, a NIP-50 search, or kind:39002 #p:[me], does not return it. Such a filter returns only the legacy channel types (stream, forum, dm, workflow), so a channel type added later is also left out.
  • A filter that names it with #h, or with #d on kinds 39000–39003, reads it under the ordinary rules: the reader is a member, or it is open. A channel of a type added later is read the same way.
  • A filter on kinds 39000–39003 with #t:["system"] or any #P also returns the system channels the reader can read, then matches as usual. It does not return channels of a type added later. For example, {kinds:[39000], #P:[<creator>], #t:[<label>]} finds one creator's labeled channel, system or not. Values in one tag filter combine with OR, so #t:["system", <label>] does not mean "a system channel with this label".

Push notifications skip system channels. Every kind:44100/44101 membership notification carries its channel type as t, and a filter without #t sees only the legacy types, so system channel notices reach only a filter with #t:["system"] (see NIP-CL).

Events are stored channel-scoped so access control applies — private channel member lists are only visible to members. Discover groups via historical REQ:

# All groups you can see
nak req -k 39000 --auth --sec <privkey> ws://localhost:3000

# Specific group's members (filter by d tag)
nak req -k 39002 --tag "d=<channel-uuid>" --auth --sec <privkey> ws://localhost:3000

Note: Channel-scoped storage means live global subscriptions ({kinds:[39000]}) won't receive these via fan-out. Clients discover groups via historical REQ queries. Live push for open-channel discovery is a future enhancement.

Membership Notifications

The relay emits relay-signed notifications when members are added or removed:

Kind Meaning Tags Scope
44100 Member added p = target pubkey, h = channel UUID Community-global
44101 Member removed p = target pubkey, h = channel UUID Community-global

Each notice also has t = the channel type (stream, forum, dm, workflow, system). A filter with #t gets only the types it names. A filter without #t gets only stream, forum, dm and workflow, and so does an untagged notice stored before the tag existed (NIP-CL).

Stored community-globally (channel_id = None inside the connected community) so agents and clients can subscribe without knowing channel UUIDs in advance. Client-submitted kind:44100/44101 events are rejected — only the relay keypair may sign these.

Subscription constraint: Global REQs that can match p-gated kinds (44100, 44101, 1059) must include a #p filter where all #p values match the authenticated pubkey. The relay rejects subscriptions that omit #p or include other pubkeys (prevents eavesdropping on others' membership changes and DMs). Error: restricted: p-gated events require #p matching your pubkey.

nak req -k 44100 -k 44101 --tag "p=<your-hex-pubkey>" \
  --auth --sec <privkey> ws://localhost:3000

Sending Messages

# Send a kind:9 message
nak event -k 9 -c "Hello from NIP-29!" --tag "h=<channel-uuid>" \
  --auth --sec <privkey> ws://localhost:3000

# Subscribe to channel messages
nak req -k 9 --tag "h=<channel-uuid>" --stream \
  --auth --sec <privkey> ws://localhost:3000

# React to a message (#h optional but recommended; channel derived from #e target)
nak event -k 7 -c "+" --tag "h=<channel-uuid>" --tag "e=<message-event-id>" \
  --auth --sec <privkey> ws://localhost:3000

# Subscribe to reactions to channel messages — include #h for live delivery (see note below)
nak req -k 7 --tag "h=<channel-uuid>" --stream \
  --auth --sec <privkey> ws://localhost:3000

# Delete a message (#h optional; #e required; must be self-authored)
nak event -k 5 -c "reason" --tag "h=<channel-uuid>" --tag "e=<message-event-id>" \
  --auth --sec <privkey> ws://localhost:3000

# Create a group
nak event -k 9007 --tag "name=my-channel" --tag "visibility=open" \
  --auth --sec <privkey> ws://localhost:3000

# Search messages (NIP-50)
nak req -k 9 --tag "h=<channel-uuid>" --search "search query" -l 20 \
  --auth --sec <privkey> ws://localhost:3000

# Reply to a message (NIP-10 threading)
nak event -k 9 -c "Reply text" --tag "h=<channel-uuid>" \
  --tag "e=<parent-event-id>;;reply" \
  --auth --sec <privkey> ws://localhost:3000

# Fetch gift-wrapped DMs (NIP-17)
nak req -k 1059 --tag "p=<your-hex-pubkey>" \
  --auth --sec <privkey> ws://localhost:3000

Note: The relay derives a reaction's channel from its #e target (client #h is ignored for channel determination). Reactions to channel-scoped events are therefore channel-scoped. Live fan-out keeps channel-scoped and global subscriptions strictly separate, which means a kinds-only subscription ({"kinds":[7]}) receives none of those reactions — subscribe with {"kinds":[7],"#h":["<channel-uuid>"]} instead. #h matching works whether or not the signed reaction carries an h tag: explicit h tags are matched directly, and tagless reactions match via their stored channel.

Tested Clients (Direct)

Client Platform Evidence Notes
BuzzTestClient Rust (repo) Automated E2E Full NIP-29 flow: discovery (39000/39001/39002), kind:9 send/receive, reactions, deletions, h-tag enforcement
E2E nostr interop Rust (repo) Automated E2E NIP-50 search (3 tests), NIP-10 threads (3 tests), NIP-17 gift wraps (3 tests), DM discovery (1 test)
nak CLI Manual (verified) kind:9 send/recv, NIP-50 search, NIP-10 thread replies, group discovery

Not verified in-repo (anecdotal / expected based on NIP-29 support):

  • Chachi (Web/Mobile) — NDK-based; NIP-29 native
  • 0xchat (Mobile) — NIP-29 native

Relay Membership (NIP-43)

When BUZZ_REQUIRE_RELAY_MEMBERSHIP=true, every authenticated connection is checked against the relay_members table. In today's single-community deployment this is the relay-wide member list; in multi-community mode the same rule is scoped to the host-derived community. Only pubkeys with a row for that community may use that community. The relay owner is bootstrapped automatically from RELAY_OWNER_PUBKEY on startup.

CLI: Managing Members

Use buzz-admin — the operator CLI shipped in the relay image — to manage relay membership. In a Docker Compose deployment, use run.sh:

# Add a member (accepts bech32 npub or 64-char hex; default role: member)
./run.sh add-member npub1abc...
./run.sh add-member <64-char-hex-pubkey>
./run.sh add-member npub1abc... --role admin

# Remove a member
./run.sh remove-member npub1abc...
./run.sh remove-member npub1abc... --role member   # only removes if role matches

# List all members
./run.sh list-members

Or invoke buzz-admin directly inside the container:

docker compose exec relay buzz-admin add-member --pubkey npub1abc...
docker compose exec relay buzz-admin add-member --pubkey npub1abc... --role admin
docker compose exec relay buzz-admin remove-member --pubkey npub1abc...
docker compose exec relay buzz-admin list-members

Exit codes:

Code Meaning
0 Success
1 Validation error (bad pubkey, bad role, usage error)
2 Not found (remove: member does not exist)
3 Cannot remove relay owner (use RELAY_OWNER_PUBKEY to change owner)
4 Role mismatch (--role check failed)
5 DB/Redis/internal error

Required environment variables for member management:

Variable Notes
DATABASE_URL Postgres connection string
REDIS_URL Redis connection string
BUZZ_RELAY_PRIVATE_KEY Hex private key — required to sign kind:13534 events

NIP-43 Admin Events (WebSocket)

Relay membership can also be managed over WebSocket using NIP-43 admin events. These require the sender to be authenticated (NIP-42) as the relay owner or an admin.

Kind Action Required tags
9030 Add member ["p", "<hex-pubkey>"], optional ["role", "member|admin"]
9031 Remove member ["p", "<hex-pubkey>"], optional ["role", "member|admin"]
9032 Change role ["p", "<hex-pubkey>"], ["role", "member|admin"]
9033 Set workspace profile (icon) ["icon", "<https-url or data:image/* URL>"] (empty clears)

Example using nak:

# Add a member (owner or admin must sign)
nak event -k 9030 \
  --tag "p=<target-hex-pubkey>" \
  --tag "role=member" \
  --auth --sec <owner-or-admin-privkey> \
  ws://localhost:3000

# Remove a member
nak event -k 9031 \
  --tag "p=<target-hex-pubkey>" \
  --auth --sec <owner-or-admin-privkey> \
  ws://localhost:3000

# Change a member's role to admin
nak event -k 9032 \
  --tag "p=<target-hex-pubkey>" \
  --tag "role=admin" \
  --auth --sec <owner-or-admin-privkey> \
  ws://localhost:3000

After each add/remove/role-change, the relay publishes a kind:13534 membership list event (relay-signed, NIP-70 protected) that clients can subscribe to:

# Subscribe to the live membership roster
nak req -k 13534 --auth --sec <privkey> ws://localhost:3000

A kind:9033 command similarly makes the relay store the workspace icon (per community) and serve it in the standard NIP-11 icon field of its relay information document. Clients render it in the workspace rail/switcher; anyone can read it (curl -H 'Accept: application/nostr+json' http://localhost:3000), but only admins/owners can set it. Full spec: docs/nips/NIP-WP.md.

Known Limitations

  1. CLI intentionally does not emit kind 8000/8001 deltas — publish_nip43_delta is in-process-only (no Redis hop), so a sidecar call stores but never pushes. The 13534 list snapshot is the authoritative roster and rides Redis to live clients. Do not wire a delta call that passes in-process tests and silently no-ops in the deployed compose exec path.

  2. The custom_created_at = max(now, newest_existing_13534 + 1s) bump defeats same-second domination for serial invocations; it does NOT serialize concurrent CLI processes — two near-simultaneous adds can read the same newest timestamp and collide on the bumped second. run.sh serialization is the guard against parallel adds (e.g. xargs -P). When adding multiple members in a loop, add sleep 1 between invocations.


Relay Environment Variables (NIP-29 relevant)

Variable Required Default Description
BUZZ_PUBKEY_ALLOWLIST ❌ false Enable pubkey allowlist for NIP-42 pubkey-only auth
BUZZ_RELAY_PRIVATE_KEY ❌ random Hex secret key for relay signing (discovery events, system messages)
BUZZ_REQUIRE_AUTH_TOKEN ❌ false Require authenticated NIP-42 for all connections

Security Notes

Direct Path

  • Pubkey allowlist is fail-closed. DB errors deny the connection.
  • API token users bypass the allowlist. The allowlist only gates pubkey-only NIP-42.
  • kind:9 requires #h tag. Messages without a channel-scoped #h tag are rejected.
  • kind:7 derives channel from target. Reactions look up the target event's channel via #e — client-supplied #h tags are ignored. Reactions to unknown events are rejected (fail-closed).
  • kind:5 uses #h if present, but doesn't require it. Deletions validate author-match against target events via #e tags. Only self-authored events can be deleted (admin deletions use kind:9005).
  • Client-submitted kind:44100/44101 rejected. Membership notifications can only be signed by the relay keypair.

Troubleshooting

Direct Path

Symptom Cause Fix
auth-required: verification failed Pubkey not in allowlist (when enabled), or NIP-42 auth failed Add pubkey to pubkey_allowlist table; verify NIP-42 challenge/response
invalid: channel-scoped events must include an h tag kind:9 sent without #h tag Include --tag "h=<channel-uuid>"
invalid: reaction target event not found Reaction references unknown event Ensure the target event exists in the relay
No discovery events Channel is private + you're not a member Join the channel first

Further Reading

  • nostr-protocol/nips — the upstream NIP specifications (NIP-01, NIP-29, NIP-42, and the other NIPs referenced throughout this guide).
  • docs/nips/ — Buzz's own NIP extension documents.
  • ARCHITECTURE.md — event kinds, wire protocol, and relay internals.