Concord and NIP-29: Two Designs for Group Chat on Nostr

What the relay can see, how spam gets stopped, and whether push notifications work — the same three questions asked of both nostr group-chat designs, and the different strengths each buys with its foundational bet.

cloud fodder

July 12, 2026 · 29 min read

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:

Across events and connections, the relay can derive:

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:

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:

Two gaps the spec's moderation story doesn't fully close:

  1. 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.
  2. 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:

  1. 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.
  2. 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).
  3. 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:

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:

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:

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:

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:

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 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.

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:

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.

#concord#nip29#nostr#privacy
1

Written by

captain of a sailing vessel building relay.tools electron microscopic time preference capable email: cloudfodder@relay.tools

cloudfodder@relay.tools

Comments1

Dawn

I've been curious about the trade offs. Thank you for sharing this.
Open in a Nostr client