CORD-03: Channels
A Channel is one message plane inside a Community: defined in the Control Plane, but carrying its own chat on an independent Chat Plane (CORD-02). A Community is simply a set of Channels unified under a common Control Plane.
Channels come in two kinds, differing only in who may read them:
Public: readable by every member. Its key is derived from the community_root, so nothing is ever delivered and it rotates for free whenever the base does.
Private: readable only by granted role-holders. Its key is an independent random secret, delivered on grant and rekeyed on removal (CORD-06).
1. Keying
Section titled “1. Keying”Every Channel is addressed and encrypted through the one group_key derivation (CORD-02); only its secret and epoch differ by kind:
Public channel_pk = group_key("concord/channel", community_root, channel_id, root_epoch).pkPrivate channel_pk = group_key("concord/channel", channel_key, channel_id, channel_epoch).pkBecause channel_id is folded into the derivation, every Channel gets its own distinct address regardless of which secret feeds it. A Public Channel is just “a Channel whose key derives from the community_root”, not a separate mechanism.
A Public Channel needs no key generation or distribution — every member can derive it, it adds nothing to an invite, and a single Refounding (CORD-06) severs it along with the base. A Private Channel needs real independence (a leaked channel key exposes only that one Channel, and it rotates independently), so it carries its own key and its own epoch.
2. Metadata
Section titled “2. Metadata”A Channel is defined by a ChannelMetadata entity in the Control Plane (CORD-04), holding its channel_id, name, and private flag. Every Channel is callable — a call is simply started in it (CORD-07), so there is no per-Channel voice flag. Edits fold as versioned editions like any control state, so a rename or visibility change is an authorized, convergent edit rather than a new Channel. The edition’s content (the channel_id is the edition’s eid, CORD-04), plus the optional client-extensible custom object (CORD-02 §6):
{ "name": "general", "private": false } // name ≤ 64 bytes, the protocol-wide cap (CORD-04){ "name": "lounge", "private": false } // any Channel is callable (CORD-07)A Channel is created by minting a random 32-byte channel_id and publishing this entity’s first edition (MANAGE_CHANNELS, the owner at genesis); a Private one mints and delivers its independent key alongside. A Channel is deleted by an edition setting "deleted": true. Deletion is terminal: the id is never reused, clients drop the Channel from display and may discard its keys. Its history stays decryptable to anyone who already held them — deletion, like privatising, can’t unshare the past.
Converting a Public Channel to Private is a key rotation: the Channel mints its own independent key at the next channel_epoch (monotonic, never resetting — the first privatisation is epoch 1 and each later one climbs), delivers it to the desired role-holders, and flips its metadata flag, all as a single authorized action (MANAGE_CHANNELS). The channel_id never changes across any conversion; only the key and epoch turn over. Because the epoch only climbs, privatise → publish → privatise is safe: each private generation lives at a distinct epoch, a stale key (always a lower epoch) can never share a coordinate with the current one, and a joiner is simply handed the current (key, epoch) in its invite (CORD-05). Privatising protects the future only: pre-conversion history was written under community_root-derived keys every member holds, so it stays readable to all members.
Converting Private to Public reverses it: the Channel begins deriving from the community_root going forward, and a member joining after the switch reads only the now-public history, never the prior private messages (they never held that key).
3. Messages
Section titled “3. Messages”A Channel’s Chat Plane carries the ordinary append events (messages, reactions, edits, deletes), each wrapped as a Stream event (CORD-01), signed by the Channel’s group_key and sealed to the author’s real identity within:
{ "kinds": [1059], "authors": ["<channel_pk per held epoch>"] }Clients load a Channel newest-first and paginate backwards, querying every epoch pubkey they hold (a Public Channel across the base epochs, a Private one across its own), so history spanning a rekey stays continuous.
Concord Channels make CORD-01’s binding a requirement: each message MUST commit ["channel", channel_id] and ["epoch", n] inside the author-signed rumor, and a receiver MUST check both strict-equal against the Channel and epoch whose key decrypted the wrap, dropping a mismatch. No member can re-wrap another’s message into a different Channel or replay it across an epoch.
Concord distinguishes two ways one message references another. An inline quote stays a kind 9 and carries a q tag (NIP-C7) citing the quoted rumor id; it renders inline in the timeline. A threaded reply is a separate action — a kind 1111 comment (NIP-22) with uppercase K/E/P pinning the thread root and lowercase k/e/p the immediate parent (all rumor ids) — and renders in a thread off its root rather than as a top-level row. A reply inherits its parent’s uppercase root tags verbatim, so the root stays stable at any depth. Both bind channel/epoch like any Chat rumor, and reactions, edits, and deletes target either by rumor id (their k tag naming 9 or 1111). See CORD-02 Appendix B and examples §2.1–2.2.