CORD-02: Communities
A Community is a collection of Channels sharing one membership and authority model, built over Private Streams (CORD-01).
It runs on three secrets, kept deliberately apart:
community_id: its permanent identity, a self-certifying commitment to the owner’s key. It never appears on the wire (every coordinate derives from it one-way), but it travels inside invites, so any member can verify who founded the Community.
community_root: its private access key, generated and distributed by the creator. Holding the current community_root is membership. Rotating it (a Refounding, CORD-06) severs removed members without the Community losing its identity.
control_root: its staff write key. The Control Plane carries state every member must sync in full, so publishing to it is restricted: every member holds its derived pubkey to subscribe, verify, and read, but only the owner and staff (CORD-04 §3) hold the secret (§2). It rotates alongside the community_root at every Refounding (CORD-06).
1. Identity - The community_id
Section titled “1. Identity - The community_id”A Community’s identity is a self-certifying commitment to its owner:
community_id = sha256("concord/community" || owner_xonly || owner_salt)The owner_salt is 32 random bytes minted alongside the Community (so one owner can run many). It isn’t secret and travels inside invites, letting any member recompute the community_id and confirm the founder.
Forging a different owner onto an existing community_id is a second-preimage on SHA-256 (infeasible), and the owner signs the Community’s genesis Control Plane, proving they hold the secret key.
Genesis is exactly two owner-signed editions: the Community’s metadata (§6) and one public Channel named #general (CORD-03). No default roles, no scaffolding — the creator shapes everything else.
Binding identity to the owner’s key makes ownership unforgeable, at a price: there is no succession. A lost owner key can never be replaced, and a stolen one is stolen supremacy (as with any Nostr identity, nsec theft is terminal). The clean exit is Dissolution (§9); a voluntary owner-signed succession is possible future work.
2. Access - The community_root and control_root
Section titled “2. Access - The community_root and control_root”The community_root is a separate 32-byte private key, deliberately not derived from the community_id, so access can rotate while identity stays fixed.
It is the base key every member holds, gating every Public Channel (each derives its key from it, CORD-03) and the reading of the Control Plane (§5). New members receive it (alongside any Private Channel keys) inside their invite, and it rotates on member removal for private communities (CORD-06).
The control_root
Section titled “The control_root”The Control Plane is the one plane a regular member must never be able to write. It is consensus-critical state that every member keeps complete (§5), so if any member could publish there, any member could flood it: valid-looking wraps by the mile, every one of which the whole Community must fetch and decrypt, drowning the plane and burying moderator actions behind spam. Chat flooding is a moderation problem in one Channel, and Guestbook flooding coalesces away off-consensus (§5) — but Control flooding attacks the state everyone must fold, so it is cut off structurally.
The cut is a second secret, the control_root: 32 random bytes minted by the creator alongside the community_root, held only by the owner and staff — every member holding a Control-writing permission (CORD-04 §3). The Control Plane’s per-epoch signer keypair derives from it (§5), so only its holders can mint a wrap that verifies at the plane’s address; anyone else’s forgery fails the wrap signature and is dropped by relays and readers alike. Every member holds the derived pubkey (control_pk) — delivered in invites (§7), synced in the Community List (§8), and handed forward by base rekey blobs (CORD-06) — which is all reading takes: the wrap’s content is encrypted under a community_root-derived read key (§5).
Possession of the control_root is a spam gate, never authority (CORD-04): every edition is still judged by its sealed actor’s rank in the owner-rooted Roster, so a leaked or retained secret lets an attacker flood, not forge. The secret follows the membership key’s lifecycle: it is delivered on promotion inside the staff-making Grant itself (CORD-04 §3), synced across its holder’s own devices in the Community List (§8), and it rolls with the community_root at every Refounding (CORD-06). A demoted staffer therefore keeps only the flooding power until the next rotation — a Community that distrusts a former staffer Refounds, the same answer it has for a removed member.
3. Epochs
Section titled “3. Epochs”An epoch is a u64 counter attached to each key. It bumps only on a Rekey — a membership change where somebody is removed (CORD-06).
4. Addressing - Keys become Streams
Section titled “4. Addressing - Keys become Streams”Every plane inside a Community is a Private Stream (CORD-01), addressed by a pubkey derived from one of the Community’s symmetric keys, per epoch:
group_key(label, secret, id, epoch): seed = hkdf(secret, label, id, epoch) // 32-byte seed (byte-exact layout in the Appendix) sk = scalar_normalize(seed) // a valid secp256k1 secret key pk = xonly_pubkey(sk) // the Stream addressThe pk is where a plane’s events live ({"kinds":[1059],"authors":[pk]}), and the sk signs its giftwraps. Only a holder of the secret can derive the pk, so only members can produce events at a plane’s address — an outsider cannot even identify it. This is also the spam boundary: flooding a Chat or Guestbook plane is possible only for authenticated members — a moderation problem with moderation tools (a Ban silences instantly, CORD-04), and folds bound their own processing with hard caps regardless. The Control Plane’s boundary is tighter still: its signer derives from the staff-held control_root (§2), so a regular member cannot flood the one plane everyone must sync in full (§5).
Rotating the epoch rotates the pk, keeping a plane’s traffic unlinkable across epochs.
Ordering. Every event Concord compares is ordered by a millisecond timestamp, so events in the same second still sort deterministically. Nostr’s created_at is second-granularity, so a rumor carries an ["ms", <0..999>] tag with the sub-second remainder; the true time is created_at * 1000 + ms. Every comparison in the protocol — message order, Guestbook recency (§5), Community List tiebreaks (§8) — uses this basis.
5. Planes
Section titled “5. Planes”A Community’s state lives across three kinds of plane.
Control Plane
Section titled “Control Plane”One per Community — written under the control_root, read under the community_root (§2). Carries the authoritative state (metadata, Roles, grants, the banlist, Channel definitions) as folded, versioned editions (CORD-04). Every member keeps the entire Control Plane in sync — it is small and must stay complete, which is exactly why members cannot write to it (§2). Its stream key is split (CORD-01, Write-Restricted Streams): the address-and-signer keypair derives from the staff-held control_root, while the wraps’ content is encrypted under the read key every member derives:
control_pk = group_key("concord/control-signer", control_root, community_id, epoch).pk // the address; its sk signs the wraps (staff only)control_conv_key = group_key("concord/control", community_root, community_id, epoch).conv_key // encrypts the wraps (every member)A reader subscribes by control_pk — held, never derived (§2) — checks each wrap’s signature against it, and decrypts with control_conv_key. A verifying wrap proves only that a control_root holder published it, never who or with what right: the seal’s actor and the Roster remain the sole authority (CORD-04).
Legacy epochs. An epoch minted before the split had no signer key: the concord/control derivation alone was the plane — its pk the address and wrap signer, its conv_key the encryption, every member holding both. A client MUST retain that use of the derivation to read such epochs and the Communities still sitting in one; the two schemes never collide (different labels, different addresses), and the first compliant base rotation upgrades a Community to the split (CORD-06 §3).
Chat Plane
Section titled “Chat Plane”One per Channel, each with independent keys. Carries that Channel’s messages, loaded newest-first and paginated backwards. Channels and their keys are covered in CORD-03.
channel_pk = group_key("concord/channel", channel_key, channel_id, epoch).pkGuestbook Plane
Section titled “Guestbook Plane”One per Community, keyed by the community_root — necessarily member-writable, unlike Control, because Joins and Leaves are each member’s own word. Carries only membership motion: self-signed Joins and Leaves, plus authorized Kicks (one layer of a removal, CORD-04 §6). Never messages and never authority (a Ban lives on the Control Plane, CORD-04). The Guestbook is off-consensus: nothing in Control or Chat depends on it, so it loads last and can lag without harm.
guestbook_pk = group_key("concord/guestbook", community_root, community_id, epoch).pkA client folds the Guestbook by syncing its history and coalescing it flat — one final state per npub, where their latest Join, Leave, or Kick wins (a Kick honored only if its signer holds KICK and outranks the target, CORD-04). It then merges that fold with the local member list: every valid event a client decrypts names its real author, and an author seen publishing is observably present, auto-included even if their Join never arrived. Observation only counts forward: an author re-enters the list on activity newer than their latest Leave, Kick, or Ban, so a departed member’s old history can never resurrect them. The coalesced Guestbook, merged with observed authors, minus the Banlist, yields the Complete Memberlist.
Two fold restrictions keep the coalesce clean:
- An entry dated more than one hour ahead of the receiver’s clock is dropped outright — ample for clock skew, and a deterrent against squatting “latest” with a forged future date. An
mstag outside0..999is malformed and its entry is dropped, not interpreted, or the excess would smuggle arbitrary “future” past the clock check. - Entries tying on time break by the lower rumor id (the inner event’s, never the outer wrap’s, which differs per re-wrap). The tie-break is author-grindable, an accepted residual: the coalesce is per-npub, so an author only ever grinds ties against their own entries.
Snapshots. The Guestbook rides the epoch, so a Refounding (CORD-06) would otherwise start it empty. Instead, as a final non-gating step, the refounder coalesces the old epoch’s Guestbook, subtracts the removed, and publishes the survivors into the new epoch as snapshot entries. A snapshot is secondhand — the refounder’s attestation, not a member’s own word — so it merely seeds an npub’s state at the snapshot’s timestamp, and any self-signed entry (or authorized Kick) newer than it supersedes it. It is honored only from the npub whose Refounding minted that epoch.
A snapshot lists present members only (absence means “no seed”, never a negative state) and chunks at 400 members per event, all chunks sharing one snapshot id and one timestamp. Chunks are independently useful: a partially received snapshot seeds whoever arrived, and the rest heal by observation. A member entering a new epoch and finding their own state absent simply publishes a fresh Join, self-signed and unsuppressable — a Refounder omitting someone creates a blip, never a disappearance.
The three Guestbook rumors (each wrapped as a Stream event, CORD-01, at guestbook_pk):
// Join / Leave: self-signed, the content is the verb{ "kind": 3306, "pubkey": "<member>", "content": "join", // or "leave" "tags": [ ["invite", "<creator npub hex>", "<label>"] ] } // optional, Joins only: invite attribution (CORD-05)
// Kick: admin-signed, names its target, cites the Grant it acts under (the vac, CORD-04){ "kind": 3309, "pubkey": "<admin>", "content": "", "tags": [ ["p", "<target-hex>"], ["vac", "<grant eid>", "<grant version>", "<grant edition hash>"] ] }
// Snapshot: refounder-signed, present members only, chunked{ "kind": 3312, "pubkey": "<refounder>", "content": "[\"<npub-hex>\", \"<npub-hex>\", …]", "tags": [ ["snap", "<snapshot-id>", "<i>", "<n>"] ] } // one id + one created_at across all n chunksSeal encryption (CORD-01). The Control Plane’s seals MUST be plaintext (kind 20014); the Chat, Guestbook, and rekey planes’ seals MUST be encrypted (kind 20013). Only the Control Plane can be plaintext, because compaction (CORD-06) re-wraps its signed editions into a new epoch, and a signature over ciphertext could not survive that re-encryption. Every other plane either re-seeds with fresh attestations (the Guestbook snapshot) or never crosses an epoch (a Channel’s messages), and their double encryption guarantees the content is never a standalone public artifact — a member’s messages and a private roster cannot be lifted out and displayed as public Nostr events, except by a deliberate, authorized Pin (CORD-04 §7), which standardizes with proof the disclosure any keyholder could always have made informally.
6. Metadata
Section titled “6. Metadata”A Community presents a name, an optional description, and an optional icon, held in one Control Plane entity (vsk 0, Appendix B) and edited like any versioned edition (CORD-04), gated by MANAGE_METADATA. The edition’s content:
{ "name": "Vector", "description": "Private messaging, no compromises.", // optional "relays": ["wss://…", "wss://…"], // the Community's relay set (up to 5 recommended) "icon": { "url": "…", "key": "…", "nonce": "…", "hash": "…" }, // optional, encrypted-blob pointer "banner": { "url": "…", "key": "…", "nonce": "…", "hash": "…" }, // optional, same shape as icon "message_expiration": 2592000, // optional, the disappearing-messages timer in seconds (CORD-08) "custom": { "rules": "Be excellent to each other." } } // optional, client-extensibleThe name caps at 64 bytes and the description at 10000 bytes, counted as UTF-8. The 64-byte name cap is uniform across the protocol (Channels and Roles carry the same one).
Images never touch a media server in plaintext: the icon and optional banner are each encrypted under a fresh random key per image and uploaded as an ordinary blob. The entity carries only the pointer ({url, key, nonce, hash}); a member fetches the blob, decrypts, and verifies the hash, so the media server learns nothing and a swapped blob fails closed. The name and icon also ride inside invite bundles (CORD-05) purely as a preview — the Control Plane fold is always the authority.
Relays. The Community’s relay list lives here so it can evolve: a metadata edition replaces aged or retired relays, clients follow the fold, and a client may optionally rebroadcast old events to a newly adopted relay. Up to 5 stable relays is the recommendation, not a rule: past that, extra relays cost more than they buy (every publish fans out N times, every fetch waits on the slowest). A client MAY truncate a longer set to the first few, so a bundle or metadata edition MUST stay usable when its relay list is trimmed. The copy inside an invite bundle (CORD-05) is a join-time snapshot; the fold is always the authority.
Custom fields. A client may attach fields the protocol never defined inside the optional custom object — rules text, a theme, whatever. The protocol treats it as opaque and folds it atomically with the rest of the entity (same edition, same gate, same convergence). Two disciplines keep it safe: an editor MUST round-trip fields it doesn’t understand (editing the name never wipes another client’s rules), and custom shares the entity’s size budget — anything heavy belongs in an encrypted blob behind a pointer, like the icon. Client-specific keys should carry a prefix (vector/…, soapbox/…); generically useful ones (rules) stay plain. Top-level fields outside custom are reserved for the protocol. The same object is permitted on ChannelMetadata (CORD-03).
7. Invites
Section titled “7. Invites”An invite delivers the access keys to a new member: the community_id and owner_salt (to verify the owner), the community_root, the current epoch’s control_pk (§2 — read access to the Control Plane, never write), and the keys for whichever Channels the invite grants. Possession of the keys is membership; the invite is simply how they’re handed over — a shareable link (CORD-05), or the same bundle giftwrapped straight to an npub as a Direct Invite (CORD-05 §6).
8. The Community List
Section titled “8. The Community List”A member’s memberships sync across their devices (and their clients — two apps can serve one npub) as the Community List: the kind 13302 replaceable (Appendix B), NIP-44-encrypted to self, one document holding every Community they’re in and every one they’ve left:
{ "entries": [ { "community_id": "<hex>", "seed": { <join material> }, // the earliest epoch you held, only ever moves BACKward on merge "current": { <join material> }, // the freshest snapshot, replaced on every Refounding or rename "added_at": 1719800000000 } ], // ms, tiebreaks against a tombstone "tombstones": [ { "community_id": "<hex>", "removed_at": 1722400000000 } ] }Join material is the bundle’s membership subset (CORD-05): community_id, owner, owner_salt, community_root, root_epoch, control_pk, channels, relays, name, plus control_root when the member holds it (§2) — the List carries every private key its holder has, so a staffer’s own devices can write, exactly as channels carries only the Private Channel keys they were granted. Never the icon (a device folds it from the Control Plane) and never the link fields (expiry and attribution belong to the invite, not the membership).
The two snapshots solve opposite problems. seed holds the earliest epoch you ever held, the anchor for full-history backfill; current holds the latest, so a fresh device reconstructs the Community instantly with no epoch-by-epoch walk. The merges mirror each other: seed keeps the lower epoch, current keeps the higher, and an epoch tie breaks on the lexicographically lowest canonical bytes of the whole snapshot — a total order, so two devices never flap competing republishes.
Tombstones are per-Community and timestamped: the newest of added_at and removed_at wins, so a re-join legitimately resurrects a membership while a backfill can never re-add a tombstoned id. A tombstone is permanent (pruning would let a long-offline device resurrect a Community you left), and a tombstoned entry stays in the document, or merges would depend on gossip order.
The List caps at 50 memberships: it is one NIP-44 event, and NIP-44 plaintext hard-caps at 65,535 bytes, so the cap is a protocol constant, not client taste. The count is not the whole budget — join material carrying private-channel keys can overflow the event well below 50 — so a client MUST verify the serialized List fits before publishing. The round-trip discipline applies here too (§6): preserve what you don’t understand.
9. Dissolution
Section titled “9. Dissolution”A Community ends by an owner-signed tombstone, published at a coordinate derived from the community_id alone (Appendix A) — no key and no epoch involved, so every member past or present resolves the same address and a Refounding can never strand the grave:
dissolved_pk = group_key("concord/dissolved", community_id, 0…0).pkThe tombstone is terminal and chainless: no version to race, nothing to edit. The presence of one valid owner-signed edition at the coordinate is the state, verified against the owner the community_id commits to (§1). Anyone holding the community_id can find the address, but only the owner’s signature counts. On sight, a client seals the Community: read-only, subscriptions halted, held keys still open history but nothing new is honored.
Death wins every race: a Refounding cannot cross a tombstone (no epoch advance past it is honored), and the seal is one-way — there is no un-dissolve. One carve-out: a member’s delete of their own past message is always honored, even post-seal. A self-scrub can’t inject content, and a departing member deserves to erase themselves.
The tombstone itself is one owner-signed edition (CORD-04) carrying nothing but its binding:
{ "kind": 3308, "pubkey": "<owner>", "content": "", "tags": [ ["vsk", "10"], ["eid", "<community_id>"] ] } // chainless: no ev, no ep, presence is the stateThe eid binds the tombstone to the Community it kills, and a verifier MUST check it. Everything else about this coordinate is public: dissolved_pk derives from the community_id with no secret in the input, so anyone holding that id — it ships in every invite — derives the whole keypair, reads the plane, and signs at it. The single thing an attacker cannot manufacture is an owner-signed vsk 10 rumor.
A tombstone whose payload named no Community handed them one. The seal is plaintext (§5), so it re-wraps verbatim with its signature intact — the same property compaction depends on. An owner’s genuine tombstone for community X could therefore be lifted off X’s dissolved plane and re-wrapped at the address of any other Community the same owner runs, killing it permanently: no membership, no keys, nothing forged, and by the rule above, no recovery. Naming the community_id inside the signed payload is what makes a seal minted for X fail at Y.
A verifier MUST refuse a tombstone whose eid is not this Community’s id, including the all-zero placeholder earlier revisions specified. Accepting that value is the vulnerability, and the failure modes are not symmetric: refusing leaves a previously-dissolved Community reading alive, which its owner corrects by re-dissolving, while accepting leaves every owner of more than one Community a single public id away from an unrecoverable kill.
Appendix A: Derivations (frozen)
Section titled “Appendix A: Derivations (frozen)”Everything Concord addresses on the wire derives from a Community secret through one of the shapes below, all frozen: changing any labeled byte re-addresses every prior event and forces a migration. id is always a raw 32-byte value (never hex), and epoch_be a u64 big-endian.
A.1 HKDF
Section titled “A.1 HKDF”hkdf(secret, label, id, epoch): HKDF-SHA256( ikm = secret, salt = ∅, // zero-length info = utf8(label) || 0x00 || id[32] || epoch_be[8], // trailing fields omitted per A.6 len = 32 )The id is always present, 32 bytes, all-zeroes where a label has no meaningful id. The epoch is the only omittable field: a label with no epoch in A.6 omits the 8 bytes entirely. The scalar_normalize retry counter (A.3) appends after whatever fields are present.
A.2 group_key (CORD-02)
Section titled “A.2 group_key (CORD-02)”A pseudonym label’s 32-byte hkdf output is a seed, normalized into a secp256k1 keypair whose x-only pubkey is the on-wire Stream address:
group_key(label, secret, id, epoch): seed = hkdf(secret, label, id, epoch) sk = scalar_normalize(seed) // A.3 pk = xonly_pubkey(sk) // the address (authors filter) conv_key = nip44_conversation_key(sk, pk) // NIP-44 self-ECDH; encrypts the wrapA.3 scalar_normalize
Section titled “A.3 scalar_normalize”Must yield a valid secp256k1 secret key: if seed is not a valid scalar, append one incrementing counter byte to the hkdf info and retry, the counter starting at 0. The reject branch is ~2⁻¹²⁸ rare; the counter keeps it deterministic across implementations.
A.4 community_id (CORD-02)
Section titled “A.4 community_id (CORD-02)”A plain SHA-256 commitment, not the hkdf construction:
community_id = sha256( utf8("concord/community") || owner_xonly[32] || owner_salt[32] )A.5 epoch-key commitment (CORD-06)
Section titled “A.5 epoch-key commitment (CORD-06)”prevcommit = sha256( utf8("concord/epoch-key-commitment") || prev_epoch_be[8] || prev_key[32] )A.6 Labels
Section titled “A.6 Labels”| Label | secret (ikm) | id | epoch | Yields |
|---|---|---|---|---|
concord/channel |
channel key or community_root | channel_id | yes | a Channel’s group key (CORD-03) |
concord/control |
community_root | community_id | yes | the Control Plane read key — its conv_key encrypts the wraps; pre-split epochs used its pk/sk as the plane’s address and signer too, retained for reading them (§5) |
concord/control-signer |
control_root | community_id | yes | the Control Plane signer — its pk is the address, its staff-only sk signs the wraps (§5) |
concord/rekey-pseudonym |
prior community_root | channel_id | new_epoch | a channel rekey address (CORD-06) |
concord/base-rekey-pseudonym |
prior community_root | community_id | new_epoch | a base rekey address (CORD-06) |
concord/recipient-pseudonym |
rotator_xonly || recipient_xonly | scope_id | new_epoch | a rekey blob locator (CORD-06) |
concord/guestbook |
community_root | community_id | yes | the Guestbook Plane group key (§5) |
concord/voice-signer |
channel key or community_root | channel_id | yes | a Channel’s SFU room keypair (CORD-07) |
concord/voice-media |
channel key or community_root | channel_id | yes | a Channel’s raw 32-byte call media key (CORD-07) |
concord/voice-sender |
voice_media_key | sha256(SFU identity) | — | a publisher’s per-sender frame key (CORD-07 §3) |
concord/dissolved |
community_id | 0…0 | — | the dissolution tombstone address (§9) |
concord/grant |
community_id | member_xonly | — | a member’s Grant coordinate, its edition eid (CORD-04) |
concord/banlist |
community_id | 0…0 | — | the Banlist coordinate (CORD-04) |
concord/pins |
community_id | channel_id | — | a Channel’s Pin List coordinate (CORD-04 §7) |
concord/invite-links |
community_id | creator_xonly | — | a creator’s invite Registry coordinate (CORD-05 §5) |
concord/invite-key |
token | 0…0 | — | public-invite decrypt key (CORD-05) |
concord/invite-locator, concord/invite-signer |
— | — | — | retired: the bundle’s coordinate and signer no longer derive from the token; the link carries the coordinate and the creator holds the signer (CORD-05 §2) |
Appendix B: Kinds (frozen)
Section titled “Appendix B: Kinds (frozen)”Every durable plane event is a kind 1059 wrap around a seal (CORD-01), the inner rumor carrying the functional kind. The Chat, Guestbook, and rekey planes use the encrypted kind 20013 seal; the Control Plane uses the plaintext kind 20014 seal (§5). Every layer of the nesting is a NIP-44 plaintext, and NIP-44 hard-caps plaintext at 65,535 bytes: implementations MUST enforce the cap at every layer themselves (libraries are lenient, and a lenient publisher mints events a strict reader cannot decrypt). Concord reuses a standard kind wherever one fits and keeps a dedicated 33xx block where none does. A retired number is never reused.
Concord carries no version tag, anywhere: an outer tag would unmask the camouflage (a tagged 1059 no longer blends with giftwrap traffic), and the frozen derivations already partition incompatible revisions by address — a breaking change re-labels and becomes a different universe, while additive change rides unknown-field round-tripping (§6). Absence of a version field always means this spec.
| Kind | Function | Plane |
|---|---|---|
| 9 | Message (NIP-C7 shape) | Chat |
| 1111 | Threaded reply (NIP-22 shape) | Chat |
| 7 | Reaction (NIP-25 shape) | Chat |
| 5 | Delete (NIP-09 shape) | Chat |
| 1740 | Timer notice (CORD-08 §4) | Chat |
| 3302 | Edit | Chat |
| 3303 | Rekey blobs (CORD-06) | rekey addresses |
| 3306 | Join / Leave | Guestbook |
| 3308 | Control edition, sub-kinded below | Control |
| 3309 | Kick | Guestbook |
| 3310 | WebXDC peer signal | Chat |
| 3312 | Guestbook snapshot (§5), chunked, refounder-signed | Guestbook |
| 3300, 3301, 3304, 3305, 3307 | retired | none |
| 3311 | retired (briefly the typing indicator, renumbered into the ephemeral range as 23311) |
none |
| 23308 | retired (briefly a seal-less Control inner, superseded by the plaintext seal) | none |
Some actions are ephemeral, realtime-only with nothing worth storing: the same wrap-and-seal shape at the same Stream address, but the outer wrap is kind 21059, NIP-59’s ephemeral gift wrap (identical structure to 1059, relays MUST NOT store it). The rumor rides the ephemeral range too, so no layer of the event asks to be stored:
| Kind | Function | Plane |
|---|---|---|
| 23311 | Typing indicator | Chat |
| 23313 | Voice presence (CORD-07) | Chat |
One event rides a standard NIP-59 giftwrap rather than a stream wrap, addressed to a person, never a plane: the Direct Invite (CORD-05 §6), a kind 3313 rumor carrying an invite bundle, in a kind 13 seal under an ephemeral-author, recipient-p-tagged kind 1059 wrap. Its wrap carries an outer ["k", "3313"] tag — a deliberate exception to the no-outer-tags rule (the only other is the NIP-40 expiration tag on a disappearing-messages Chat wrap, CORD-08 §2), justified because it isn’t camouflaged stream traffic. The tag lets a recipient index their invites ({"kinds":[1059], "#p":[…], "#k":["3313"]}) without decrypting their whole giftwrap inbox. No stream wrap ever carries it:
| Kind | Function | Plane |
|---|---|---|
| 3313 | Direct invite (standard NIP-59 wrap, k-tagged) |
none, person-to-person |
Three events live outside the wrap:
| Kind | Function |
|---|---|
| 33301 | the public invite bundle (CORD-05), addressable, signed by its per-link keypair at an empty d (the link’s naddr is the coordinate), its sub-kind tag marking it live (6) or a revocation tombstone (9) |
| 13302 | a member’s self-encrypted Community List (§8), replaceable, one per user, signed by their real key |
| 13303 | a member’s self-encrypted Invite List (CORD-05 §4), replaceable, one per user, signed by their real key |
A Control edition (3308) names its entity type by a sub-kind (vsk):
| vsk | Entity |
|---|---|
| 0 | Community metadata (§6) |
| 1 | Role (CORD-04) |
| 2 | Channel metadata (CORD-03) |
| 3 | Grant (CORD-04) |
| 4 | Banlist (CORD-04) |
| 5 | reserved (role ordering) |
| 6, 9 | claimed by the addressable invite marker above |
| 7 | retired (v1 owner attestation, obsoleted by the self-certifying community_id) |
| 8 | Invite-link registry (CORD-05) |
| 10 | Dissolved tombstone (§9), chainless, exempt from version discipline |
| 11 | Pin List (CORD-04 §7) |