Files
amethyst/docs/brainstorms/2026-03-16-blossom-protocol-research.md
T
nrobi144 2a6087af27 feat(media): global media player — persistent playback across navigation
Media playback now survives navigation. A GlobalMediaPlayer singleton owns
VLC players and exposes StateFlows. Composables are thin viewports.
NowPlayingBar has full controls (volume, mute, save, fullscreen).
GlobalFullscreenOverlay renders video fullscreen above all screens.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 12:27:13 +02:00

17 KiB

Blossom Protocol Research

Date: 2026-03-16 Sources: hzrd149/blossom GitHub (BUD specs), NIP-B7, Nostrify docs, Amethyst upstream codebase, Primal blog posts


Overview

Blossom (Blobs On Simple Serverom... or something) is a specification for HTTP endpoints that let users store binary blobs on publicly accessible servers. Blobs are content-addressed by their SHA-256 hash. Uses Nostr keypairs for identity and authorization.

Two Nostr event kinds:

  • Kind 24242 -- Authorization token (BUD-11)
  • Kind 10063 -- User's Blossom server list (BUD-03, NIP-B7)

BUD index (BUD-00 through BUD-11):

BUD Name Status Required
00 BUD framework - -
01 Server requirements + blob retrieval draft mandatory
02 Upload + management draft optional
03 User server list (kind 10063) draft optional
04 Mirroring draft optional
05 Media optimization draft optional
06 Upload requirements (HEAD preflight) draft optional
07 Payment required (402) draft optional
08 NIP-94 file metadata tags draft optional
09 Blob report draft optional
10 Blossom URI scheme draft optional
11 Nostr authorization draft optional

BUD-01: Server Requirements + Blob Retrieval

Status: draft mandatory

CORS

All responses MUST set Access-Control-Allow-Origin: *.

Preflight (OPTIONS) responses MUST also set:

Access-Control-Allow-Headers: Authorization, *
Access-Control-Allow-Methods: GET, HEAD, PUT, DELETE

MAY set Access-Control-Max-Age: 86400 (cache 24h).

Error Responses

Any 4xx/5xx response MAY include X-Reason header with human-readable error message.

Endpoints

All endpoints served from domain root. No path prefix.

GET / -- Get Blob

GET /b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf HTTP/1.1
Host: cdn.example.com

Response:

HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Length: 184292

<binary blob data>
  • MUST accept optional file extension in URL (.pdf, .png, etc.)
  • MUST return correct Content-Type regardless of extension
  • MUST default to application/octet-stream if MIME unknown
  • MAY require authorization (BUD-11)

Proxying/Redirection:

  • 3xx redirects MUST redirect to URL containing same SHA-256 hash
  • Destination MUST set Access-Control-Allow-Origin: *, Content-Type, Content-Length

Range Requests:

  • Servers SHOULD support Range header (RFC 7233) on GET
  • Signal via Accept-Ranges: bytes and Content-Length on HEAD

HEAD / -- Has Blob

Identical to GET but MUST NOT return body. MUST return same Content-Type and Content-Length headers.


BUD-02: Upload + Management

Status: draft optional

Blob Descriptor

The standard JSON response for all upload/mirror operations:

{
  "url": "https://cdn.example.com/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf",
  "sha256": "b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553",
  "size": 184292,
  "type": "application/pdf",
  "uploaded": 1725105921
}

Fields:

  • url -- Public URL to GET /<sha256> endpoint with file extension
  • sha256 -- Hex-encoded SHA-256 of the blob
  • size -- Size in bytes
  • type -- MIME type (fallback application/octet-stream)
  • uploaded -- Unix timestamp

MAY include: magnet, infohash, ipfs

PUT /upload -- Upload Blob

PUT /upload HTTP/1.1
Host: cdn.example.com
Authorization: Nostr <base64url-encoded kind 24242 event>
Content-Type: image/png
Content-Length: 184292
X-SHA-256: b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553

<raw binary data>

Response (success):

HTTP/1.1 200 OK
Content-Type: application/json

{
  "url": "https://cdn.example.com/b167...553.png",
  "sha256": "b167...553",
  "size": 184292,
  "type": "image/png",
  "uploaded": 1725105921
}

Key rules:

  • Server MUST NOT modify the blob
  • Server MUST compute SHA-256 over exact bytes received
  • Client SHOULD include Content-Type and Content-Length
  • Client MAY provide X-SHA-256 header (hex lowercase)
  • Server MAY use X-SHA-256 for pre-upload rejection policies
  • Success: 2xx with Blob Descriptor
  • Failure: 4xx with error message

GET /list/ -- List Blobs (Unrecommended)

Optional. Returns JSON array of Blob Descriptors for a pubkey.

Query params:

  • cursor -- SHA-256 of last blob (cursor-based pagination)
  • limit -- Max results
  • since/until -- Filter by upload date (deprecated for pagination)

Sorted by uploaded descending.

DELETE / -- Delete Blob

DELETE /b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf HTTP/1.1
Host: cdn.example.com
Authorization: Nostr <base64url-encoded kind 24242 event>
  • Multiple x tags in auth token MUST NOT be interpreted as batch delete

BUD-03: User Server List

Kind 10063 (replaceable event).

{
  "kind": 10063,
  "tags": [
    ["server", "https://cdn.self.hosted"],
    ["server", "https://cdn.satellite.earth"],
    ["alt", "File servers used by the author"]
  ],
  "content": ""
}
  • Tag order = priority. Most trusted/reliable first.
  • Clients MUST upload to at least the first server in user's list.
  • Clients MAY mirror to other listed servers via BUD-04.

Discovery flow when URL breaks:

  1. Extract 64-char hex hash from broken URL
  2. Fetch author's kind:10063 event
  3. Try each listed server in order
  4. Fall back to well-known servers

BUD-04: Mirroring

Status: draft optional

PUT /mirror -- Mirror Blob

PUT /mirror HTTP/1.1
Host: backup-server.example.com
Authorization: Nostr <base64url-encoded kind 24242 event>
Content-Type: application/json

{
  "url": "https://cdn.satellite.earth/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf"
}

Response: Blob Descriptor (same as upload).

Key rules:

  • Server downloads blob from provided URL
  • Server SHOULD use Content-Type from origin server
  • Server verifies downloaded blob hash matches x tag in auth token
  • Returns 2xx + Blob Descriptor on success, 4xx on failure

Typical flow:

  1. Client uploads to Server A, gets Blob Descriptor with URL
  2. Client sends URL to Server B's /mirror with same upload auth token
  3. Server B downloads from Server A
  4. Server B verifies hash matches x tag
  5. Server B returns Blob Descriptor

BUD-05: Media Optimization

Status: draft optional

PUT /media -- Optimized Upload

PUT /media HTTP/1.1
Host: trusted-server.example.com
Authorization: Nostr <base64url-encoded kind 24242 event with t=media>
Content-Type: image/png
Content-Length: 4194304

<raw binary data>

Response: Blob Descriptor -- but hash will differ from input because server transforms the file.

Key differences from /upload:

  • Server MAY modify/optimize the blob (strip EXIF, compress, transcode)
  • The returned SHA-256 will be of the optimized blob, not the original
  • Client has NO control over optimization process
  • t tag in auth event must be media (not upload)

HEAD /media

Same as HEAD /upload (BUD-06) but for the media endpoint.

Client Implementation Pattern

  1. User selects a "trusted processing" server
  2. Client uploads original media to /media on trusted server
  3. Gets back optimized blob descriptor (new hash)
  4. Client signs new upload auth for the optimized hash
  5. Calls /mirror on other servers to distribute the optimized blob

This is what Primal does -- all Primal 2.2+ apps use /media by default, strips metadata, then mirrors.


BUD-06: Upload Requirements (HEAD Preflight)

Status: draft optional

HEAD /upload -- Pre-flight Check

Client sends blob metadata, server says yes/no before actual upload.

Request:

HEAD /upload HTTP/1.1
Host: cdn.example.com
X-Content-Type: application/pdf
X-Content-Length: 184292
X-SHA-256: 88a74d0b866c8ba79251a11fe5ac807839226870e77355f02eaf68b156522576
Authorization: Nostr <base64url-encoded event>

Success:

HTTP/1.1 200 OK

Failure examples:

HTTP/1.1 400 Bad Request
X-Reason: Invalid X-SHA-256 header format. Expected a string.

HTTP/1.1 401 Unauthorized
X-Reason: Authorization required for uploading video files.

HTTP/1.1 403 Forbidden
X-Reason: SHA-256 hash banned.

HTTP/1.1 411 Length Required
X-Reason: Missing X-Content-Length header.

HTTP/1.1 413 Content Too Large
X-Reason: File too large. Max allowed size is 100MB.

HTTP/1.1 415 Unsupported Media Type
X-Reason: Unsupported file type.

Note: Uses X-Content-Type, X-Content-Length, X-SHA-256 headers (not standard Content-*).


BUD-07: Payment Required

Servers MAY return 402 Payment Required with payment method headers:

HTTP/1.1 402 Payment Required
X-Cashu: "<NUT-24 cashu token>"
X-Lightning: "<BOLT-11 invoice>"

After payment, client retries with proof:

  • Cashu: serialized cashuB token per NUT-24
  • Lightning: preimage of the BOLT-11 payment

HEAD requests inform about cost but should not be retried with payment; proceed to PUT/GET after paying.


BUD-08: NIP-94 File Metadata Tags

Servers MAY include a nip94 field in Blob Descriptor responses:

{
  "url": "https://cdn.example.com/b167...553.pdf",
  "sha256": "b167...553",
  "size": 184292,
  "type": "application/pdf",
  "uploaded": 1725105921,
  "nip94": [
    ["url", "https://cdn.example.com/b167...553.pdf"],
    ["m", "application/pdf"],
    ["x", "b167...553"],
    ["size", "184292"],
    ["magnet", "magnet:?xt=urn:btih:..."],
    ["i", "infohash-here"]
  ]
}

Follows NIP-94 tag format as KV pairs. Allows clients to get standardized metadata without separate requests.


BUD-09: Blob Report

PUT /report

Body: signed NIP-56 report event (kind 1984):

{
  "kind": 1984,
  "content": "This blob contains illegal content",
  "tags": [
    ["x", "<sha256>", "illegal"],
    ["p", "<uploader-pubkey>"]
  ]
}

Server maintains records for operator review. Optionally authorizes trusted moderators for autonomous removal.


BUD-10: Blossom URI Scheme

Format:

blossom:<sha256>.<ext>[?param1=value1&param2=value2...]

Example:

blossom:b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf?xs=cdn.example.com&xs=backup.example.com&as=<author-hex-pubkey>&sz=184292

Query parameters:

  • xs -- Server domain hints (tried first). Repeatable.
  • as -- Author hex pubkey for BUD-03 server list lookup. Repeatable.
  • sz -- Size in bytes.

Resolution priority:

  1. Direct server hints (xs) via GET /<sha256>
  2. Author server lists (fetch kind:10063 for each as pubkey)
  3. Fallback to well-known servers or local cache

BUD-11: Authorization

Kind 24242 Event Structure

{
  "id": "<event-id>",
  "pubkey": "<hex-pubkey>",
  "created_at": 1725105921,
  "kind": 24242,
  "tags": [
    ["t", "upload"],
    ["x", "b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553"],
    ["expiration", "1725109521"],
    ["server", "cdn.example.com"]
  ],
  "content": "Upload cat photo",
  "sig": "<schnorr-signature>"
}

Required Tags

Tag Description
t Action verb: get, upload, list, delete, media
expiration Unix timestamp when token expires (NIP-40)

Optional Tags

Tag Description
x SHA-256 hash of specific blob. Multiple allowed.
server Domain restriction (lowercase). Multiple allowed.
size File size in bytes (Amethyst adds this)

Authorization Header

Authorization: Nostr <base64url-encoded JSON of the kind 24242 event>

Note: Amethyst uses standard Base64 (not Base64url), which works in practice.

Endpoint Authorization Requirements

Endpoint t tag Hash source x tag
GET/HEAD / get URL path optional
PUT /upload upload X-SHA-256 header required
HEAD /upload upload X-SHA-256 header required
DELETE / delete URL path required
GET /list/ list -- N/A
PUT /mirror upload mirrored blob hash required
PUT /media media X-SHA-256 header required
HEAD /media media X-SHA-256 header required

Validation Checklist (Server)

  1. Event kind == 24242
  2. created_at < now
  3. expiration > now
  4. t tag matches endpoint action
  5. server tags (if present) include this server's domain
  6. x tags (if required) match the blob hash

Security Note

Unscoped tokens (no server tag) can be replayed to other servers. Always scope delete tokens.


Error Handling -- HTTP Status Codes

Code Meaning When
200 Success GET, HEAD, successful upload/mirror/delete
3xx Redirect GET with CDN redirect (must preserve hash in URL)
400 Bad Request Invalid headers, malformed auth
401 Unauthorized Missing or invalid authorization
402 Payment Required BUD-07 paid servers
403 Forbidden Hash banned, user blocked
404 Not Found Blob doesn't exist
411 Length Required Missing Content-Length / X-Content-Length
413 Content Too Large File exceeds server limit
415 Unsupported Media Type Server doesn't accept this MIME type

All error responses MAY include X-Reason header.


Server Limits Notes
blossom.nostr.build 100 MiB hard, 20 MiB free Run by nostr.build team. Supports BUD-01,02,04,05,06,08
blossom.band 100 MiB hard, 20 MiB free Community server
blossom.primal.net Integrated with Primal stack Uses /media by default, strips metadata
cdn.satellite.earth Unknown Satellite CDN
blossom.azzamo.net Free tier + premium Azzamo's server
blosstr.com Enterprise-grade Commercial offering

Rate limiting is not standardized in the protocol. Each server implements its own policies. Free tiers generally have stricter limits. BUD-06 HEAD preflight is the mechanism for discovering server limitations before uploading.


Client Implementations

Amethyst (Kotlin -- upstream)

Quartz library (quartz/src/commonMain/kotlin/.../nipB7Blossom/):

  • BlossomAuthorizationEvent -- Kind 24242 event creation (get, upload, delete, list)
  • BlossomServersEvent -- Kind 10063 server list management
  • BlossomUploadResult -- Blob Descriptor deserialization (kotlinx.serialization)
  • BlossomUri -- BUD-10 URI parsing/serialization

Android app (amethyst/src/main/java/.../service/uploads/blossom/):

  • BlossomUploader -- PUT /upload + DELETE implementation using OkHttp
  • BlossomServerResolver -- BUD-10 URI resolution with LruCache
  • ServerHeadCache -- HEAD request caching for blob existence checks
  • UploadOrchestrator -- Orchestrates NIP-95, NIP-96, and Blossom uploads

Upload flow:

  1. Read file, compute SHA-256 hash + size
  2. Compute blurhash metadata locally
  3. Create kind 24242 auth event (t=upload, x=hash, expiration=+1hr)
  4. Base64-encode auth event JSON
  5. PUT /upload with Authorization: Nostr <base64>, Content-Type, Content-Length
  6. Parse Blob Descriptor response
  7. Download + verify the uploaded file (re-hash check)

Key: Amethyst does NOT use /media endpoint. Uses /upload only. No mirroring implemented.

Primal

  • All Primal 2.2+ apps use /media (BUD-05) by default
  • Strips all metadata before saving
  • Optionally mirrors to other Blossom servers per user settings
  • Deeply integrated into Primal stack, enabled by default

Nostrify (TypeScript/Web)

const uploader = new BlossomUploader({
  servers: ['https://blossom.primal.net/'],
  signer: window.nostr,
  expiresIn: 60, // seconds
});

const tags = await uploader.upload(file);
// Returns NIP-94 tags: url, x, ox, size, m
  • ox tag = original hash (before server processing)
  • x tag = final hash (after optimization if /media used)

NDK Blossom (@nostr-dev-kit/ndk-blossom)

npm package wrapping BUD-01 through BUD-06. TypeScript.

Dart NDK (dart-nostr.com)

Has Blossom use case documentation. Flutter/Dart integration.


Key Protocol Design Decisions

  1. Content addressing via SHA-256 -- Same file = same hash everywhere. Deduplication is free.
  2. Servers are interchangeable -- Any server with the blob can serve it. URLs break? Find the hash elsewhere.
  3. No server-side processing on /upload -- Bit-perfect storage. Hash computed over exact bytes received.
  4. /media is the exception -- Trusted server processes/optimizes. New hash for result.
  5. Authorization is opt-in per endpoint -- Servers choose what to protect.
  6. User controls server list -- Kind 10063 event = user's preferred servers.
  7. Mirror for redundancy -- Upload once, mirror to N servers.

Unanswered Questions

  • Does BUD-11 require base64url (no padding) or standard base64? Spec says base64url, Amethyst uses standard base64 -- servers seem to accept both.
  • What's the recommended expiration window for auth tokens? Amethyst uses 1 hour.
  • How do clients handle the /media flow when the optimized hash differs from original? Need to re-sign auth for mirror requests with the new hash.
  • Is there a standard way to discover server capabilities (which BUDs supported)? Not currently -- no capability endpoint defined.
  • How to handle upload failures mid-stream for large files? No chunked upload in spec.
  • Server-side dedup behavior when same hash uploaded by different users? Implementation-specific.