A Blossom media server for Nostr running on Fastly Compute, optimized for video content.
Client → Fastly Compute (Rust WASM) → GCS (blobs) + Fastly KV (metadata)
├── Cloud Run Upload (Rust) → GCS + Transcoder trigger
├── Cloud Run Transcoder (Rust, NVIDIA GPU) → HLS segments to GCS
├── Cloud Run Process-Blob (Python) → C2PA validation + SafeSearch moderation
└── Cloud Logging (audit trail)
- Fastly Compute Edge (
src/) - Rust WASM service on Fastly. Handles uploads, metadata KV, HLS proxying, admin, provenance - Cloud Run Upload (
cloud-run-upload/) - Rust service on GCP. Receives video bytes, sanitizes (ffmpeg -c copy), hashes, uploads to GCS, triggers transcoder, receives audit logs - Cloud Run Transcoder (
cloud-run-transcoder/) - Rust service on GCP with NVIDIA GPU. Downloads from GCS, transcodes to HLS via FFmpeg NVENC, uploads segments back - Cloud Run Process-Blob (
cloud-functions/process-blob/) - Python/Flask service on GCP. Triggered by GCS object finalization via Eventarc. Validates C2PA Content Credentials (c2patool) and runs SafeSearch moderation (Vision API), then updates Fastly KV metadata via webhook - GCS bucket:
divine-blossom-media - CDN:
media.divine.video(Fastly)
- BUD-01: Blob retrieval (GET/HEAD)
- BUD-02: Upload/delete/list management
- BUD-03: User server list support
- Nostr auth: Kind 24242 signature validation (Schnorr signatures)
- Shadow restriction: Moderated content only visible to owner
- Range requests: Native video seeking support
- HLS transcoding: Multi-quality adaptive streaming (1080p, 720p, 480p, 360p)
- WebVTT transcripts: Stable transcript URL at
/<sha256>.vttwith async generation - Provenance & audit: Cryptographic proof of upload/delete authorship with Cloud Logging audit trail
- Tombstones: Legal hold prevents re-upload of removed content
- Admin soft-delete: DMCA/legal removal with full audit trail while preserving recoverable storage
- Admin restore: Re-index and restore previously soft-deleted blobs
- C2PA trust checking: Validates Content Credentials (C2PA) manifests and signer trust chains on uploaded media
- Fastly CLI
- Rust with wasm32-wasi target
- GCP project with GCS bucket and Cloud Run
- Fastly account with Compute enabled
rustup target add wasm32-wasi- Create a GCS bucket with HMAC credentials
- Set up Fastly stores:
# Create KV store
fastly kv-store create --name blossom_metadata
# Create config store
fastly config-store create --name blossom_config
# Create secret store with GCS HMAC credentials
fastly secret-store create --name blossom_secretsConfig store flags:
| Key | Description | Default |
|---|---|---|
ENABLE_PHYSICAL_DELETE |
When "true", creator-delete actions via /admin/api/moderate physically remove bytes from GCS and purge edge caches. When "false", status flip only (bytes preserved). Admin DMCA via /admin/api/delete is unconditionally soft-delete regardless of this flag. |
"false" |
# Copy the example config and fill in your credentials
cp fastly.toml.example fastly.toml
# Edit fastly.toml with your GCS credentials (this file is gitignored)
# Then run:
fastly compute serveNote: fastly.toml is gitignored to prevent accidentally committing secrets. The [local_server.secret_stores] section is only used for local testing.
fastly compute publish| Method | Path | Description |
|---|---|---|
GET |
/<sha256>[.ext] |
Retrieve blob |
HEAD |
/<sha256>[.ext] |
Check blob exists |
GET |
/<sha256>.vtt |
Retrieve WebVTT transcript (on-demand generation) |
HEAD |
/<sha256>.vtt |
Check transcript status/existence |
GET |
/<sha256>/VTT |
Alias for transcript retrieval |
| Method | Path | Description |
|---|---|---|
POST |
/v1/subtitles/jobs |
Create subtitle job (video_sha256, optional lang, optional force) |
GET |
/v1/subtitles/jobs/<job_id> |
Get subtitle job status (queued, processing, ready, failed) |
GET |
/v1/subtitles/by-hash/<sha256> |
Idempotent hash lookup for existing subtitle job |
| Method | Path | Auth | Description |
|---|---|---|---|
PUT |
/upload |
Required | Upload blob |
HEAD |
/upload |
None | Get upload requirements |
DELETE |
/<sha256> |
Required | Permanently delete your own blob |
GET |
/list/<pubkey> |
Optional | List user's blobs |
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/<sha256>/provenance |
None | Get provenance info (owner, uploaders, auth events) |
POST |
/admin/api/delete |
Admin | Soft-delete blob, remove it from public serving/indexes, and optionally set legal hold |
POST |
/admin/api/restore |
Admin | Restore a soft-deleted blob to active, pending, or restricted |
Every upload and delete stores the signed Nostr auth event (kind 24242) in KV as cryptographic proof of who authorized the action. The /provenance endpoint returns:
{
"sha256": "abc123...",
"owner": "<nostr_pubkey>",
"uploaders": ["<pubkey1>", "<pubkey2>"],
"upload_auth_event": { ... },
"delete_auth_event": null,
"tombstone": null
}All uploads and deletes are logged to Google Cloud Logging via the Cloud Run upload service. Each audit entry includes: action, SHA-256, actor pubkey, timestamp, the signed auth event, and a metadata snapshot. Logs are queryable via Cloud Logging with labels service=divine-blossom, component=audit.
DELETE /<sha256>is a direct user delete and permanently removes the canonical blob.POST /admin/api/deleteis an admin soft-delete. It marks the blob asdeleted, stops all public serving, removes it from user/recent indexes, and preserves the stored blob so it can be recovered later.POST /admin/api/restorerestores a soft-deleted blob and re-indexes it.legal_hold: truesets a tombstone that prevents re-upload of the same hash even if the stored blob is preserved.
curl -X POST https://media.divine.video/admin/api/delete \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '{"sha256": "abc123...", "reason": "DMCA #1234", "legal_hold": true}'curl -X POST https://media.divine.video/admin/api/restore \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '{"sha256": "abc123...", "status": "active"}'When legal_hold: true, a tombstone is set preventing re-upload of the removed content (returns 403).
The cloud-functions/process-blob module validates C2PA Content Credentials on uploaded media using c2patool.
- Manifest extraction — runs
c2patool --detailed <file>to read the embedded C2PA manifest - Trust chain validation — runs
c2patool --detailed <file> trust --trust_anchors <trust_anchors.pem>to verify the signer against trusted certificate authorities
Validation results (manifest presence, trust status, claim generator, issuer) are attached to the blob's metadata via the Fastly KV webhook.
Controlled by the C2PA_MODE environment variable:
| Mode | Behavior |
|---|---|
off (default) |
No C2PA validation |
log |
Validates and logs results but does not block content |
enforce |
Rejects unsigned or untrusted content (sets status to restricted) |
| Variable | Description | Default |
|---|---|---|
C2PA_MODE |
Validation mode (off, log, enforce) |
off |
C2PA_TRUST_ANCHORS |
Path to trusted CA certificates (PEM) | /app/trust_anchors.pem |
C2PA_CHECK_IMAGES |
Also validate image uploads (true/false) |
false |
C2PA_MAX_FILE_SIZE |
Skip C2PA validation for files above this size (bytes) | 2147483648 (500MB) |
C2PA_WARN_FILE_SIZE |
Log a warning for files above this size (bytes) | 268435456 (256MB) |
The bundled trust_anchors.pem contains ProofSign CA certificates (ECDSA P-256) for verifying C2PA manifests signed by ProofMode on Android and iOS. Replace or extend this file with additional CA certificates as needed.
The module runs on Cloud Run as a Flask/gunicorn service. The Dockerfile installs c2patool v0.26.33 from GitHub releases. C2PA validation runs before SafeSearch moderation to short-circuit untrusted content early and save Vision API costs.
Management operations (PUT /upload, DELETE /<sha256>, GDPR vanish) use
Blossom auth events (kind 24242):
{
"kind": 24242,
"content": "Upload blob",
"tags": [
["t", "upload"],
["x", "<sha256>"],
["expiration", "<unix_timestamp>"]
]
}Send as: Authorization: Nostr <base64_encoded_signed_event>
Viewer/list requests additionally accept valid NIP-98 HTTP auth (kind 27235)
for the exact request URL and method. Protected blob/media GET routes also
accept Blossom GET auth (kind 24242) with:
{
"kind": 24242,
"tags": [
["t", "get"],
["x", "<sha256>"],
["expiration", "<unix_timestamp>"]
]
}If multiple Authorization headers are present, viewer auth succeeds when any
valid NIP-98 or Blossom GET header matches the request. age_restricted blobs
are served to any authenticated viewer and return 401 {"error":"age_restricted"}
to anonymous requests. restricted blobs remain shadow-banned and only serve
to the owner or an admin. Blossom does not currently read any hosted-session
age-verification claim or external viewer adult-verification service when
serving media.
Admin and moderation endpoints accept an X-Request-Id header and include
its value in related log lines so retries and partial failures can be traced
across stderr.
- If the caller sends
X-Request-Id, its first 16 characters are used. - If absent, the leading segment of the Cloudflare-provided
cf-rayheader is used (free correlation with Cloudflare edge logs). - If neither is present, a short hex ID is generated from the nanosecond clock.
Upstream services integrating with Blossom (e.g. divine-moderation-service
for creator-delete) are encouraged to forward a stable X-Request-Id
across their retry loops so both sides share the same correlation token.
Log lines are prefixed with [req=<id>]; the [PURGE] logs emitted by
the Fastly cache-purge path include the blob sha256 as their surrogate
key, which serves as the cross-reference when purging is triggered from
a moderate/delete request.
MIT