Direct upload
The bytes go from the browser to Cloudflare, not through your Worker.
Your Worker mints an upload URL for the right backend and records a row. The client uploads to that URL.
**Which backend depends on **the type, and that is the whole reason this capability exists separately from plain object storage: an image wants variants and transformation at serve time, a video wants adaptive playback, and a document wants neither.
The record is the thing you hold
A row in your own database, carrying what was uploaded, who owns it, and — once enrichment runs — the text.
The store’s own id is what the asset is keyed by, because both image and video stores mint one rather than taking a name you choose. So there is nowhere to put a project segment, and ownership is carried in metadata instead: every asset is stamped with the project and the environment.
That stamp is merged last, so a caller’s own metadata cannot displace it — and a Worker that cannot name its project refuses to mint rather than writing an asset nobody can attribute.
It is weaker than a name, and deliberately so: nothing stops two projects’ assets sitting side by side, because ids are unique and neither can adopt the other’s. What the stamp buys is attribution — knowing which app owns an asset, and being able to sweep one app’s without touching another’s.
Enrichment is opt-in, and runs on your bill
Three Workflows, each independent and each off unless you ask:
Alt text for images. Useful for accessibility, and useful for search.
Transcription for audio.
Text extraction for documents.
The inference runs on your own AI 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 quietly calling somebody else’s API would be invisible and convenient.
Turning on one does not turn on the others, and none costs anything until a file of that type is uploaded.
Reading the text back
In d1 record mode — the default — the text is queryable. Search a transcript with plain SQL, and join it against your own tables. Embed it instead and you have semantic search over your own media; either way the reference has the record shape.
In kv mode it is key lookup only, and you cannot search what it holds.
That is the one decision to make at add time, and the default is right unless you are certain you will never want to search: enrichment produces text specifically so that something can read it.
Feeding it to semantic search
Transcripts and extracted text are the obvious thing to embed.
Compose vector and a spoken sentence in a video becoming a search result needs no glue between the two — you write the extracted text into the corpus and query it.
Chunking stays yours, because how a long document is split depends on what your documents are.
It presigns through the object-store seam
Media does not reimplement object storage. It uses the same seam, against its own bucket and its own credential name — and inherits none of that capability’s tables, routes, quotas or share links.
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.
Serving
An image gets variants and transformation at serve time rather than at upload — so a new size is a URL rather than a re-upload of everything.
A video gets adaptive playback, so the bitrate follows the connection.
Signed, expiring playback URLs are what you get. There is no DRM.
What is not here
No editing. No cropping, trimming, filters or watermarks — though serve-time variants cover most of what people mean by this.
No moderation. The enrichment Workflows describe content; they do not judge it. Whether an image belongs on your platform is a policy question with a human at the end of it.
No live streaming. Video-on-demand only. Live is a different product with different economics.