# How Buzz Implements Blossom Media Uploads with S3/MinIO Storage

> Learn how Buzz implements Blossom media uploads using S3/MinIO storage. Discover the Nostr kind 24242 authorization and SHA-256 content binding for secure, tenant-aware asset management.

- Repository: [Block Open Source/buzz](https://github.com/block/buzz)
- Tags: how-to-guide
- Published: 2026-08-29

---

**Buzz stores binary assets in S3-compatible object storage using the Blossom protocol, which requires Nostr kind 24242 authorization events with SHA-256 content binding and tenant-aware server tags.**

The Buzz Nostr relay (developed by Block) handles media through a cryptographically secured pipeline that implements the Blossom specification. This system binds upload tokens to specific file hashes and tenant hosts, then streams verified content to configurable S3-compatible backends such as MinIO or AWS S3.

## The Blossom Authentication Flow (Kind 24242 Events)

All media operations in Buzz require a signed Nostr event of **kind 24242** (the Blossom authorization kind). This event acts as a cryptographically verifiable capability token.

### Constructing the Authorization Event

Clients must generate an event containing four mandatory tags:

- **`t`** — The verb describing the intended action (`upload` or `get`)
- **`expiration`** — A Unix timestamp in the future after which the token becomes invalid
- **`x`** — The SHA-256 hash of the file body (required for uploads per BUD-11 §6)
- **`server`** — The hostname of the relay handling the request (enables multi-tenant isolation)

The signed event JSON is then serialized and transmitted in the HTTP `Authorization` header as `Bearer <event-json>`.

### Server-Side Verification

In [`crates/buzz-relay/src/api/media.rs`](https://github.com/block/buzz/blob/main/crates/buzz-relay/src/api/media.rs), the `extract_blossom_auth` function extracts the header and delegates validation to the `buzz-media` crate. The verification chain involves two critical functions in [`crates/buzz-media/src/auth.rs`](https://github.com/block/buzz/blob/main/crates/buzz-media/src/auth.rs):

1. **`verify_blossom_auth_event_for_`** — Validates the signature, ensures the kind is 24242, checks required tags are present, verifies the expiration timestamp has not passed, ensures the event was created recently (within the last 600 seconds by default), and confirms the `server` tag matches the tenant host.

2. **`verify_blossom_upload_auth`** — Performs all standard checks plus validates that the `x` tag matches the SHA-256 hash computed from the request body being uploaded.

```rust
// Server-side verification flow from crates/buzz-media/src/auth.rs
pub fn verify_blossom_upload_auth(
    event: &Event,
    expected_hash: &str,
    server: Option<&str>,
    max_age: u64,
) -> Result<(), AuthError> {
    verify_blossom_auth_event_for_verb(event, "upload", server, max_age)?;
    
    // Verify SHA-256 binding
    let x_tags: Vec<_> = event.tags.iter()
        .filter(|t| t.kind() == TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::X)))
        .collect();
    
    if !x_tags.iter().any(|t| t.content() == Some(expected_hash)) {
        return Err(AuthError::HashMismatch);
    }
    Ok(())
}

```

## S3/MinIO Storage Architecture

Once authorization passes, the system streams bytes to object storage through a thin abstraction layer.

### Upload Handling and Streaming

The entry point `buzz_media::upload::handle` in [`crates/buzz-media/src/upload.rs`](https://github.com/block/buzz/blob/main/crates/buzz-media/src/upload.rs) receives the verified request body and delegates to the storage layer. The `put_object` function in [`crates/buzz-media/src/storage.rs`](https://github.com/block/buzz/blob/main/crates/buzz-media/src/storage.rs) utilizes the AWS SDK for S3 (or compatible alternatives like `rusoto_s3`) to:

- Generate a unique object key (typically the SHA-256 hash from the `x` tag)
- Write the byte stream to the configured bucket
- Store metadata (MIME type, content size) in Postgres via `buzz_media::types`

```rust
// Streaming upload implementation reference
async fn handle_upload(
    auth_event: Event,
    body: impl AsyncRead,
    sha256: &str,
    bucket_cfg: &BucketConfig,
) -> Result<MediaMetadata, MediaError> {
    // Cryptographic verification first
    verify_blossom_upload_auth(&auth_event, sha256, None, 600)?;
    
    // Stream to S3/MinIO
    buzz_media::storage::put_object(bucket_cfg, sha256, body).await
}

```

### Configuration and Bucket Setup

Storage targets are defined through environment variables and the `BucketConfig` struct. The repository includes a MinIO container configuration for local development (see deployment compose files), while production deployments set the `BLOB_BUCKET` environment variable to target AWS S3, DigitalOcean Spaces, or other S3-compatible services.

```rust
// Configuration structure reference
pub struct BucketConfig {
    pub name: String,
    pub endpoint: String,
    pub region: String,
    pub access_key: String,
    pub secret_key: String,
}

```

## Integration Points

The media system spans three architectural layers: the relay API, the shared media crate, and the desktop client bridge.

### HTTP Endpoints in buzz-relay

The relay exposes REST endpoints at `POST /media` and `GET /media/<id>` in [`crates/buzz-relay/src/api/media.rs`](https://github.com/block/buzz/blob/main/crates/buzz-relay/src/api/media.rs). These handlers extract the Blossom auth header, verify tenant isolation via the `server` tag, and call into the `buzz-media` crate. The multi-tenant design ensures an auth token generated for `relay-a.example.com` cannot be replayed against `relay-b.example.com` even within the same process.

### Tauri Desktop Bridge

Desktop clients interact with the system through Tauri commands defined in [`desktop/src-tauri/src/commands/media.rs`](https://github.com/block/buzz/blob/main/desktop/src-tauri/src/commands/media.rs). This bridge signs the kind 24242 event using the user's Nostr private key, attaches the Bearer token to an HTTP request, and returns the resulting media URL to the frontend UI.

```rust
// Client-side upload flow
async fn upload_media(client: &Client, file_bytes: Vec<u8>, relay_host: &str, secret_key: &SecretKey) -> Result<String> {
    let sha256 = compute_sha256(&file_bytes);
    
    // 1. Create Blossom auth event
    let auth_event = Event::new(
        Kind::Custom(24242),
        secret_key,
        None,
        vec![
            Tag::custom("t", "upload"),
            Tag::custom("expiration", (now() + 600).to_string()),
            Tag::custom("x", &sha256),
            Tag::custom("server", relay_host),
        ],
        "upload token".to_string(),
    );
    
    // 2. Send with Authorization header
    let response = client
        .post(format!("https://{}/media", relay_host))
        .header("Authorization", format!("Bearer {}", auth_event.as_json()))
        .body(file_bytes)
        .send()
        .await?;
    
    Ok(response.json::<MediaResponse>().await?.url)
}

```

## Security Guarantees and Replay Protection

The Blossom implementation in Buzz provides three cryptographic guarantees:

- **Verb-specific authorization** — Tokens minted for `upload` cannot be replayed for `get` operations, preventing privilege escalation
- **Server-tag enforcement** — The `server` tag binds tokens to specific tenant hostnames, eliminating cross-tenant token leakage in multi-community deployments
- **Content-addressed integrity** — The SHA-256 `x` tag ensures the uploaded bytes match the authorized hash, preventing tampering between auth and storage

## Summary

- Buzz implements the Blossom protocol using **Nostr kind 24242 events** as authorization tokens for all media operations
- The `buzz-media` crate handles verification in [`crates/buzz-media/src/auth.rs`](https://github.com/block/buzz/blob/main/crates/buzz-media/src/auth.rs) using `verify_blossom_upload_auth` and `verify_blossom_auth_event_for_verb`
- Verified uploads stream to **S3-compatible storage** (MinIO or AWS S3) via `put_object` in [`crates/buzz-media/src/storage.rs`](https://github.com/block/buzz/blob/main/crates/buzz-media/src/storage.rs)
- The `server` tag and verb-specific checks prevent **cross-tenant replay attacks** and privilege escalation
- Desktop clients use the Tauri bridge in [`desktop/src-tauri/src/commands/media.rs`](https://github.com/block/buzz/blob/main/desktop/src-tauri/src/commands/media.rs) to sign events locally before transmission

## Frequently Asked Questions

### What is the Blossom protocol in Buzz?

**Blossom is a decentralized media hosting specification** that uses Nostr events for authorization. In Buzz, it refers specifically to the kind 24242 event flow where clients cryptographically sign upload or download intents, and relays verify these signatures before serving or storing binary data. This eliminates the need for traditional bearer tokens or API keys while maintaining user sovereignty over media access.

### How does Buzz verify media upload authorization?

**Buzz verifies uploads through a three-stage check** defined in [`crates/buzz-media/src/auth.rs`](https://github.com/block/buzz/blob/main/crates/buzz-media/src/auth.rs). First, `extract_blossom_auth` parses the Authorization header. Then `verify_blossom_auth_event_for_verb` validates the signature, checks the event kind is 24242, verifies the expiration timestamp, and ensures the `server` tag matches the request host. Finally, `verify_blossom_upload_auth` confirms the `x` tag matches the SHA-256 hash of the request body.

### Can Buzz use AWS S3 instead of MinIO?

**Yes, Buzz works with any S3-compatible storage provider.** The `BucketConfig` struct in [`crates/buzz-media/src/storage.rs`](https://github.com/block/buzz/blob/main/crates/buzz-media/src/storage.rs) accepts endpoint, region, and credential configurations via environment variables. While the development environment uses MinIO (configured in the Docker compose files), production deployments can target AWS S3, Cloudflare R2, or DigitalOcean Spaces by setting the appropriate `BLOB_BUCKET` and endpoint parameters.

### How does the SHA-256 x-tag prevent tampering?

**The `x` tag binds the authorization token to specific content.** When a client requests an upload token, they must include the SHA-256 hash of the file they intend to upload in the `x` tag. The server recomputes the hash from the actual request body in `verify_blossom_upload_auth` and rejects the request if the values diverge. This prevents attackers from obtaining a token for one file and uploading different content.