Uploads and delivery
Serve public and private objects
Choose public buckets, custom domains, Worker authorization, cache policy, and download headers for the required audience.
Before serving anything from R2, answer one question per object: may anyone on the internet read this?
The answer picks the delivery path.
Public objects: a custom domain
Public assets can use a custom domain attached to the bucket. Cloudflare serves them directly, with no Worker code involved:
npx wrangler r2 bucket domain add my-app-files \
--domain files.example.com --zone-id 023e105f4ecef8ad9ca31a8372d0c353
Now https://files.example.com/logos/header.svg serves the object stored at key logos/header.svg. The domain runs through Cloudflare, so cache rules and the object’s Cache-Control metadata apply. A popular image gets served from the edge instead of hitting the bucket every time.
There’s also an r2.dev URL you can enable per bucket. It’s rate-limited and meant for testing. Don’t put production traffic on it.
Private objects: a Worker in front
Private user files stay behind a Worker, or behind a short-lived presigned URL. Either way, something checks authorization before the bytes move. Here’s the Worker version:
const allowed = await userMayRead(session, exportId)
if (!allowed) return new Response('Forbidden', { status: 403 })
const object = await env.FILES.get(`exports/${exportId}.csv`)
if (!object) return new Response('Not found', { status: 404 })
const headers = new Headers()
object.writeHttpMetadata(headers)
headers.set('Content-Disposition', 'attachment; filename="report-july.csv"')
headers.set('Cache-Control', 'private, no-store')
return new Response(object.body, { headers })
Set the content type and Content-Disposition on purpose. attachment forces a download with a filename you choose. inline renders in the browser. Without either, browsers guess, and they don’t all guess the same way.
Configure CORS only for the browser origins and methods that need it. A wildcard origin on a bucket that serves anything private is a standing invitation.
Keys leak
Object keys end up in logs, browser history, and referrer headers. So a public URL must never carry a secret. A URL like files.example.com/exports/acme-corp-revenue-2026.csv on a public bucket is one forwarded link away from an incident.
If the audience is “this user only”, the object belongs behind the Worker check. An unguessable name is not a substitute for authorization.
Try this: serve one public image and one private export, then check three things. An anonymous request gets the image but a 403 on the export. The image response carries your cache headers. The export downloads with the filename you set.
Lesson completed