Skip to content

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.

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 p tags; streams reverse this: fixed author, ephemeral p tag.
  • 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_at is not altered or tweaked.

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

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 pubkey field, an authors filter, and every key inside a tag is the 32-byte schnorr x-only key in hex — not npub/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", never 4.
  • Empty content is "", never null and never omitted.
  • created_at is unix seconds, untweaked. Sub-second ordering rides a tag, never a mutated created_at (Concord uses ["ms", <0..999>], CORD-02).

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.

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

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