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.
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.
<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 login | Authenticate and cache an API token at ~/.iceberg/config.json |
| iceberg whoami | Show the account behind the saved token |
| iceberg config show | Print saved url + token + tenant + cdn |
| iceberg config set --url --token --tenant --cdn | Set 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 cloudinary | Bulk 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.
/docs
Interactive reference rendered from the spec. Try requests with your own token.
/openapi.json
OpenAPI 3.1. Feed it to a client generator, Postman, or your own validator.
/llms.txt
The whole API in about 1,600 tokens of Markdown. /llms-full.txt adds one curl per endpoint.
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.
Cloudflare R2
Object storage that's S3-compatible and bills $0.015/GB stored. Egress to the internet via Cloudflare's edge is free.
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.
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.
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.