CORD-01: Private Streams
A private stream is a multi-party message stream on Nostr, built by sharing one private key between all participants.
Participants sign NIP-59 giftwrap events with the shared key and publish them to relays. Anyone holding the key can query the stream by its public key:
{ "kinds": [1059], "authors": ["<stream pubkey>"] }Joining a stream means receiving the private key, typically via an invite link.
Stream Event
Section titled “Stream Event”A stream event is a kind 1059 event. It looks like a regular giftwrap and blends in with giftwrap traffic, but differs from NIP-59:
{ "id": "<id>", "kind": 1059, "pubkey": "<stream pubkey>", "content": nip44_encrypt(conv_key, { "id": "<id>", "kind": 20013, "pubkey": "<real author's pubkey>", "content": nip44_encrypt(conv_key, { "id": "<id>", "kind": 9, "pubkey": "<real author's pubkey>", "content": "Hey chat!", "tags": [], "created_at": 1686840217, }), "tags": [], "created_at": 1686840217, "sig": "<real author's signature>" }), "tags": [ ["p", "<ephemeral pubkey>"] ], "created_at": 1686840217, "sig": "<stream signature>"}- NIP-59 uses ephemeral authors and fixed
ptags; streams reverse this: fixed author, ephemeralptag. - The wrap is NIP-44-encrypted under the stream conversation key (self-ECDH of the shared stream key with the stream pubkey), never the
p-tagged key. - The seal is kind 20013 (encrypted) or 20014 (plaintext), not kind 13, so relays never store it as a standalone event.
created_atis not altered or tweaked.
Encrypted vs plaintext seals
Section titled “Encrypted vs plaintext seals”A seal’s content takes one of two forms, declared by its kind so a reader never sniffs the content:
- Kind 20013, encrypted seal: the rumor is NIP-44-encrypted inside the already-encrypted wrap (as in the example above). The rumor is never a standalone artifact, so no relay — honest or malicious — can retain and display it as a public event.
- Kind 20014, plaintext seal: the seal’s
contentholds the rumor’s serialized JSON string verbatim. A signature over encrypted content is bound to the key that encrypted it and breaks if re-encrypted under a different key, so a stream that must carry a signed event across a key rotation uses this form. Verbatim means byte-verbatim: a re-wrap MUST carry the exact bytes forward, never re-serialize them, or two clients would compute different hashes from the same signed rumor.
Which form applies is a fixed property of the protocol layered over the stream, per context — never a per-message choice.
Encoding
Section titled “Encoding”For two clients to build byte-identical events:
- Hex is lowercase. Every 32-byte value (ids, pubkeys, keys, hashes, tokens) is 64 lowercase hex characters.
- Pubkeys are x-only hex, never bech32. A
pubkeyfield, anauthorsfilter, and every key inside a tag is the 32-byte schnorr x-only key in hex — notnpub/nsec, not a 33-byte compressed key. - Tag values are strings. A number (an epoch, an edition version, a
vsk) is its decimal form with no leading zeros:"4", never4. - Empty content is
"", nevernulland never omitted. created_atis unix seconds, untweaked. Sub-second ordering rides a tag, never a mutatedcreated_at(Concord uses["ms", <0..999>], CORD-02).
Binding
Section titled “Binding”The wrap is signed by the shared stream key, so any keyholder can re-publish a decrypted seal at another stream address: the author’s signature proves who wrote a rumor, not the context it was for. A protocol that splits a stream into sub-contexts (rooms, epochs) MAY bind them: commit the context as tags inside the signed rumor, and check them against the coordinate whose key opened the wrap.
Write-Restricted Streams
Section titled “Write-Restricted Streams”A stream’s two capabilities — publishing and reading — normally travel together in the one shared key. A protocol layered over streams MAY split them: the stream keypair (the address, signing the wraps) is held by a narrower set of writers, while the wrap’s content is encrypted under the conversation key of a second shared key — the read key, the same self-ECDH construction — held by the full readership. A reader then needs only the writers’ pubkey to subscribe, verify the wrap signatures, and decrypt, but cannot mint a wrap that verifies at the address. The split gates flooding, not truth: whatever authority scheme rides the seals is unchanged, and a valid wrap proves only that some writer published it. Concord’s Control Plane uses this split (CORD-02 §5).
Deletions
Section titled “Deletions”To prevent participants from deleting each other’s giftwraps, relays should reject giftwrap deletions by author. This matches NIP-59, which already requires relays to allow deletion by the p-tagged user: giftwraps are owned by the recipient, not the sender. Clients should use at least one relay that rejects giftwrap deletions.
Users delete their content in a stream by sending giftwrapped kind 5 deletion events to it. They can also delete their own giftwraps by p tag (on NIP-59-supporting relays) if the client saved the ephemeral key.
Removing Participants
Section titled “Removing Participants”Removing a participant requires a new stream. Clients can do this automatically, upgrading an existing chatroom to a new stream and merging events from both into one view.