Cloudflare: Workers AI and D1, R2, KV storage#
vid2context.com runs as one Cloudflare Worker. Two provider packages let that Worker use Cloudflare's own free AI and storage:
providers/cloudflare-ai: Whisper speech-to-text, LLM chat and bge-m3 embeddings through the Workers AI binding inside a Worker, and Whisper over REST anywhere else.providers/storage-cloudflare: the library (saved contexts, collections, transcript search, key-value state) on D1, with R2 for large contexts and uploads, and KV for small state.
How bindings reach providers#
A Worker gets its bindings (env.AI, env.DB, ...) as objects next to its string vars. createWorkersContext(env) from @vid2context/registry/workers keeps string values in ctx.env and passes every other value through as ctx.bindings. Providers decide whether they are enabled from string env alone, so each piece is turned on by a var that names its binding, and the binding is looked up in ctx.bindings when it is used:
Var (string, in vars) |
Turns on | Reads binding |
|---|---|---|
CLOUDFLARE_AI_BINDING=AI |
Workers AI binding rungs: stt/cloudflare-binding/..., llm/cloudflare-binding/..., embed/cloudflare-binding/... |
ctx.bindings.AI |
VID2CONTEXT_D1=DB |
the D1 storage backend (priority 20, ahead of MemoryStorage) |
ctx.bindings.DB |
VID2CONTEXT_R2=MEDIA |
R2 for contexts over 1,000,000 bytes of JSON, and putBlob / getBlob / deleteBlob for uploaded media |
ctx.bindings.MEDIA |
VID2CONTEXT_KV=KV |
Workers KV as the storage's KvStore (without it, a D1 table) |
ctx.bindings.KV |
If a var names a binding that is missing, the call fails with no-provider and a hint showing the wrangler.jsonc entry to add. VID2CONTEXT_STORAGE=memory (or another backend's name) turns the D1 backend off.
wrangler.jsonc#
The bindings the deployed Worker needs, for the owner of the root wrangler.jsonc to adopt. Create the resources first:
npx wrangler d1 create vid2context # prints the database_id
npx wrangler r2 bucket create vid2context
npx wrangler kv namespace create vid2context # prints the id{
"ai": { "binding": "AI" },
"d1_databases": [
{
"binding": "DB",
"database_name": "vid2context",
"database_id": "<from wrangler d1 create>",
"migrations_dir": "providers/storage-cloudflare/migrations"
}
],
"r2_buckets": [{ "binding": "MEDIA", "bucket_name": "vid2context" }],
"kv_namespaces": [{ "binding": "KV", "id": "<from wrangler kv namespace create>" }],
"vars": {
"CLOUDFLARE_AI_BINDING": "AI",
"VID2CONTEXT_D1": "DB",
"VID2CONTEXT_R2": "MEDIA",
"VID2CONTEXT_KV": "KV"
}
}The Worker entry must build its context with createWorkersContext(env, signal) (it already does), and the server needs no other change: storage comes from the registry's enabled storage provider.
Migrations#
The schema is in providers/storage-cloudflare/migrations/ (numbered .sql files). Apply it with:
npx wrangler d1 migrations apply vid2context --remote # or --local for wrangler devThe same statements are built into the package, and the storage checks the schema_version table when it starts and applies anything missing, so a fresh database also works without the command. Every statement is idempotent. src/migrations.ts is the source; a test keeps the .sql files identical to it.
Tables: videos (one row per saved context, with the context JSON or an R2 key), segments (one row per transcript segment), segments_fts (an FTS5 index over segment text, kept in sync by triggers), collections, collection_items, kv (key-value state when no KV namespace is bound), vector_chunks (reserved for a vector index) and schema_version.
What the storage does#
- Search uses FTS5 with
bm25ranking. The query is reduced to words (letters and digits), each quoted, joined with OR, so FTS5 syntax in user input is never interpreted.collectionIdandvideoIdsnarrow the search. - Listing is newest first with an opaque cursor.
- Deleting a context also deletes its segments, search rows, collection memberships and R2 object. Deletes of unknown ids do nothing, as in
MemoryStorage; collection updates on unknown ids thrownot-found. - KV TTLs: Workers KV rejects
expirationTtlbelow 60 seconds, so shorter TTLs are raised to 60. The D1 fallback table honours any TTL. - Not included: a Vectorize-backed
VectorStore. Miniflare has no local Vectorize engine, so it could not be verified without creating a real index; until then the storage does not claimVectorStore, and callers fall back toMemoryVectors.
Binding versus REST#
| Inside a Worker (binding) | Node, browser or a Worker without the binding (REST) | |
|---|---|---|
| Speech-to-text | stt/cloudflare-binding/<model> |
stt/cloudflare/<model> with CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN |
| LLM | llm/cloudflare-binding/<model> (with streaming) |
llm/cloudflare/... in providers/llm-free-catalog |
| Embeddings | embed/cloudflare-binding/@cf/baai/bge-m3 |
embed/cloudflare/... in providers/embed |
Binding rungs need no token and skip a network hop. On Node, local rungs still come first under the default free policy; the REST rungs sit in the free tier with the other hosted free options.
Model input formats, verified against the live models on 2026-11-14: whisper-large-v3-turbo takes base64 audio in JSON and returns segments, words and the language; the older whisper takes raw audio bytes over REST (a byte array through the binding) and returns words, which are grouped into segments. Chat models return OpenAI-shaped choices and also a response field, which JSON mode returns already parsed. Workers AI defaults max_tokens to 256, so the rungs ask for 4,096 unless the caller sets maxTokens.
Free allocations (checked 2026-11-14)#
| Service | Free allocation | Source |
|---|---|---|
| Workers AI | 10,000 neurons per day on Free and Paid plans, reset 00:00 UTC. Whisper large-v3-turbo about 47 neurons per audio minute; bge-m3 about 1,075 neurons per million tokens | developers.cloudflare.com/workers-ai/platform/pricing |
| D1 (Free plan) | 5 million rows read and 100,000 rows written per day, 5 GB total, 500 MB per database, 10 databases. A row is at most 2,000,000 bytes; a query takes at most 100 bound parameters | developers.cloudflare.com/d1/platform/pricing and /limits |
| R2 | 10 GB-month storage, 1 million Class A and 10 million Class B operations per month, free egress (Standard storage only) | developers.cloudflare.com/r2/pricing |
| KV (Free plan) | 100,000 reads and 1,000 writes per day, 1 GB storage, values up to 25 MiB, keys up to 512 bytes, minimum TTL 60 s | developers.cloudflare.com/kv/platform/limits |
Saving a context writes one row per transcript segment (plus the FTS rows the triggers add), so a long video with 500 segments uses about 1,000 of D1's 100,000 free daily row writes.
Testing#
npx vitest run providers/storage-cloudflareruns the full Storage contract on Miniflare, which is workerd's real D1, R2 and KV, over the shared fixture contexts. No account needed.npx vitest run providers/cloudflare-aireplays responses recorded from Workers AI (REST directly, and the binding rungs through an adapter that returns the same recordedresult).LIVE=1withCLOUDFLARE_ACCOUNT_IDandCLOUDFLARE_API_TOKENset also runs REST Whisper and bge-m3 from Node, and startswrangler devwith the test Workers in each package'stest/worker/: one calls every binding rung against real Workers AI, the other saves contexts into local D1 and finds them again by search.