Chapter 37 - Private Data
37
Nostr Implementation Possibility - Hex № 37

Draft Wraps

Save unfinished events as encrypted drafts on your own relays, so no one reads a post before you publish it.

1 Overview

A user often starts a post and finishes it later, on another device. Nostr has no server-side account to hold that unfinished text. This NIP fills the gap with a draft wrap: an encrypted container event for an unsigned draft of any other kind.

The NIP defines three kinds. Kind 31234 is the draft wrap itself. Kind 1234 is a checkpoint that records a revision of a draft. Kind 10013 is a relay list for private content, which tells clients where to store draft wraps.

The draft stays private because the client encrypts it with NIP-44 to the signer's own public key. Only the author can decrypt it. Relays see an opaque string, never the draft text.

Hint!

A draft wrap is a container, not a post. The inner draft event is unsigned. When the user hits publish, the client signs the inner event, sends it as its real kind, and deletes the wrap.

Device A writes draft Private relay from kind 10013 Device B resumes draft EVENT kind 31234 REQ / decrypt Content is NIP-44 encrypted to the author's own public key.
Fig. 37-1 — Draft sync. One device stores an encrypted draft; another device of the same user fetches and decrypts it.

2 The Draft Wrap - Kind 31234

Kind 31234 is an addressable event (see NIP-01 storage classes). It stores one unsigned draft event of any other kind. Build it in three steps. Serialize the draft event to a JSON string. Encrypt that string with NIP-44 to the signer's own public key. Place the ciphertext in content.

Wire format - kind 31234 draft wrap
{
  "kind": 31234,
  "tags": [
    ["d", <identifier>],
    ["k", <kind of the draft event>],   // required
    ["expiration", <unix ts, ~now + 90 days>] // recommended
  ],
  "content": nip44Encrypt(JSON.stringify(draft_event)),
  // other fields per NIP-01
}
Kind 31234 tag reference
TagRequiredRule
dyesThe identifier of this draft. Makes the event addressable; a new version replaces the old one.
kyesThe kind number of the inner draft event. Lets clients filter drafts by target kind without decrypting.
expirationrecommendedA NIP-40 expiration tag. The source suggests about 90 days from creation.

Two more rules apply to the wrap:

  • Deletion. A blank content field signals that the author deleted the draft. Publish a new version of the same addressable event with empty content.
  • Where to publish. Clients SHOULD publish kind 31234 events to the relays listed in the user's kind 10013 event (see §4).
Tech Note

The source example writes the expiration value as the literal string "now + 90 days". That is pseudocode. NIP-40 requires a unix timestamp string. Compute the timestamp; do not copy the literal.

3 Checkpoints - Kind 1234

Kind 1234 defines a checkpoint. A checkpoint belongs to one parent kind 31234 draft wrap. Checkpoints can provide a revision history of the parent draft.

A checkpoint points to its parent with an a tag. Its content holds the draft revision, encrypted the same way as the parent: JSON-stringify the draft event, then encrypt with NIP-44.

Wire format - kind 1234 checkpoint
{
  "kind": 1234,
  "tags": [
    ["a", "31234:<pubkey>:<identifier>"]
  ],
  "content": nip44Encrypt(JSON.stringify(draft_event))
}
Tech Note

Kind 1234 sits in the regular storage range, so relays keep every checkpoint. The spec does not say what happens to checkpoints when the parent draft is blanked or expires. Clients should add their own NIP-40 expiration tags to checkpoints, or delete them with NIP-09.

kind 10013 private relay list kind 31234 draft wrap (latest) kind 1234 checkpoint rev 1 kind 1234 checkpoint rev 2 stored on "a" tag
Fig. 37-2 — Event structure. The relay list says where the wrap lives. Checkpoints reference the wrap with a tags.

4 Relay List for Private Content - Kind 10013

Kind 10013 is a replaceable event. It indicates the user's preferred relays for private events such as draft wraps. The public tags array stays empty. The relay list itself is private.

The event MUST include a list of relay URLs in private tags. Private tags are a JSON-stringified tag array, encrypted with NIP-44 to the signer's own keys, and placed in content.

Wire format - kind 10013 private relay list
{
  "kind": 10013,
  "tags": [],
  "content": nip44Encrypt(
    JSON.stringify([
      ["relay", "wss://myrelay.mydomain.com"]
    ])
  )
  // ...other fields per NIP-01
}
  • Clients MUST publish kind 10013 events to the author's NIP-65 write relays. Any client of the user can then find the list.
  • Private storage relays SHOULD require NIP-42 authentication. They SHOULD only allow downloads of events signed by the authenticated user. This recommendation keeps ciphertext and metadata away from strangers.
Warning!

Encryption hides the draft text, not the metadata. The k tag, the d tag, and the timestamps are public on any relay that serves the event. Use an authenticated private relay if that metadata matters to you.

5 Worked Example

Alice starts a long-form article (kind 30023) on her laptop. Her client wraps the unsigned draft, stores it, and later blanks it after she publishes. The inner draft event looks like this before encryption:

Inner draft event - unsigned, before encryption
{
  "pubkey": "a1fc5dfd…9b03",
  "created_at": 1767225600,
  "kind": 30023,
  "tags": [["d", "my-article"], ["title", "Why relays matter"]],
  "content": "Relays are simple servers that…"
  // no id, no sig — the draft is unsigned
}
Session transcript - wss://myrelay.mydomain.com
-- laptop stores the draft wrap on the private relay --
→ ["EVENT", {"kind":31234,"pubkey":"a1fc5dfd…9b03","created_at":1767225601,
     "tags":[["d","draft-my-article"],["k","30023"],["expiration","1775001601"]],
     "content":"AsWxoZ0e…Jw2Q==","id":"7d21c04b…","sig":"90ffab12…"}]
← ["OK", "7d21c04b…", true, ""]

-- phone finds the draft later --
→ ["REQ", "drafts", {"kinds":[31234], "authors":["a1fc5dfd…9b03"]}]
← ["EVENT", "drafts", {"kind":31234, "tags":[["d","draft-my-article"],["k","30023"], …], …}]
← ["EOSE", "drafts"]
-- the phone decrypts content with NIP-44 and restores the editor --

-- after publishing, the laptop blanks the wrap to delete the draft --
→ ["EVENT", {"kind":31234, "tags":[["d","draft-my-article"],["k","30023"]],
     "content":"", "created_at":1767312000, …}]
← ["OK", "e93aa1c7…", true, ""]

Between edits, Alice's client may also store kind 1234 checkpoints with the tag ["a", "31234:a1fc5dfd…9b03:draft-my-article"]. Each checkpoint keeps one encrypted revision, so she can roll back.

6 Version History

The draft-event idea started inside the old NIP-35 draft in March 2024 and moved to its own number soon after. Notable changes, newest first:

  • 20262026-05Reworked as "Draft Wraps" (PR #1653): kind 31234 wraps, kind 1234 checkpoints, and the kind 10013 private-content relay list in its current form.
  • 20252025-10Improved the draft-event spec (PR #2016).
  • 20242024-05Moved from the NIP-35 draft into NIP-37; added private outbox relays for draft storage.
  • 20242024-03Added draft deletion by blanking content, so drafts die even on relays without deletion support.
  • Origin2024-03Vitor Pamplona wrote the first encrypted draft-event spec.
Tech Note

Git rename detection chains this file into the unrelated history of the old NIP-35 pages (torrents, user discovery). The entries above keep only the commits about drafts.

Original Proposal - Credits

AuthorVitor Pamplona
First commit2024-05-30 — “moves from NIP-35 to NIP-37” (draft spec first written 2024-03)
Statusdraft - optional — clients may skip drafts entirely
Source37.md on GitHub - full change history

7 See Also

  • NIP-01 — the base protocol; addressable and replaceable storage classes used by kinds 31234 and 10013.
  • NIP-44 — the encryption scheme that protects the draft content and the private relay list.
  • NIP-40 — expiration tags, recommended on every draft wrap.
  • NIP-42 — relay authentication, recommended for private storage relays.
  • NIP-65 — the relay list metadata; kind 10013 goes to its write relays.
  • NIP-09 — event deletion requests, useful for cleaning up checkpoints.