AssetFlow / Asset CDN
Live · serving 6,219 assets from Cloudflare R2

This is where our images actually live.

AssetFlow is a small, self-hosted CDN we run on top of Cloudflare R2. Every image and video your apps use is stored here and served straight from Cloudflare's edge network — no per-GB bandwidth bill, no vendor lock-in.

One rule for integrators: store id, render delivery_url, never build a URL. Every asset the API returns carries both.

6,219
Images stored
2.7GB
Total library
$0/GB
Egress

Quick start

Four ways to use AssetFlow — pick the one that matches the work you're doing.

Take delivery_url from the API response (or the dashboard's "Copy URL") and drop it into any <img>, <video>, CSS background-image, or fetch — no auth required.

HTML
<img src="https://katalyst-iceberg-delivery.juliavictorio16.workers.dev/a/387f8ade-9d5c-4c58-b7a5-3f9f2f0a1b2c" alt="…" />

<!-- video: poster and HLS hang off the same id -->
<video poster="https://katalyst-iceberg-delivery.juliavictorio16.workers.dev/a/387f8ade-…/poster.jpg"
       src="https://katalyst-iceberg-delivery.juliavictorio16.workers.dev/a/387f8ade-…/hls/master.m3u8"></video>

The URL never changes: replace, roll back, move or rename the asset and the same /a/<id> keeps serving. Do not build a URL from a key or read StorageKey; see the URL forms below for the two older shapes you may still meet.

CLI reference

Every iceberg subcommand.

Command Purpose
iceberg loginAuthenticate and cache an API token at ~/.iceberg/config.json
iceberg whoamiShow the account behind the saved token
iceberg config showPrint saved url + token + tenant + cdn
iceberg config set --url --token --tenant --cdnSet the API base, token, tenant id, and public CDN base URL
iceberg upload <file> [--key K] [--content-type CT] [--legacy]Presigned PUT directly to R2, then promotes the row to ready. --legacy sends bytes through the API
iceberg ls [--prefix P] [--folder F] [--type CT] [--status S] [--limit N] [--after ID] [--all]One page of assets (default 100, max 1000), newest first. --all follows the cursor to the last page
iceberg url <key>Print the legacy /<tenant>/<key> alias URL, which redirects to /a/<id>. For the canonical URL read delivery_url from the API
iceberg rm <key>Permanent delete (DELETE /assets/<key>): the asset must be trashed, past its retention window, and the token minted with can_purge
iceberg migrate cloudinaryBulk import from a Cloudinary account (dry-run by default; --apply to upload)
iceberg media backfill [--dry-run] [--limit N]Run the metadata probe over assets that never had one
iceberg media bundle-backfill [plan | status | start | resume | cancel]HLS inventory and plan (read-only by default); mutations need --apply --approval-ref

The three URL forms

Only the first is a contract. The other two exist so nothing already pasted into a page breaks.

Form Example Status
/a/<id> katalyst-iceberg-delivery.juliavictorio16.workers.dev/a/387f8ade-… Canonical. What delivery_url returns; served by the edge Worker with no extra hop. Derived media hang off it: /poster.jpg, /hls/master.m3u8, /variants/<256|1024|2048>, /playback.json.
/<tenant>/<key> katalyst-iceberg-delivery.juliavictorio16.workers.dev/t1/hero/welcome.webp Legacy, kept permanently. Every pre-2026-09-04 path (the Cloudinary migration included) is a durable alias. Slower: one authority lookup plus a 308 to /a/<id> per request. New integrations must not create more of these.
/objects/<id> katalyst-iceberg-delivery.juliavictorio16.workers.dev/objects/387f8ade-… Compatibility only, retired 2026-12-31. Answers 308 to /a/<id> until then, 404 after. Backfill stored HTML to delivery_url before that date.

API reference

Hand-written OpenAPI 3.1 for the public integration surface, version 1.0.0. Every path and field in it exists in the router.

For AI assistants

Paste this URL into Claude, ChatGPT, Cursor or any assistant that can fetch a page, then ask it to write your integration. It gets the one rule, the upload flow, paging, trash rules and the error shape in one read.

https://katalyst-iceberg-delivery.juliavictorio16.workers.dev/llms.txt

How it's built

The architecture is intentionally boring. Each layer has one job.

Storage

Cloudflare R2

Object storage that's S3-compatible and bills $0.015/GB stored. Egress to the internet via Cloudflare's edge is free.

Delivery

katalyst-iceberg-delivery.juliavictorio16.workers.dev

A Cloudflare Worker in front of R2. It checks with the API that an asset is still deliverable (not trashed, not unpublished), then serves from the edge cache. The API is never in the byte path.

Catalog

Go API + Postgres

Small Go binary on Railway. Owns auth, tenant policy, presigned-URL minting, and the metadata rows. Egress here stays flat regardless of image traffic.

Uploads

Direct-to-R2 presigned

Client → R2 directly. The API hands out a one-time signed URL and verifies completion. A 5 GB upload costs the same in server bandwidth as a 5 KB one.

Common questions

What if a URL returns 404?

Either the asset is in the trash (the edge answers 404 at every URL form until it is restored), or the URL was built by hand from a key instead of taken from delivery_url. Look the key up with GET /assets?prefix=<key>&limit=1 (or status=trashed) and render the URL it returns.

Can I resize an image on the fly?

Fixed widths, yes. Optimizable images get 256, 1024 and 2048 px variants at upload, served at /a/<id>/variants/<width>. Arbitrary on-the-fly sizes are not offered.

What does this cost monthly?

Around $5/month total — fixed compute for the API + Postgres + a few cents of R2 storage. The bill does not scale with image traffic; that's the whole point of the architecture.