> **Nostr Agent Onboarding** · [start here](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/start.md) · [index](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/llms.txt) · source: `nostr-dev/docs/sources/blossom.md` · snapshot 2026-10-10
>
> Paths such as `~/Documents/…`, `~/Production Environment/…`, `repos/…` and services on `localhost` refer to the author's workstation and are **not available to you** — read them as worked examples of a setup you can recreate.

# Blossom — binary blob storage for Nostr

**Spec home:** <https://github.com/hzrd149/blossom>
**Status:** running locally as part of the ngit-relay container at
`http://localhost:8081`. See `configs/local-services.json` → `always_on.ngit_relay_container.services.blossom`
for the full descriptor.

## What Blossom is

Blossom = "Blobs Stored Simply on Mediaservers." A protocol for storing and
serving arbitrary binary blobs, addressed by their **sha256**, with **Nostr
keys for authentication**. Specs are called BUDs ("Blossom Upgrade Documents").

The blob — not the URL — is the identity. The same blob is the same on every
server, and any server can serve it. Mirroring is a sha256 copy, no rewriting
of references.

## Why it pairs with Nostr

Nostr events carry `e`, `p`, `a` references — pointers into the event graph.
Blossom plays the same role for **content-addressed binary data**: an image,
a video, an audio file, a tarball, anything. A Nostr event with a
`url` / `imeta` tag pointing at `https://<host>/<sha256>` is a Blossom
reference.

The clean division of labor:
- **Relays** store events (signed JSON, small, structured).
- **Blossom servers** store blobs (binary, large, unstructured), keyed by hash.
- **Pubkeys** authenticate writes to both.

## BUD-01 — server requirements (the surface area)

| Method | Path | Purpose | Auth |
|---|---|---|---|
| `GET`    | `/<sha256>`                | fetch blob bytes               | none |
| `HEAD`   | `/<sha256>`                | check existence + size         | none |
| `PUT`    | `/upload`                  | upload a blob                  | kind:24242 event in `Authorization: Nostr <base64>` header |
| `GET`    | `/list/<pubkey-hex>`       | list blobs uploaded by pubkey  | none |
| `DELETE` | `/<sha256>`                | delete a blob                  | kind:24242 event |

The auth event (kind 24242) carries:
- `t` tag = the action (`upload`, `delete`, `list`, etc.)
- `x` tag = the sha256 of the blob being acted on (for upload/delete)
- `expiration` tag = unix timestamp after which the auth is no longer valid

Spec: <https://github.com/hzrd149/blossom/blob/master/buds/01.md>

## The BUDs you will actually meet

| BUD | What |
|---|---|
| **01** | Server requirements + blob retrieval (`GET`/`HEAD /<sha256>`) |
| **02** | Upload and management (`PUT /upload`, `GET /list/<pubkey>`, `DELETE /<sha256>`). `/list` returns `{ url, sha256, size, type, uploaded }` per blob. |
| **03** | **User Server List** — the user's kind **10063**, `["server", "https://…"]` tags. This is how a client (or an nsite gateway) discovers where someone's blobs live. |
| **04** | **Mirroring** — `PUT /mirror`; ask one server to pull a blob from another by URL. Cheap redundancy, same sha256. |
| **05** | Media optimisation (`PUT /media`) — server-side transcode; returns a *different* sha256. |
| **06** | Upload requirements (`HEAD /upload`) — ask before you send, so a server can refuse on size/type without the body. |
| **09** | Blob report |

Note BUD-03 is the *server list*, not mirroring — a common mix-up, and it
matters because 10063 is what the nsite gateway reads.

## How to use the local Blossom server

### From a shell with `nak`

```bash
NSEC=$(jq -r .nsec /dev/shm/nostr-dev-session/keys.json)   # ephemeral key

# upload
nak blossom --sec "$NSEC" -s http://localhost:8081 upload ./some-file.png

# list our blobs
nak blossom --sec "$NSEC" -s http://localhost:8081 list

# fetch (no auth needed)
curl -s http://localhost:8081/<sha256> -o some-file.png

# delete
nak blossom --sec "$NSEC" -s http://localhost:8081 delete <sha256>
```

### From TypeScript

Working example: `sdk/examples/blossom-upload.ts`. Run with:

```bash
cd ~/Documents/nostr-dev/sdk
NOSTR_SECRET_KEY=$(jq -r .nsec /dev/shm/nostr-dev-session/keys.json) \
  npm run blossom-upload -- ./some-file.png
```

It computes the sha256, builds and signs a kind:24242 auth event, PUTs the
blob, then verifies via HEAD + GET + list.

## Public servers: what each one actually does

**Blossom servers are not interchangeable.** They differ on which MIME types
they accept and — critically — on whether they preserve the `Content-Type` you
uploaded with. That type is recorded at upload, keyed by sha256, and is what an
nsite gateway forwards to the browser, so a server that rewrites it silently
breaks stylesheets and service workers.

Measured 2026-09-21 by uploading the same probe files from a throwaway key and
reading back what each server serves.

| Server | Accepts | Content-Type fidelity | Notes |
|---|---|---|---|
| **`https://nostr.download`** | everything probed | **faithful** — `text/css`, `text/javascript`, `application/wasm` all preserved | **The default first server for anything web.** Serves 200 direct. |
| `https://blossom.primal.net` | everything probed | **rewrites `text/*` → `text/plain`**; `application/*` passes through | Fine as an availability mirror, never as the type-of-record. 302-redirects blob GETs. |
| **`https://azzamo.media`** (= `https://blossom.azzamo.media`) | **media only** | **faithful** (`image/png` preserved) | See below. |
| `https://blossom.band` | — | — | Rejects fresh-key uploads outright: 400 on text, 415 on wasm. |
| `https://cdn.satellite.earth` | — | — | **500 on upload and GET.** Dead. |
| `https://inner.sebastix.social` | (invite) | faithful | API is at the **root** path; `…/blossom/` is an HTML catch-all that 200s for everything and false-positives every dedup check. |
| `http://localhost:8081` (ngit-relay) | everything probed | **faithful** | The local server. 100 MB/file, 50 GB total. |

### `azzamo.media` — the owner's media server

`azzamo.media` and `blossom.azzamo.media` are the same host. It self-describes
as *Azzamo Blossom 1.0.0*, BUD-01/02/04/05/06/09, `upload`/`media`/`mirror`
enabled, `discovery` off. It is **first in the owner's kind 10063**
(`npub1t6jxfqz9…`, published by Amethyst), which is what their phone uploads
through.

Two things to know before reaching for it:

1. **Uploads are open to any key**, not just the owner's — a throwaway key was
   accepted. (Contrast `relay.azzamo.net`, which *is* `payment_required` +
   `restricted_writes`. The Blossom side is not gated the same way.) So
   "available via the Constant keypair" is true, and it is also available via
   any other key you sign with.
2. **It is a media server and refuses everything else**, by MIME, with HTTP 415:

   | Probe | Result |
   |---|---|
   | `.png` | **accepted**, served as `image/png` |
   | `.html` | 415 — "MIME type text/html is not allowed for security reasons" |
   | `.js` | 415 — "MIME type text/javascript is not allowed for security reasons" |
   | `.css` | 415 — "MIME type text/css is not supported" |
   | `.txt` | 415 — "MIME type text/plain is not supported" |
   | `.wasm` | 415 — "MIME type application/wasm is not supported" |

**So: use `azzamo.media` for owner-signed media** — images, video, audio, the
things kind 10063 exists for, and the side-channel's file drops. **Never list
it as an nsite `server`**: an nsite is mostly HTML/CSS/JS/wasm and every one of
those uploads will 415. For nsites the list stays
`nostr.download` first, `blossom.primal.net` second — see
[`../patterns/nsite-publishing.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/nsite-publishing.md) §4.

> **Stale entry in the owner's live 10063.** It lists
> `blossom.azzamo.media/`, then `cdn.satellite.earth/`, then
> `blossom.primal.net`. The middle one returns 500 on everything — a dead
> fallback in a live media list. Worth republishing that 10063 from Amethyst
> without it. (Do **not** rewrite it with tooling — `nsyte --publish-server-list`
> and friends would clobber it; see [`../patterns/nsite-publishing.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/nsite-publishing.md) §3.)

## Quirk to know

The upload response from the local Blossom server returns
`"url":"http://localhost:3334/<sha256>"` — that's the **internal khatru port**
inside the container, not the external nginx port. Don't follow that URL;
use your configured base URL (`http://localhost:8081`) for subsequent
GET/HEAD/DELETE. The TS example handles this correctly by computing the
sha256 client-side and using the configured server URL.

## Limits (configured in `repos/ngit-relay/.env`)

```
NGIT_BLOSSOM_MAX_FILE_SIZE_MB=100
NGIT_BLOSSOM_MAX_CAPACITY_GB=50
```

`NGIT_OWNER_NPUB` in the same file is treated as the privileged owner whose
limits don't apply. By default it's set to a placeholder npub
(`npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr`). If you
want the workspace's ephemeral session key to be the owner, edit `.env` and
restart with `sudo docker compose up -d`. Don't bother — for local dev the
default 100MB / 50GB caps are plenty.

## When to use Blossom (vs alternatives)

- **Use Blossom when**: you need to attach binary content (images, video,
  files, archives, blobs) to Nostr events and want the references to be
  content-addressed and mirrorable across servers without breaking links.
- **Use a CDN / object store when**: the binary content has nothing to do
  with Nostr identity (you're just hosting static assets for a web app).
- **Use IPFS when**: you specifically want a global content-addressed
  network with active replication and existing tooling. Blossom is
  intentionally simpler — sha256 + HTTP, no DHT.

## Related work and clients

- **hzrd149's `blossom-server`** (TypeScript reference impl): <https://github.com/hzrd149/blossom-server>
- **`blossom-audit`** — audit a server's BUD compliance:
  `npx blossom-audit audit http://localhost:8081 bitcoin --sec <nsec>`
- **`bloss`** (Go), **`blossom-server-rs`** (Rust) — alternative implementations
- Most modern Nostr clients (Amethyst, Damus, Coracle) already speak Blossom
  for image/video uploads.

## How this informs design

When designing a Nostr feature that involves binary content:

1. **Don't store binary data inside events.** Events are gossiped, indexed,
   replicated — fat events are expensive everywhere.
2. **Store the binary in Blossom; reference it by sha256 in the event.**
3. **Publish to multiple Blossom servers** for redundancy; the sha256 stays
   the same so consumers can fall back from one server to another.
4. **Consider mirroring** (BUD-04) if you operate a relay — it's cheap to
   serve a blob you already have, and it improves the network's resilience.
5. **Pick the server for the content type.** Media → `azzamo.media` (the
   owner's) or `blossom.primal.net`. Anything a browser has to interpret —
   HTML, CSS, JS, wasm → `nostr.download` first, because it is the only public
   server measured to preserve those types faithfully.
