pithy add media --with-prerequisites--with-prerequisites composes secrets, which media requires — two credential secrets are read through the registry.
Add auth too. An upload belongs to somebody, and without an identity composed those routes deny.
What lands in your repo
apps/<worker>/pithy.config.ts gains the registration:
media({
recordStore: "d1",
}),apps/<worker>/wrangler.jsonc gains two bindings it can write, in every environment stanza:
| Binding | Type | What it is |
|---|---|---|
DB | d1 | The media records — and, in d1 record mode, the transcriptions and extracted text |
MEDIA_BUCKET | r2 | Where audio and documents live |
Three Workflow bindings are not written here — MEDIA_IMAGE_TO_TEXT, MEDIA_AUDIO_TRANSCRIBE and the video one. They are optional, and wrangler requires both a name and a class_name on every entry with the deployed name being per project and environment. They arrive with provision.
One migration runs, creating the media record tables.
recordStore is the one decision to make now
| Value | What you get |
|---|---|
d1 (default) | Transcriptions and extracted text are queryable. Search a transcript with SQL, join it against your own tables |
kv | Key lookup only. Cheaper for a pure fetch-by-id workload, and you cannot search what it holds |
Pick d1 unless you are certain you will never want to search the text. Enrichment produces text specifically so that something can read it, and kv closes that door.
The choice also changes what provision creates: a MEDIA KV namespace exists only in kv record mode, and in d1 mode the binding is dropped rather than pointed at a namespace that never existed.
Then provision
pithy media provision --api-token "$IMAGES_STREAM_TOKEN" \
--r2-access-key-id "$R2_KEY" --r2-secret-access-key "$R2_SECRET"That creates the bucket and the namespace, writes the two credential secrets, and deploys the media worker that hosts the enrichment Workflows.
Two credentials, because there are two owners. One is media’s own token for minting Images and Stream direct-upload URLs. The other belongs to storage’s object store, which media presigns through and whose key pair media never sees.
Neither is minted. Cloudflare exposes no API for creating an R2 S3 access-key pair, and the permission catalog carries no Images or Stream keys. You make them by hand and hand them over — on the flags, or as R2_CREDENTIALS in the account config.
Each environment’s deploy needs the app DB id from that environment’s stanza and the environment’s secrets database, so pithy secrets provision has to have run. Both are refused rather than deployed around.
pithy media is the whole command surface, including what --storage deletes.
Turning enrichment on
Enrichment is off unless you ask for it, and it runs on your Workers AI binding — so the inference lands on your bill and your users’ files never leave your infrastructure to be processed.
Each of the three is independent. Turning on transcription does not turn on alt text, and neither costs anything until a file of that type is uploaded.
What to compose beside it
vector, if the extracted text is worth searching. Transcripts and document text are the obvious thing to embed, and a spoken sentence becoming a search result needs no glue code between the two.
storage, if your users also upload files you do not intend to process. The two do not share a bucket, a quota or a set of tables, which is what keeps an avatar and a data export from competing for the same allowance.
Check it worked
pithy doctor reports media under the Worker’s health. Before provision, the MEDIA_BUCKET binding reads as declared with no resource behind it — the correct state, not a fault.