Concord and NIP-29: Two Designs for Group Chat on Nostr
- Concord
- NIP-29
- The same questions, opposite answers
- Lurkers, expanded: why there is no list, and where a lurker would show up anyway
- 1. Membership is key possession, and the roster is self-reported
- 2. The invite trail: three key-handoff paths, none of which leave a member-visible record
- 3. The hidden cost: invisibility is symmetric with rekey exclusion
- 4. Where lurkers do show up: below the protocol, at the relay
- 5. Bottom line on lurkers
Two group-chat designs live on nostr today, built from different foundations. Concord (the CORD-01 through CORD-07 documents) derives everything from keys that no server ever holds; NIP-29 makes the relay the enforcer of the group’s rules. This article asks each design the same three questions — what can the relay and members actually see, what does spam control look like, and are push notifications feasible — and analyzes each spec on its own terms. A short closing section looks at how thoroughly the two sets of answers mirror each other, and a final section digs into the strangest consequence of Concord’s design: the lurker.
Concord
Architecture recap, as it bears on these questions
Everything in Concord is a “private stream” (CORD-01): a kind 1059 giftwrap-lookalike, but with the NIP-59 pattern inverted — fixed author, ephemeral p tag, instead of ephemeral author, fixed p. The stream’s author pubkey is derived one-way from a community secret (group_key(label, secret, id, epoch)), so only keyholders can compute the address, and only keyholders can sign valid events at it. Each channel, the control plane, and the guestbook is its own stream address, rotating only when an epoch bumps (i.e., only on removals). Inside the encryption, every message is sealed with the author’s real npub signature.
The trust anchor is key possession, verified by every client. Everything below follows from that.
1. What the relay can see
The relay sees more than “only noise,” and it’s worth being precise about it, because it’s exactly the raw material for both the rate-limiting and push questions.
Per event, the relay sees:
- The stream pubkey (
authorsfield) — meaningless label, but stable for an entire epoch. Epochs bump only on removals (CORD-02 §3), so a healthy community’s channel address can be stable for months or years. - An untweaked
created_at(CORD-01 explicitly does not tweak timestamps, unlike NIP-59’s recommendation — sub-second ordering rides an encryptedmstag instead). So the relay gets second-accurate timing of every message. - Ciphertext size (NIP-44 padding is coarse; long messages are distinguishable from “ok”).
- A random ephemeral
ptag (nothing).
Across events and connections, the relay can derive:
- Per-channel traffic volume and rhythm. One author pubkey = one channel-epoch. Message counts, burst patterns, active hours, approximate channel size by activity.
- Community clustering via subscription filters. A client subscribes with
{"kinds":[1059],"authors":[pk1, pk2, pk3, ...]}listing every plane it holds. That filter hands the relay the grouping: these N addresses belong together (one community: control + guestbook + channels), and this connection is a member of all of them. With NIP-42 AUTH, that links a real npub to the address set; without it, an IP. - Approximate member count by counting distinct connections subscribing to the same address set.
- Call activity: ephemeral 21059 presence heartbeats every 30s at a channel address are a distinctive realtime pattern — the relay can see a call is happening and roughly how many participants, even though it must not store the events.
- Concord usage flags on real npubs: kind 13302/13303 (Community List / Invite List) are signed by the user’s real key — the relay learns “this npub uses Concord and belongs to roughly N communities” (count/size, not which). A k-tagged Direct Invite wrap tells the relay “someone invited this npub to some community” — the spec is upfront about this narrow unmasking (CORD-05 §6).
- Public invite bundles (kind 33301) sit bare on the relay — content encrypted, but their existence and refresh cadence are visible.
One claim worth pushing back on: the camouflage. CORD-01 says stream events “blend in with giftwrap traffic,” and Appendix B forbids outer version tags to preserve that. But the blend is structurally broken by the inversion itself: real NIP-59 giftwraps use a fresh ephemeral author per event — a pubkey that authors two 1059s, let alone ten thousand, is trivially identifiable as a Concord stream (or at least “not a DM”) by any relay running GROUP BY pubkey. What the design genuinely protects is content, membership, and which-community — not the fact that an address is a busy group channel. The threat model holds up fine if you read “camouflage” as “unlinkable to a community,” but not as “indistinguishable from DMs.”
What the relay genuinely cannot see: content, inner authors, the member list, community/channel names, roles, bans, who kicked whom, which epoch-N address succeeded which epoch-N−1 address (rotation unlinkability), and — absent AUTH — who anyone is.
2. What members can see
Inside the keys, transparency is near-total, and the spec is refreshingly honest about it:
- Every author’s real npub — every seal is signed by the real identity (CORD-01). Note this also means messages are non-repudiable among keyholders: your signature over the (sealed) content is right there. If keys ever leak, your authorship is provable to the world. Concord is no worse than NIP-59 here, but it’s the opposite of a deniable-messaging design.
- The complete member list: Guestbook joins/leaves/kicks, coalesced, plus “observably present” authors (CORD-02 §5) — anyone who publishes is on the list even without a Join.
- The full audit log: the edition chains are the log — every ban, kick, grant, rename, forever, signed (CORD-04 §1).
- The banlist, in full, as a plain npub array.
- Invite attribution: a Join can carry which link (and whose) brought the joiner (CORD-02 §5) — social-graph data members see.
- Rekey inclusion: members can compute fellow members’ rekey locators and confirm who was included in a rotation (CORD-06 §2 — acknowledged as an accepted trade for NIP-46 bunker compatibility).
- Presence, typing, call participation with SFU identities.
The most important member-side metadata fact is a negative one: lurkers are invisible. Membership is key possession, there is no enforced list, and “observation” only catches people who publish. Anyone who ever received an invite can silently read everything until a rekey they’re excluded from — and nothing in the protocol can detect them. For a “Discord-style public community” that’s fine (Discord lurkers exist too), but members should understand the roster is a floor, never a ceiling. The final section of this article expands on exactly how far this goes — and where it stops.
Other observers: the voice broker/SFU see IPs, timing, participant counts under a room label that’s stable per epoch (CORD-07 §2 states this cost plainly — a broker can link a channel’s calls “across months”). The broker cannot tell which community a room belongs to, and the SFU forwards only ciphertext. Media servers hosting icons see nothing (encrypted blobs, fetch-side hash check).
3. Spam and relay-side rate limiting
The spec’s own position (CORD-02 §4) is that key derivation is the spam boundary: an outsider can’t even compute a channel’s address, and can’t sign a valid wrap for it without the stream key, so flooding is strictly an insider problem, answered with moderation (a Ban silences instantly, client-side). That’s correct as far as it goes, but there’s a real gap between “silenced” and “stopped,” and the relay’s options are genuinely constrained:
What a relay fundamentally cannot do: per-member rate limiting. All events at a channel share one outer signing key; inner authors are sealed. The relay cannot tell one member’s 1,000 messages from 1,000 members’ one message.
What a relay can do, and the trade-offs:
- Per-connection / per-IP write throttles. The workhorse. Cheap, privacy-preserving, defeats naive floods; rotatable IPs defeat it in turn.
- NIP-42 AUTH + per-npub write quotas. Effective and standard, but the authed npub isn’t bound to the inner author (a member can auth with a throwaway), and it hands the relay the identity↔address-set linkage described above. For a community relay this is probably an acceptable trade; for a general public relay it degrades the privacy story.
- Per-stream-pubkey caps — careful, this one is a griefing weapon. Capping writes per author pubkey caps the whole channel, so one insider spammer exhausts the shared budget and the relay throttles legitimate members. A per-author cap converts an insider spam problem into an insider DoS problem. If used at all, it should be a very high ceiling (circuit-breaker against runaway floods), not a fairness mechanism.
- Paid / allowlisted relays. Honestly the realistic deployment. The spec already pushes each community toward ~5 stable, community-chosen relays (CORD-02 §6). A community relay that requires payment or a shared write credential solves outsider spam entirely and localizes insider abuse to identifiable (paying) connections. This is where the “dumb interchangeable relay” story quietly becomes “the community probably runs or rents its relays” — still no trusted server, but not zero infrastructure either.
- Proof-of-work, and a spec-compatible trick. NIP-13 normally needs a nonce tag, which the no-outer-tags rule forbids (Appendix B). But the wrap’s
ptag is a random ephemeral pubkey with no verified structure — a client can grind that value until the event id has leading zeros. PoW-gated writes with zero protocol change and no new outer metadata. If I were extending the spec, I’d write this up as the anti-spam CORD; it’s the only relay-enforceable, member-blind, identity-free limiter available.
Two gaps the spec’s moderation story doesn’t fully close:
- A banned member keeps burning bandwidth until the Refounding lands. The Ban is a client-side display filter — honest clients decrypt, check the banlist, and drop. The relay can’t distinguish the spammer’s events, so every member still downloads and decrypts the flood. Only the rekey/refounding (rotating the address out from under them) actually stops the traffic, and CORD-06 refoundings are deliberately heavyweight. For a large community with an aggrieved insider, the window between Ban and Refounding is an unthrottleable bandwidth tax on every member.
- Old-epoch addresses are floodable forever. A removed member permanently holds prior epochs’ keys and can write valid events to old addresses indefinitely; clients paginate history “across every epoch pubkey they hold” (CORD-03 §3). The clean client-side fix — which the spec doesn’t state but should — is to bound old-epoch history queries with
until: <rotation time>: nothing legitimate lands at epoch N after epoch N+1 began, so post-rotation writes to dead addresses are simply never fetched. That one line neutralizes the whole vector and would be a worthwhile addition to CORD-06.
4. Push notifications
Not inherently broken — they just have to be the Signal/Matrix “wake and fetch” shape, because nothing else is possible. No server can know a message’s content, author, or even relevance (mentions are inside the encryption), so server-composed notification payloads are off the table by design. What works:
The push-gateway pattern. The device registers its address set (the derived stream pubkeys it holds — the device can compute these; it holds the keys) with a push gateway. The gateway subscribes to the community’s relays with an authors filter, and on each event fires an opaque ping — {event_id, relay} at most — through APNs/FCM/UnifiedPush. The device wakes, fetches, decrypts locally, applies its own logic (mention? muted channel? banned author?), and renders or suppresses the notification. On iOS this lives in a Notification Service Extension sharing the key store with the app, exactly like Signal; on Android, UnifiedPush or an FCM data message.
Privacy cost: the gateway learns your address set and activity timing — which is precisely the metadata the relay already has on any long-lived subscription. A push gateway is metadata-equivalent to a relay; it adds no new class of leak. Self-hosting it, or using your community’s relay operator as the gateway, keeps the exposure inside the existing trust perimeter.
Three Concord-specific frictions:
- Mention filtering can’t be server-side, so busy channels mean busy radios. In a 5,000-member community, every message in every channel is a device wake-up whose relevance is only determinable after local decryption. Discord-scale communities will hurt battery unless clients get aggressive: register push only for DM-ish/small channels, digest-batch at the gateway (“≥1 event at address X in the last 2 min“), or let users opt channels into push individually. This is the real scalability tension — not feasibility, but wake-up economics.
- Rekeys silently break registrations. An epoch bump moves the address; the gateway is watching a dead pubkey. A device offline through a Refounding gets no pushes for the new epoch until it comes online, fetches its rekey blob, and re-registers. Post-ban, everyone’s push goes briefly dark. Clients should treat “re-register push filters” as a mandatory step of rekey processing (worth a sentence in CORD-06, honestly).
- A tempting anti-pattern to avoid: registering by filter you hold keys for is fine; anything where the gateway holds keys to pre-filter is not — that’s just the bot idea below with worse framing.
The bot-as-member idea: it works, and the protocol is almost suspiciously well-shaped for it. A bot is just an npub holding the keys — invite it (Direct Invite, CORD-05 §6, no permission gates key handoff), and it decrypts everything in realtime and can push selectively: mentions only, keywords, per-channel rules — solving the wake-up economics problem the blind gateway can’t. Rekey blobs are even deliberately designed so a NIP-46 bunker-style service can open them with a single nip44_decrypt (CORD-06 §1), and the bot re-follows epoch rotations automatically as long as it’s included in rekeys. Two caveats:
- It’s a full member, trust-wise. A shared community notification bot reads every message of every member — a plaintext-holding single point of the exact kind the protocol was built to delete. And per the lurker property above, if it’s read-only, other members can’t even see it exists. The honest deployment is per-user: your bot, holding your memberships (it could even sync them from your kind 13302 Community List), self-hosted or run by a party you’d trust with your keys anyway. That’s essentially “your always-on device,” which is a coherent trust story. A community-operated bot should at minimum be a visible, disclosed member.
- Refoundings must include it, or it goes dark — same as any member, but nobody notices a dead bot until notifications stop.
Overall read: blind gateway for “something happened” + local decrypt-and-filter is the protocol-pure answer and works today; a per-user keyholding bot is the good-UX answer at large-community scale. They compose — bot for mentions, gateway as fallback.
5. General observations on the spec itself
Strong points. This is a carefully engineered spec, well above the average NIP in rigor. Standouts: the self-certifying community_id (ownership is a hash preimage, unforgeable, serverless); the identity/access key split so removal never destroys identity; the plaintext-seal trick (kind 20014) that lets control-plane compaction re-wrap signed editions across epochs with signatures intact — that’s the load-bearing insight making cheap refoundings possible; frozen byte-exact derivations with a label registry; and unusually disciplined handling of races (refounding collisions converge on lexicographically-lowest key, down-only heal; guestbook clock-skew and tie-grinding bounds; the vac block-until-synced authority citation that kills stale-grant attacks). The “three removals” decomposition (CORD-04 §6) — silence is free, departure is cooperative, severance is cryptographic — is honest in a way most group-chat designs aren’t.
Deliberate trade-offs to be clear-eyed about:
- No forward secrecy or post-compromise security within an epoch — explicitly traded against MLS (README). One compromised device leaks that epoch’s history for every plane it holds keys to, and since the Community List’s seed deliberately retains the earliest epoch for backfill, a compromise tends to leak all history. Right trade for public communities, wrong tool for anything high-stakes.
- Traffic analysis is the accepted cost: stable per-epoch addresses + untweaked timestamps + subscription-filter grouping give relays a rich activity graph, just an anonymous one. The privacy claim is confidentiality and membership-privacy, not observability-privacy.
- No owner succession — lost nsec means the community can only die by Dissolution (acknowledged in CORD-02 §1).
- Scale ceilings from NIP-44’s 64KB envelope: ~500-npub banlist, 50-community list, 400-member snapshot chunks, 120-recipient rekey blobs. Fine for the target size; the banlist sharding is flagged as future work and will be the first wall a genuinely large community hits.
NIP-29
Architecture recap, as it bears on the same questions
NIP-29 is a much shorter spec, built on a single foundational choice: the relay is the server. A NIP-29 group is a set of rules a relay enforces around an arbitrary group id string. Users publish ordinary, plaintext, real-key-signed nostr events carrying an h tag naming the group; the relay accepts or rejects them at write time based on membership state it maintains. That state is itself built from moderation events (kinds 9000–9020: put-user, remove-user, delete-event, etc.) that the relay validates against roles, and the relay publishes the authoritative views — metadata (39000), admin list (39001), member list (39002) — signed by its own master key. Privacy flags (private, hidden, restricted, closed) are relay-enforced access control. There is no encryption anywhere in the design — not at rest, not end-to-end.
The trust anchor is the relay’s word, signed by the relay’s key. Everything below follows from that.
1. What the relay can see
Everything. This isn’t a gap in the design; it’s the design. The relay is a full Discord server in the metadata sense:
- All content, in plaintext, forever, of every group it hosts — including private groups, where “private” means the relay filters reads by others, not that the relay can’t read.
- Every author’s real npub on every message — events are normal signed nostr events.
- The complete social graph of the group: the member list (it maintains it), the admin hierarchy and roles, every join request with its optional reason text, every leave, every invite code and who redeemed it, every moderation action and its stated reason.
- Call participation with real identities: the LiveKit JWT’s identity is the user’s hex pubkey by spec (the
subrequirement), and kind 39004 has the relay actively publishing who is currently in a call. And since the NIP specifies no media encryption, the LiveKit server — typically run by the same operator — can watch and record the calls themselves, not just the metadata. - Plus everything any relay sees: IPs, timing, client fingerprints.
What outsiders and non-members see depends entirely on per-group flags, with permissive defaults: omit private and all messages are publicly readable; omit hidden and the group’s metadata, admin list, and (if published) full member list are public events anyone can fetch. Even a locked-down group leaks its existence and its admin roster unless the operator suppresses those events. And because messages are ordinary signed plaintext events, anyone with read access can copy them out of the relay and republish them anywhere with authorship cryptographically attached — the previous timeline-reference hack tries to stop relays from accepting out-of-context rebroadcasts, but nothing stops screenshots-with-proofs, and 4-byte event-id prefixes chosen by clients is a thin mechanism even for its narrow goal.
What members see: whatever the relay chooses to show them — and notably, possibly less than the truth. Kind 39002 explicitly says clients shouldn’t assume the member list is complete, and all group state arrives signed by the relay key, with no way to verify it independently. A malicious or compromised relay can present different member lists to different users, silently drop one user’s messages for everyone else (shadowban), or fabricate moderation history, and no client can detect it from the protocol alone.
The retroactive-exposure profile is the thing to be clear-eyed about: a subpoena or compromise of the relay yields the complete plaintext archive of every group it ever hosted, member lists included. private is a promise, not a property.
2. Spam and rate limiting
This is where the relay-as-server bet pays off most directly — the party that owns the storage is the party that enforces the rules:
- Write-time membership enforcement. In a
restrictedgroup, a non-member’s event is rejected before it’s stored. The spam boundary is admission control, enforced at the socket. - Per-identity rate limiting is trivial. The relay sees the real author of every event, so per-npub quotas, per-group throughput caps, and burst limits are all straightforward.
- Bans actually enforce. A remove-user (9001) means the relay rejects the target’s future writes — the mute is at the socket, not in the client — and delete-event (9005) actually deletes.
- Admission-funnel control. Join requests (9021) are a chokepoint the relay fully controls: invite codes, pending review, payment required — the spec’s rejection-message conventions explicitly anticipate paid admission. Join-request flooding is the residual vector, and it’s plain-vanilla relay abuse handled with plain-vanilla tools (IP limits, PoW, payment).
- Open-write groups (no
restrictedflag) are just public nostr inboxes and inherit the ordinary firehose spam problem — WoT filters, PoW, payments, nothing group-specific.
Two structural caveats. First, the enforcement is only as good as the operator: moderation semantics (“what each role is capable of”) are explicitly undefined, relay-internal policy — so anti-spam quality is an operator characteristic, not a protocol guarantee, and varies per relay. Second, spam control doesn’t compose across forks: the same group id on another relay is a different enforcement domain with a different admin reality, and the previous-tag hack only weakly tethers them.
3. Push notifications
A solved problem here, as a direct payoff of plaintext:
- Server-side composition works. The relay (or any gateway it feeds) sees content, p-tag mentions, membership, and group metadata, so it can send fully composed, filtered notifications — “Alice mentioned you in Pizza Lovers” — exactly like Discord. Mention filtering, keyword alerts, per-channel mutes, digests: all server-side, zero battery cost for irrelevant traffic. There are no epochs, no rotating addresses, nothing to re-register, and relevance is decided before the radio ever wakes.
- The natural deployment is the relay itself offering a push endpoint, since for private groups any third-party gateway would need read access anyway — meaning the gateway is either the relay or effectively a member.
- The one privacy note: if you push composed payloads, Apple/Google see notification content in transit; a privacy-conscious client can still choose wake-and-fetch with an opaque ping. But that’s an option here, not a requirement.
4. General observations on the spec itself
Strong points. It’s radically simple — a client can implement it in an afternoon with ordinary event handling, no key derivation, no chain verification. Messages are normal nostr kinds (threads, articles, calendars, livestreams all work in-group via one h tag), which gives it immediate breadth. Moderation is instant and real. The operational details show deployment experience: late-publication rules, duplicate: rejection prefixes, capability probing for AV, invite preauthorization codes.
Costs of the bet, to be equally clear-eyed about:
- The group id self-certifies nothing. It’s “a random string of any length” with no commitment to a founder or key. Group identity is relay-scoped — the naddr embeds the relay’s pubkey — so squatting the same id on another relay is trivial, forks share an id but no canonical truth, and there’s no protocol answer to “which Pizza Lovers is the real one.” The spec punts that question to social convention.
- Relay master key compromise is total. Whoever holds it signs the metadata, the admin list, the member list — the entire authority layer of every group on the relay stands or falls with one key.
- Undefined role semantics hurt interop. Kind 39003 advertises role names; what they permit is “specific to each relay and not specified here.” Two relays hosting “the same” forked group can disagree about what a moderator may do.
- Community portability is aspirational. The spec gestures at moving/forking groups, but the member list, admin state, and history all live in one operator’s database, reachable only as relay-signed snapshots. That database can be subpoenaed, hacked, sold, or switched off; the fork mechanism softens that death into a lossy, identity-ambiguous migration.
- The AV section trusts the operator with media. Relay-issued JWTs, real-pubkey identities, relay-published participant lists, and no E2EE requirement on LiveKit — a full-visibility call system. Fine for public spaces, worth stating plainly.
The same questions, opposite answers
Read side by side, the two designs turn out to be near-perfect duals — every answer above flips. Where a Concord relay sees an anonymous traffic graph, a NIP-29 relay sees everything. Where a Concord member recomputes every authority claim from the owner’s key down, a NIP-29 member trusts relay-signed assertions. Where a NIP-29 ban enforces at the socket the instant it’s issued, a Concord ban is a client-side filter that only becomes real when a heavyweight refounding lands. Even the call systems mirror: relay-issued identities and relay-published participant lists on one side, a broker that can’t tell which community a room belongs to forwarding ciphertext on the other.
Neither is better. NIP-29 gets effortless moderation, spam control, and push because it centralizes exactly the visibility Concord spends seven documents eliminating, and Concord pays for its privacy in precisely those three currencies. What each bet buys:
| Concord (keys are the server) | NIP-29 (the relay is the server) | |
|---|---|---|
| Relay sees | Anonymous traffic graph (volumes, timing, groupings); never content or identity | Everything: plaintext content, real identities, full social graph, calls |
| Members can verify | Everything — authority is recomputed from the owner key by every client | Nothing independently — all state is relay-signed assertion |
| Spam control | Connection throttles, payment, PoW via the p-tag grind; real enforcement is client-side and lags (ban→refounding window) | Write-time membership checks, per-npub limits, bans that enforce at the socket |
| Push | Wake-and-fetch; mention filtering must be on-device; rekeys break registrations; keyholder bot is the scalable answer | Server composes and filters natively; no rotation, no gaps |
| Worst-case relay compromise | Metadata | The complete archive |
| Community survives its infrastructure | Yes — relays are interchangeable ciphertext buckets | Only via lossy forking with ambiguous identity |
The notification bot is the cleanest tell. In Concord, getting server-side filtering back means adding a trusted, keyholding member — and the honest deployment is per-user, because a communal one is exactly the plaintext-holding middleman the protocol set out to delete. In NIP-29 that trusted member was there all along, holding the database, which is why push is trivial and why private is a promise rather than a property. Each design’s hardest problem is the other’s foundation.
Lurkers, expanded: why there is no list, and where a lurker would show up anyway
The natural objection to Concord’s lurker property — “wouldn’t you see them in a list of pubkeys somewhere, and who invited them?” — assumes two records exist: a membership roster and an invite log. Concord has neither, and it’s worth walking through why not, because the absence is structural, not an oversight.
1. Membership is key possession, and the roster is self-reported
There is no admission event. CORD-02 §7 says it flatly: “Possession of the keys is membership; the invite is simply how they’re handed over.” The only membership record is the Guestbook, and every entry in it is either (a) a self-signed Join the member chose to publish, (b) an authorized Kick, or (c) a refounder’s snapshot — which is itself just a coalesce of (a) and (b) from the prior epoch. The “Complete Memberlist” (CORD-02 §5) is Guestbook fold + observably-present authors − banlist. Both inputs require the member to have published something. The name oversells it: it’s complete over participants, and a floor over keyholders.
Could the protocol have mandated a Join? No — not without a party to enforce it. The Join is a self-signed event the joiner publishes; a modified client simply doesn’t. Nothing downstream depends on it (the Guestbook is explicitly off-consensus — “nothing in Control or Chat depends on it”), so a missing Join breaks nothing, is noticed by no one, and can’t be punished. Any enforced roster needs a gatekeeper who checks credentials at read time, and the entire design premise is that no such party exists: the relay can’t gate reads on membership because it can’t tell members from anyone else, and other members can’t gate reads because reading is just fetching ciphertext you already hold keys to. NIP-29 has real rosters precisely because it has the gatekeeper.
2. The invite trail: three key-handoff paths, none of which leave a member-visible record
“Who invited them” is recorded in exactly one place — and it’s voluntary, self-claimed, and unverified.
- Public link (CORD-05 §1–2). The bundle sits bare on a relay; redeeming it is an anonymous fetch plus a local decrypt with the fragment token. No redemption event, no registration, no counter. The link creator cannot know how many people opened their link. The Registry (§5) lists which links exist — coordinates only, so members can see the door, never who walked through it. The only usage signal is the
["invite", creator, label]tag a joiner optionally echoes in their Join — and note that tag is the joiner’s claim, signed by the joiner alone. Nothing from the creator countersigns it. A joiner can omit it, or for that matter attribute themselves to any creator they like. Invite attribution in Concord is a courtesy, not a chain of custody. - Direct Invite (CORD-05 §6). Person-to-person NIP-59, touching no community plane. It “appears in no Registry” by design. The inviter knows privately; other members learn of it only via the same optional Join attribution. (The k-tagged wrap does leak “someone invited this npub to some community” — but to the invitee’s inbox relay, not to the community’s members, and never which community.)
- The whisper. Any keyholder can hand the key material to anyone, out of band, and the spec is admirably explicit that this is the floor: “No permission gates it, because none could: any keyholder can whisper keys, the ungateable act §2 already accepted as the floor” (CORD-05 §6). This is the path that kills any hope of an invite log even in principle. Keys are 32-byte strings; a protocol cannot observe a string being pasted into Signal.
So the answer to “who invited the lurker” is: at most one person knows (the inviter, if there was a discrete one), and the protocol never asks them to say.
3. The hidden cost: invisibility is symmetric with rekey exclusion
This is the refinement that materially softens “silently read everything, forever.” A rotator building a Rekey (CORD-06) enumerates recipients from the only list that exists — the Complete Memberlist. A lurker isn’t on it, so no rotator can include them, so the first epoch bump severs them exactly as if they’d been banned. The lurker can watch the rekey event land at the old address (they hold the old root), see that no blob locator is theirs, and that’s the end: history stays open, the future goes dark.
The consequences sort by community type:
- Public communities: epochs bump only on removals (CORD-02 §3), and public communities don’t refound on bans (a Ban there is a client-side silence). A lurker in a public community genuinely lurks forever. But a public community has, by definition, accepted world-readability — a live invite link means anyone can mint themselves read access anyway. Lurkers add nothing to its threat model.
- Private communities: every ban is a Refounding, so lurker lifetime = time to the next moderation event. A drama-free private community never rotates and the lurker persists; an active one sheds its lurkers as a side effect of unrelated bans. This makes the invisible-forever scenario real only in exactly the communities where nothing contentious is happening.
- The strategic lurker therefore faces a standing choice: stay invisible and accept severance at the next rotation, or publish a Join and become durable — visible on the roster, included in rekeys, but now subject to being banned like anyone else. You cannot be both invisible and rekey-durable. (A lurker who notices they’ve been severed can’t quietly re-enter, either: the new epoch’s addresses derive from a root they don’t hold. Re-entry means a fresh invite.)
One wrinkle: a per-user notification bot (see the push-notifications section of the Concord analysis) is a deliberate lurker, and this symmetry is exactly why refoundings must include it — a bot that never publishes is severed by the first rotation unless its owner Joins it or the rotator is told about it.
4. Where lurkers do show up: below the protocol, at the relay
The one honest answer to “wouldn’t you see them somewhere” is: not in any protocol artifact, but possibly in read-side telemetry. Reading requires subscribing, and a subscription filter listing the community’s stream addresses is visible to the relay it’s sent to. A community that runs its own relay can count distinct connections subscribing to its address set and compare against its roster; with NIP-42 AUTH required for reads, that becomes an npub-level list of everyone fetching the community’s ciphertext — the closest thing to a lurker roster that can exist. A community relay could go further and refuse reads to npubs not on the member list, converting detection into exclusion.
But this is operational, not protocol, and it’s leaky in every direction: the community’s events live on ~5 relays and the lurker only needs one permissive read path; an authed npub can be a throwaway unlinked to the keyholder’s identity; and nothing stops a cooperative member from simply forwarding ciphertext (or plaintext) outward — the whisper works for content, not just keys. Read-side gating raises the lurker’s effort from zero to trivial. It’s worth doing for a private community’s own relays, and worth being honest that it’s a fence, not a wall.
5. Bottom line on lurkers
The roster is a floor because the protocol’s core bet — no party who can gate access — removes every point where a mandatory list could be enforced or an invite could be forced onto the record. What the design gives back is that invisibility isn’t free: a lurker is, from the key-distribution machinery’s point of view, indistinguishable from someone already removed, and gets treated that way at the first rotation. “Anyone who ever received an invite can silently read everything, forever” should be amended to: until the next Refounding they weren’t seen for — which in a private community with active moderation is a naturally short leash, and in a public community is no leash at all, but there the door was open anyway.
Write a comment