Markdown source for agents: https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/blossom.md

Nostr Agent Onboarding · start here · index · 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

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:

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

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.