Media overview

storage stores bytes and takes no position on what they are. This one does.

@pithy-sh/media handles images, video, audio and documents: direct-upload URLs to the right backend per type, a record of what was uploaded, and opt-in enrichment — AI alt text, transcription, text extraction — running as Workflows on your own AI binding.

The backend follows the type

An image is not a video, and pretending otherwise costs you either quality or money. So config picks the backend per media type and the package does the rest:

TypeTypically
ImageCloudflare Images, for variants and transformation
VideoCloudflare Stream, for adaptive playback
Audio, documentsR2, through storage’s object-store seam

Direct upload means the bytes go from the browser to Cloudflare, not through your Worker. The Worker mints a URL and records a row.

Enrichment is opt-in, and it runs on your bill

Three Workflows, each optional and each off unless you ask for it:

Alt text for images. Generated on your own Workers AI binding. Useful for accessibility, and useful for search.

Transcription for audio. The text lands in your own D1 and is queryable.

Text extraction for documents. Same.

The inference runs on your binding, which means it lands on your bill and your customers’ files never leave your infrastructure to be processed. That is the same principle as the rest of the kit, applied to the one place where it is easy to quietly use somebody else’s API instead.

What that text is for

Transcripts and extracted text are the obvious thing to embed. vector is the capability that turns them into semantic search — a spoken sentence in a video becoming a search result needs no glue code between the two.

recordStore decides whether that text is queryable: d1 by default, so transcriptions and extracted text can be searched with SQL, or kv for a pure key-lookup workload.

It presigns through storage, and inherits nothing else

Media does not reimplement object storage. It uses storage’s ObjectStore seam — against its own bucket and its own credential name — and inherits none of storage’s tables, routes, quotas or share links for doing so.

That separation matters if you compose both: a user’s uploaded document and a user’s uploaded avatar are different products with different lifecycles, and they do not share a quota by accident.

What it deliberately does not do

No editing. No cropping, no trimming, no filters, no watermarks. Cloudflare Images does variants and transformations at serve time, which covers most of what people mean by this.

No moderation. Whether an image should be on your platform is a policy question with a human at the end of it. The enrichment Workflows describe content; they do not judge it.

No live streaming. Stream handles video-on-demand here. Live is a different product with different economics.

No DRM. Signed, expiring playback URLs are what you get.

When you would reach for it

When your users upload something you intend to do something with: display an image at three sizes, play a video at whatever bitrate the connection allows, search what somebody said in a recording.

If you only need to hold files and hand them back — an attachment, an export, a document — storage alone is smaller, simpler and cheaper.

What it needs

It requires secrets: two credential secrets are read through the registry, one for the Images and Stream token and one for the R2 key pair belonging to storage’s object store.

auth is optional and effectively required — an upload belongs to somebody.

pithy add media writes bindings and touches no Cloudflare account. The bucket, the namespace, the credentials and the enrichment host arrive with pithy media provision, which needs an R2 S3 key pair you make by hand and a token scoped to Images and Stream.

ESC