Skip to content

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


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).pk
Private channel_pk = group_key("concord/channel", channel_key, channel_id, channel_epoch).pk

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

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

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.