Skip to content

CORD-08: Disappearing Messages

A Community can make its conversation ephemeral: one timer, set by staff, after which every Chat-plane message expires — refused at ingest, hidden from display, purged from local stores, and deleted by relays via NIP-40.

Two properties anchor the design:

  • The timer is Community state, never a per-message choice: a single field in the Community-metadata entity, edited and folded like any versioned edition (CORD-04), so every member converges on one policy and every Channel applies it.
  • Expiry is cooperative, as in every disappearing-message scheme: an honest client hides and deletes, an honest relay purges, but a member could always have copied what they could read. The guarantee is hygiene against the future — a seized device, a compromised key, an archived relay — not protection from present recipients.

The timer lives in the Community-metadata entity (vsk 0, CORD-02 §6) as a top-level protocol field:

{ "name": "Vector",
"relays": ["wss://…"],
"message_expiration": 2592000 } // seconds; absent or 0 = off

message_expiration is the timer in seconds, a JSON number. Absent, 0, or malformed means off — a reader MUST NOT guess a default from garbage. Editing it is editing the metadata: the same edition chain, the same MANAGE_METADATA gate, the same fold and round-tripping discipline (CORD-02 §6), so a client that predates this CORD carries the field through a rename untouched.

The timer governs Chat planes only — every Channel in the Community, public and private alike. The other planes never expire, each for its own structural reason:

  • Control: the authoritative state must stay complete, and a compaction re-wraps signed editions verbatim across epochs (CORD-06) — an expiring edition would eat the chain out from under the fold.
  • Guestbook: membership motion is the input to the Complete Memberlist (CORD-02 §5); an expired Leave would resurrect a departed member.
  • Rekey addresses: a parked blob may be a slow member’s only way back into the new epoch (CORD-06).

While the timer is set, a sender MUST attach the NIP-40 tag to every durable Chat-plane rumor, computed from the rumor’s own created_at:

["expiration", "<created_at + message_expiration>"] // unix seconds, decimal string

The outer kind-1059 wrap MUST carry the same tag with the same value. The rumor’s copy is inside what the author signs — authoritative, unforgeable, and what every reader enforces (§3). The wrap’s copy is for relays: NIP-40-honoring relays delete the ciphertext itself, so after expiry the message is not merely unreadable but gone from public infrastructure — a future compromise of the Channel’s keys recovers nothing.

This is the second deliberate exception to the no-outer-tags rule (CORD-02 Appendix B), and it trades exactly what the rule protects: an expiring wrap still blends with giftwrap traffic (NIP-17 DM wraps carry the same tag), but the tag does reveal the timer’s value to relays and marks the stream’s wraps as chat rather than control. That leak is the price of real deletion, and a Community that won’t pay it turns the timer off. The wrap tag is hygiene, not authority: a re-published wrap with a doctored tag only makes a relay hold ciphertext longer or drop it early — readers judge by the rumor’s copy alone.

Two Chat kinds are exempt and MUST NOT carry the tag:

  • Deletes (kind 5): a delete is a tombstone, and its target may outlive it — sent under a longer timer, or none at all. An expiring delete would let the message it erased come back.
  • Timer notices (kind 1740, §4): the notice documents the policy; it must not be erased by the policy, or the room forgets why its history is missing.

Ephemeral kinds (typing, voice presence) carry nothing — no layer of them asks to be stored.

A timer change is not retroactive: the tag rides inside the signed rumor, so a message keeps the expiry it was sent under, and no edition can reach back into history — exactly the Signal behavior. And the tag as signed always governs: a client that hasn’t folded the newest metadata (or predates this CORD) sends the old timer or none, and readers honor what the rumor says rather than what the fold now prescribes. The fold governs what a compliant sender attaches next, never what an existing rumor means.

A reader:

  • MUST refuse an already-expired rumor at ingest — never store it;
  • MUST NOT display an expired rumor, including one stored before its expiry passed;
  • SHOULD physically purge expired rumors from local storage on a periodic sweep — hiding is not disappearing, and the local store is exactly the artifact a seized device surrenders.

Relays enforce the wrap’s tag per NIP-40. Clock skew is inherent and NIP-40’s semantics absorb it: the shortest timer a client should offer (a day) dwarfs any honest skew, and a rumor arriving seconds around its boundary merely expires seconds early or late everywhere.

A policy change deserves a line in the conversation, not just a diff in the fold. After publishing the metadata edition, the actor SHOULD post one notice into each Channel whose key they hold:

{ "kind": 1740, "pubkey": "<actor>", "content": "",
"tags": [ ["channel", "<channel_id>"], ["epoch", "<n>"], // the CORD-03 binding, like any Chat rumor
["timer", "2592000"] ] } // the new value in seconds; "0" = turned off

Kind 1740 is the same timer-notice kind NIP-17 disappearing-DM clients already use, carrying the same ["timer", "<seconds>"] tag; here it rides a Chat plane with the ordinary channel binding. A client renders it as an inline notice row — “Alice set disappearing messages to 30 days” — timestamped and attributed like any message.

The notice is informational, and gated like an authority claim: a reader displays it only if its author holds MANAGE_METADATA in the fold, and drops it otherwise — anyone can spell a tag, but only staff can be believed about policy. The metadata fold is always the authority; a missing notice changes nothing (a private Channel whose key the actor doesn’t hold simply gets none, and its members still see the fold), and a notice contradicting the fold is just stale.


This CORD adds one Chat-plane rumor kind to the registry (CORD-02 Appendix B):

Kind Function Plane
1740 Timer notice (§4) Chat

and one top-level field, message_expiration, to the Community-metadata entity (vsk 0).