Files
buzz/docs/architecture/subscriptions.md
npub1pejagjwzjh7y36gq97hxf62ry75u3s6c6grr0rumx2p4c5qsms9s7jdtgl 23f75dac90 docs: write full documentation content (Phase 2)
Fill all 31 stub pages scaffolded in Phase 1 with content migrated and
verified from the root docs, crate READMEs, and source code:

- getting-started/ (3): installation, quickstart, local relay (TESTING.md)
- architecture/ (8): ARCHITECTURE.md §1–§8, one page per section
- guides/ (9): development, testing, agents, workflows, self-hosting,
  nostr-clients (operational half of NOSTR.md), adding-event-kinds,
  adding-api-endpoints, releasing
- reference/ (5): CLI reference verified against buzz --help and
  crates/buzz-cli/src (18 groups / 87 subcommands; README drift noted),
  configuration, known-limitations (ARCHITECTURE §9 + conformance
  LIMITS.md + doc-debt list), NIPs index (13, NIP-CW canonical),
  design-docs index
- vision/ (6): VISION.md + 5 VISION_*.md migrated with aspirational
  banners and rewritten links

Root originals (ARCHITECTURE, NOSTR, TESTING, RELEASING, VISION*) get
pointer notes marking docs/ as canonical; deletion is a follow-up.
All relative links in the new tree verified to resolve.

Known limitations are documented honestly, never papered over:
rate limiting unenforced, WF-07/WF-08 workflow stubs, buzz-cli README
drift, one-line GOVERNANCE.md.

Note: pre-commit sadscan findings reviewed as false positives (dev-default
postgres URI and NIP-OA test vectors, both already on origin/main).

Co-authored-by: npub1pejagjwzjh7y36gq97hxf62ry75u3s6c6grr0rumx2p4c5qsms9s7jdtgl <0e65d449c295fc48e9002fae64e94327a9c8c358d206378f9b32835c5010dc0b@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1pejagjwzjh7y36gq97hxf62ry75u3s6c6grr0rumx2p4c5qsms9s7jdtgl <0e65d449c295fc48e9002fae64e94327a9c8c358d206378f9b32835c5010dc0b@sprout-oss.stage.blox.sqprod.co>
2026-07-07 12:14:33 -07:00

2.4 KiB

Subscription System

How REQ subscriptions are indexed, access-checked, and fanned out. The registry lives in crates/buzz-relay/src/subscription.rs; Redis-backed cross-node fan-out is in crates/buzz-pubsub.

SubscriptionRegistry

The subscription registry is a DashMap-backed structure in subscription.rs:

pub struct SubscriptionRegistry {
    subs: DashMap<ConnId, HashMap<SubId, SubEntry>>,
    channel_kind_index: DashMap<IndexKey, Vec<(ConnId, SubId)>>,
    channel_wildcard_index: DashMap<Uuid, Vec<(ConnId, SubId)>>,
}

pub struct IndexKey {
    pub channel_id: Uuid,
    pub kind: Kind,
}

Three-Tier Fan-Out

When an event arrives, fan_out consults three indexes in order:

Tier Index Key Use Case
1 channel_kind_index (channel_id, kind) Subs with explicit channel + kind filter — O(1) lookup
2 channel_wildcard_index channel_id Subs with channel but no kinds constraint
3 subs (linear scan) — Global subs (no channel_id) — fallback scan

Global subs (tier 3) are checked for non-channel-scoped events only. Channel-scoped events are delivered exclusively to subscriptions that carry a matching channel_id — global subscriptions are explicitly excluded from channel fan-out as a security boundary.

NIP-01 Edge Cases

  • kinds: [] (explicit empty array) means "match nothing" — NOT a wildcard. Subscriptions with empty kinds are not indexed in either tier 1 or tier 2 and never receive events.
  • kinds absent (no field) means "match all kinds" — indexed in tier 2 (channel wildcard) or tier 3 (global).

REQ Handler Access Control

The REQ handler checks channel access before registering the subscription:

1. Parse filters, extract channel_id
2. Load accessible_channel_ids for this connection's pubkey
3. If channel_id not in accessible_channels → send CLOSED "restricted: not a channel member"
4. Only then: sub_registry.register(conn_id, sub_id, filters, channel_id)

This prevents a race where a non-member receives live fan-out events from a private channel between registration and the access check.

Historical Query (EOSE)

After registering, the REQ handler queries Postgres for stored events matching the filters (up to 500 per filter, hard cap). These are sent as ["EVENT", sub_id, event] frames before ["EOSE", sub_id]. New events arriving after EOSE are delivered via the fan-out path.