Media & Files

Files live in an R2 bucket, served through signed URLs. Images get on-the-fly transforms at the edge, cached for a year.

Serving

Objects are served at /__r2/{key} only with a valid HMAC-SHA256 signature and unexpired timestamp — direct unsigned access returns 403. Templates access files through the file data source, which returns metadata plus a signed URL (default TTL: 1 hour, configurable via expires).

Uploads

Uploads use a two-step signed-URL flow, so file bytes pass through the API exactly once:

  1. Mint an upload URL (requires your API JWT):
curl -X POST https://<worker>/api/files/upload-url \
  -H "Authorization: Bearer <JWT>" \
  -H "Content-Type: application/json" \
  -d '{"key": "blog/cover.jpg", "contentType": "image/jpeg", "expiresIn": 600}'
  1. Upload the raw bytes directly with HTTP PUT to the returned URL before it expires:
curl -X PUT "<signed-url>" \
  -H "Content-Type: image/jpeg" \
  --data-binary @cover.jpg

Over MCP, the create_upload_url tool does step 1 — agents upload files themselves in one round trip.

Image optimization

Every image URL — static assets and R2 files alike — accepts transformation parameters served by Cloudflare Images: resize and crop (w, h, fit, gravity), format negotiation (f=auto serves AVIF/WebP per browser), compression (q), device pixel ratio (dpr), and more. Transformed variants are generated once and cached at the edge for a year.

<!-- R2 signed URLs: append transforms with & -->
<img src="{{ data.cover.url }}&w=800&h=600&fit=cover&f=auto&q=80" />

For the full parameter reference, format negotiation, srcset patterns, lazy-loading strategy, and quality guidance, see Image Optimization.