From 756dd7f65d6f2995e9188a0ffe54294057f8ef4f Mon Sep 17 00:00:00 2001 From: Ziga Drev Date: Sat, 1 Aug 2026 18:33:43 +0200 Subject: [PATCH] docs(nostr): document #h requirement for live reaction subscriptions (#3487) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What this fixes `fan_out_scoped` (`crates/buzz-relay/src/subscription.rs:278-394`) enforces a deliberate, symmetric scoping invariant — documented in the code itself: > Global subscriptions (channel_id = None) do NOT receive channel-scoped events. Channel-scoped subscriptions do NOT receive global events. The relay derives a reaction's stored channel from its `#e` target at ingest — client-supplied `#h` is ignored for channel determination (`NOSTR.md:50` documents this for *writing*). The consequence for *reading* is that every reaction is a channel-scoped event, so a live subscription `{"kinds":[7]}` without `#h` is a global subscription and **silently receives no reactions at all** — no error, no CLOSED, just nothing. The working form is `{"kinds":[7],"#h":[""]}`, and it works regardless of how the reaction was signed: explicit `h` tags on the event are matched directly, and tagless reactions match via the stored channel fallback (`crates/buzz-core/src/filter.rs:78-91` — fallback applies only when the event has no `h` tags; explicit tags are authoritative). `NOSTR.md` already documents this exact pitfall for group-metadata events: > **Note:** Channel-scoped storage means live global subscriptions (`{kinds:[39000]}`) won't receive these via fan-out. (`NOSTR.md:124-126`) …but has no equivalent note for reactions, which is the case a bot/integration author is far more likely to hit: any client that wants to observe approvals/reactions live (workflow reaction-triggers make this a first-class pattern in Buzz) will naturally try a kinds-only REQ first and conclude reactions are broken. We lost real debugging time to exactly this while building a headless integration (https://github.com/OriginTrail/buzz-dkg-integration); the behavior is by design, only the docs are missing. ## What this PR changes Docs only (`NOSTR.md`): a subscribe-to-reactions example in "Sending Messages", plus one note mirroring the existing 39000 note. No code changes. ## How to verify - Behavior: with the relay running, open a live REQ `{"kinds":[7]}` (no `#h`) and react to a channel message from another client → nothing is delivered; re-subscribe with `{"kinds":[7],"#h":[""]}` → the reaction arrives. - Claims against code (verified at `485d03a`): scoping invariant `crates/buzz-relay/src/subscription.rs:386-393`; channel derivation `derive_reaction_channel()` in `crates/buzz-relay/src/handlers/ingest.rs`; `#h` fallback `crates/buzz-core/src/filter.rs:78-91` and its test `h_tag_fallback_uses_stored_channel_id`. Duplicate search: no existing issue/PR found for `reactions subscription`, `fan-out kinds` (searched 2026-07-29). DCO signed-off. --------- Signed-off-by: Žiga Drev Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Co-authored-by: Žiga Drev Co-authored-by: Claude Opus 4.8 Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> --- NOSTR.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/NOSTR.md b/NOSTR.md index 91de6f6c0..cce70f2f7 100644 --- a/NOSTR.md +++ b/NOSTR.md @@ -163,6 +163,10 @@ nak req -k 9 --tag "h=" --stream \ nak event -k 7 -c "+" --tag "h=" --tag "e=" \ --auth --sec ws://localhost:3000 +# Subscribe to reactions to channel messages — include #h for live delivery (see note below) +nak req -k 7 --tag "h=" --stream \ + --auth --sec ws://localhost:3000 + # Delete a message (#h optional; #e required; must be self-authored) nak event -k 5 -c "reason" --tag "h=" --tag "e=" \ --auth --sec ws://localhost:3000 @@ -185,6 +189,14 @@ nak req -k 1059 --tag "p=" \ --auth --sec 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":[""]}` 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 |