Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 160 additions & 4 deletions 33.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,165 @@
NIP-33
======

Parameterized Replaceable Events
--------------------------------
Personal Notes
--------------

`final` `mandatory`
`draft` `optional`

Renamed to "Addressable events" and moved to [NIP-01](01.md).
This NIP defines a standard for storing personal, self-addressed encrypted notes on Nostr relays. The primary goal is privacy: all content is encrypted before leaving the client, event tags carry no plaintext metadata, and the author's pubkey is the only identity involved. Encryption uses [NIP-44](44.md) for relay payloads and AES-256-GCM for file storage.

Two event kinds are defined:

| Kind | Description |
|-------|-------------------|
| 33301 | Plain text note |
| 33302 | File attachment |

`All kinds are **addressable events** (30000–39999 range per [NIP-01](01.md)), identified by a `d` tag set to a client-generated note ID. This enables in-place replacement (edits) and deduplication across relays.

## Kind 33301 — Plain note

A plain text note. The `content` field holds a **NIP-44 ciphertext** produced by encrypting a JSON payload using the author's own keypair on both sides of the ECDH derivation(self-encryption). No other tags carry meaningful content.

**Event fields:**

| Field | Value |
|-----------|-----------------------------------------------|
| `kind` | `33301` |
| `tags` | `[["d", "<client-generated note ID>"]]` |
| `content` | NIP-44 self-encrypted JSON `{"text": "..."}` |

**Payload 'content' fields:**

| Field | Required | Description |
|-------------|----------|-------------------------------------------|
| `text` | yes | Plain text content |
| `sensitive` | no | It should be handled discreetly by the UI |

**Payload example (before encryption):**

```json
{
"text": "<note content>",
"sensitive": <true|false>
}
```


**Full event example:**

```jsonc
{
"kind": 33301,
"pubkey": "<author-pubkey>",
"created_at": 1700000000,
"tags": [
["d", "0193a4b2e8f1c3d7"]
],
"content": "<nip44-ciphertext>",
"id": "<event-id>",
"sig": "<signature>"
}
```

The decrypted `content` for the above would be:

```json
{
"text": "Remember to pick up groceries",
"sensitive": true
}
```

## Kind 33302 — Attachment

A file attachment note. The `content` field holds a NIP-44 self-encrypted JSON object describing the file. The file bytes themselves are encrypted separately with AES-256-GCM (wire format: `nonce[12] || ciphertext || mac[16]`) and uploaded to a [Blossom](https://github.com/hzrd149/blossom) server. Small files (e.g. config files, credentials backup, SSH keys) MAY be stored inline in the `data` field as base64-encoded AES-256-GCM ciphertext, removing the need for a separate file server.

**Threshold:** files 32 KB or larger SHOULD be uploaded to a Blossom server and referenced via `url`; files smaller than 32 KB MAY be stored inline in the `data` field as base64-encoded AES-256-GCM ciphertext.

**Encryption:** AES-256-GCM encryption is required for Blossom-hosted files since they are publicly accessible by URL. A fresh key MUST be randomly generated for every event, so each upload is independently encrypted. Inline files apply the same encryption uniformly, even if it's technically redundant since the payload is NIP-44 encrypted, to follow the same code path regardless of storage destination.

**Event fields:**

| Field | Value |
|-----------|-----------------------------------------------|`
| `kind` | `33302` |
| `tags` | `[["d", "<client-generated note ID>"]]` |
| `content` | NIP-44 self-encrypted attachment JSON |

**Payload fields:**

| Field | Required | Description |
|------------------|----------|-------------------------------------------------------------------|
| `filename` | yes | Original filename including extension |
| `file-type` | yes | MIME type of the file before encryption |
| `size` | yes | Original unencrypted file size in bytes |
| `dim` | no | Size of image in pixels in the form <width>x<height> |
| `x` | yes | Hex SHA-256 of the **encrypted** file bytes |
| `decryption-key` | yes | Hex-encoded 32-byte AES-256-GCM key, MUST be randomly generated per event |
| `url` | no* | Blossom URL of the encrypted file (required if `data` is absent) |
| `data` | no* | Base64-encoded encrypted file bytes (required if `url` is absent) |
| `thumbhash` | no | Base64-encoded [thumbhash](https://github.com/evanw/thumbhash) for images, shown as placeholder while loading |
| `caption` | no | Optional text caption |
| `sensitive` | no | Should be blurred in the UI, default false |

*Exactly one of `url` or `data` MUST be present.

**Payload example (before encryption):**

```json
{
"filename": "<original filename>",
"file-type": "<MIME type>",
"size": "<original unencrypted size in bytes>",
"dim": "<width>x<height>",
"x": "<hex SHA-256 of the encrypted file bytes>",
"decryption-key": "<hex-encoded 32-byte AES-256-GCM key>",
"url": "<Blossom URL>",
"data": "<base64-encoded encrypted bytes>",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you storing image/video data in relays as base64?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, drop this and make Blossom only. Optionality always sucks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files smaller than 32 KB MAY be stored inline, as stated in the NIP.
Again, we are not just dealing with media.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optionality sucks. 100% -- just store them in blossom, regardless of size -- blossom is for blobs; doesn't matter if its media or not

Copy link
Copy Markdown
Contributor Author

@dtonon dtonon Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not about optionality, it's a kind of progressive enhancement. You can use just your relays for light (< 32KB) stuff without having any other dependencies, and add a blossom server (that is actually a spec outside Nostr) if you need to save other stuff.

"thumbhash": "<base64-encoded thumbhash>",
"caption": "<optional text caption>",
"sensitive": <true|false>
}
```

**Full event example:**

```jsonc
{
"kind": 33302,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this replaceable if blossom is not?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the caption can be updated (and any future metadata, e.g. tags).

"pubkey": "<author-pubkey>",
"created_at": 1700000000,
"tags": [
["d", "0193a4b2e8f1c3d8"]
],
"content": "<nip44-ciphertext>",
"id": "<event-id>",
"sig": "<signature>"
}
```

The decrypted `content` for the above would be:

```jsonc
{
"filename": "photo.jpg",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filename is just confusing. There is no need for this. The name of the file is the hash.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filename is absolutely necessary. If I upload a document I WANT to know its exact original filename, not an useless hash. We don't just deal with images (where I might still be interested in the file name, btw).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nobody chooses filenames in modern computing architectures... this is a 90s idea. Especially in this case where people will want the metadata info, not the filename as a reference.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not only a matter of users naming files (I do it, and many other people do it), but also softwares name files with useful bits of information (dates, usernames, etc) that are useful to quickly understand the content, especially when you cannot open/parse the file.
The filename is a metadata, trashing is without any valid reason is against user's interest.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are using Blossom, the filename is the hash. There is no way around it. Otherwise, the mirroring system fails, the API fails. It's fine to add a name or description to the file as extra information, but as long as you are using Blossom, it will never be an actual filename. That's why it is confusing (and to me completely unnecessary).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot understand the problem. The filename is a metadata and I want to save it. When the user save back the file locally the original filename is proposed. What's the issue? I only see advantages.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just confusing... are we supposed to save files with that filename in disk when the user asks to save or to download? That breaks Blossom and makes things difficult to sync with other Blossom servers. Is the user going to see a readable filename in your app, but a hash on his own Blossom server? Why? That seems confusing too. How are they supposed to find things if the names keep changing?

To me, this is an unnecessary interoperability mess.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fwiw, I think filenames can have a lot of value; it doesn't matter if in blossom its served as the hash -- when you save it as whatever filename its been given

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't break Blossom in any way. And it doesn't create any confusion for the user, that for sure doesn't know (and doesn't have to know) how the blob is stored remotely. Try Manent's UX, it's simply good and meets users' basic expectations of uploading and downloading a file with the same name, or simply check it, if needed.
The user comes first; any changes should respect the values that make the experience useful; don’t simply eliminate them for the sake of “protocol cleanliness.”

"file-type": "image/jpeg",
"size": 204800,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dimensions are more important than size because clients can pre-compute screen layout and then just transition to the real image when it's available. Without it, everything moves in the UI.

Image dimensions should be a MUST everywhere.

Copy link
Copy Markdown
Contributor Author

@dtonon dtonon Mar 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that, but here we are talking about private notes that are displayed in a really simple structure. Anyway, adding the dimensions doesn't hurt, it can be useful to infer the orientation of an image.
File size is really important, since it allows to decide if the image/file should be downloaded automatically.

"dim": "800x600",
"x": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"decryption-key": "a2b4c6d8e0f1a3b5c7d9...",
"url": "https://blossom.example.com/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", // AES-256-GCM encrypted file
"thumbhash": "YQkGDYSId3h3d4d3aIeHeA==",
"caption": "Sunset at the coast",
"sensitive": true
}
```

## Notes

To add an additional layer of privacy, clients SHOULD publish these events to personal or authenticated relays (e.g. via [NIP-42](42.md)) that only serve events back to their owner. Publishing to public relays is not forbidden, but it increases metadata exposure (everyone can observe pubkey activity and event timestamps).
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using the same relay list (kind: 10013) that NIP-37 Drafts use

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a dedicated list is good, but I'm unsure about NIP-37: drafts are supposed to be temporary and so a relay, maybe a free public one, could clean them with a temporal logic; it is, however, essential that personal notes never be deleted.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not supposed to be temporary... It's just a relay list that is for private information that only the user has access to them. We save a lot of settings and other event kinds in there too. It because the main list for any private event that other users don't need to know you have.


## Reference implementation

[Manent](https://github.com/dtonon/manent) is the reference client implementing this NIP.