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>
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-Typeregardless of extension - MUST default to
application/octet-streamif 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
Rangeheader (RFC 7233) on GET - Signal via
Accept-Ranges: bytesandContent-Lengthon 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 toGET /<sha256>endpoint with file extensionsha256-- Hex-encoded SHA-256 of the blobsize-- Size in bytestype-- MIME type (fallbackapplication/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-TypeandContent-Length - Client MAY provide
X-SHA-256header (hex lowercase) - Server MAY use
X-SHA-256for 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 resultssince/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
xtags 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:
- Extract 64-char hex hash from broken URL
- Fetch author's kind:10063 event
- Try each listed server in order
- 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-Typefrom origin server - Server verifies downloaded blob hash matches
xtag in auth token - Returns 2xx + Blob Descriptor on success, 4xx on failure
Typical flow:
- Client uploads to Server A, gets Blob Descriptor with URL
- Client sends URL to Server B's
/mirrorwith same upload auth token - Server B downloads from Server A
- Server B verifies hash matches
xtag - 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
ttag in auth event must bemedia(notupload)
HEAD /media
Same as HEAD /upload (BUD-06) but for the media endpoint.
Client Implementation Pattern
- User selects a "trusted processing" server
- Client uploads original media to
/mediaon trusted server - Gets back optimized blob descriptor (new hash)
- Client signs new upload auth for the optimized hash
- Calls
/mirroron 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
cashuBtoken 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¶m2=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:
- Direct server hints (
xs) viaGET /<sha256> - Author server lists (fetch kind:10063 for each
aspubkey) - 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)
- Event kind == 24242
created_at< nowexpiration> nowttag matches endpoint actionservertags (if present) include this server's domainxtags (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.
Popular Blossom Servers
| 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 managementBlossomUploadResult-- 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 OkHttpBlossomServerResolver-- BUD-10 URI resolution with LruCacheServerHeadCache-- HEAD request caching for blob existence checksUploadOrchestrator-- Orchestrates NIP-95, NIP-96, and Blossom uploads
Upload flow:
- Read file, compute SHA-256 hash + size
- Compute blurhash metadata locally
- Create kind 24242 auth event (t=upload, x=hash, expiration=+1hr)
- Base64-encode auth event JSON
- PUT /upload with
Authorization: Nostr <base64>,Content-Type,Content-Length - Parse Blob Descriptor response
- 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
oxtag = original hash (before server processing)xtag = 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
- Content addressing via SHA-256 -- Same file = same hash everywhere. Deduplication is free.
- Servers are interchangeable -- Any server with the blob can serve it. URLs break? Find the hash elsewhere.
- No server-side processing on /upload -- Bit-perfect storage. Hash computed over exact bytes received.
- /media is the exception -- Trusted server processes/optimizes. New hash for result.
- Authorization is opt-in per endpoint -- Servers choose what to protect.
- User controls server list -- Kind 10063 event = user's preferred servers.
- 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
/mediaflow 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.