Handle R2 uploads and file delivery explicitly instead of treating bucket URLs as the product
Use presigned URLs for direct uploads, public buckets on custom domains for truly public assets, and private buckets plus Worker auth for protected files. Keep out of production, and when a preview or environment needs its own bucket, scope it intentionally instead of borrowing production storage.
R2 itself is easy to bind. The hard part is the product boundary: should the browser upload directly, should reads stay behind your Worker, should teammates authenticate through Access, or should expiring custom-domain links be validated by a Worker or WAF rule? This page is the architecture guide for those choices.
- Safest upload default
- Presigned URL plus browser-direct upload plus object key stored in your app database
- Safest private delivery default
- Private bucket plus Worker-gated reads
- Do not ship this as prod delivery
- Team-only fit
- Custom domain plus Cloudflare Access
The fast rule set
- Use presigned URLs for direct user uploads to R2.
- Use a public bucket on a custom domain for truly public assets.
- Use a private bucket plus Worker authorization for authenticated or tenant-scoped files.
- Use Cloudflare Access when the bucket should be visible only to teammates or your organization.
- Use a Worker-signed URL flow or WAF HMAC validation for expiring custom-domain media links.
- Do not use for production delivery, and disable if you protect a custom-domain bucket with Access or WAF so the bucket is not still public there.
R2 binding mechanics are not the hard part
The architectural decision is whether the browser should talk to a signed upload URL, a public custom domain, or your own Worker route. That choice matters more than the one-line config.
The usual safe upload flow is direct upload with a presigned URL
This is the usual safe default because large files do not have to stream through your app server or Worker just to end up in object storage anyway.
Cloudflare's UGC guidance says the same thing: let the Worker control auth and upload intent, then let the client stream directly to R2. If you need post-upload workflows, R2 event notifications can push object-create events into Queues for moderation, metadata writes, or follow-up processing.
Cloudflare docs
Presigned URLs
UploadsCovers supported operations, security considerations, and the custom-domain limitation for presigned URLs.
Cloudflare docs
Configure CORS
Browser uploadsUse this when browser uploads or downloads cross origins and you need the exact allowed origins, methods, and headers model.
Cloudflare docs
R2 event notifications
Post-upload workflowsUse this when uploads should trigger queue-driven moderation, indexing, metadata writes, or other follow-up work.
- Generate object keys server-side, for example .
- Restrict when signing uploads so mismatched uploads fail signature validation.
- Keep upload URLs short-lived and treat them as bearer tokens while they remain valid.
- Configure bucket CORS when the browser uploads directly.
- If uploads arrive from many regions, Local Uploads can improve cross-region write performance without changing the overall architecture.
- 1
The frontend asks your app for upload permission.
- 2
Your Worker or backend authenticates the user and validates file type, size, and the target object key.
- 3
Your backend returns a short-lived presigned URL.
- 4
The browser uploads directly to R2.
- 5
Your app stores the object key and metadata, not the presigned URL.
Store object keys, not presigned URLs
Presigned URLs are temporary access tokens. The durable thing your app should remember is the object key plus the metadata you care about.
Choose the file-delivery pattern by who should be able to read the object
Cloudflare's public bucket docs are clear about this split: custom domains are the right place for cache, WAF, Access, and other edge controls, while is a development-oriented public URL and should not be treated as the polished product surface.
When the content is private or app-controlled, the safest default is still a private bucket with a Worker route in front of it. That keeps auth and response headers under your control instead of forcing the bucket URL to become your application boundary.
Cloudflare docs
Public buckets
Public deliveryCovers custom domains, caching, access control, and the production warning.
Cloudflare docs
Protect an R2 bucket with Access
Team-only accessBest when the audience is your own organization rather than anonymous or app-authenticated users.
Cloudflare docs
Configure token authentication
Expiring linksUse this when expiring custom-domain media links should be validated with WAF HMAC rules instead of R2 presigned URLs.
| Pattern | Use it when | Main caveat |
|---|---|---|
| Public bucket on a custom domain | Images, assets, or media should be public and cacheable for anyone. | Use a custom domain for real delivery; is not the production path. |
| Private bucket plus Worker-gated reads | Access depends on the current user, tenant, payment state, or other app authorization. | Your Worker becomes the delivery boundary, so own the auth, cache headers, and response metadata deliberately. |
| Presigned URL on the S3 endpoint | A download should be directly accessible for a short time without a custom delivery layer. | Presigned URLs are bearer tokens and do not work with custom domains. |
| Custom domain plus Cloudflare Access | Only teammates or organization users should reach the bucket. | Disable so the bucket is not still reachable through the public development URL. |
| Custom domain plus Worker token auth or WAF HMAC validation | You want expiring direct links on without exposing the whole bucket. | This is not the same feature as presigned R2 URLs; you are building or validating the access layer at the custom domain boundary. |
Keep development and production boundaries honest
Cloudflare's development guidance says local Worker development uses local simulated bindings by default, and Devflare follows the same practical posture: local R2 bindings are available to your worker code, tests, and bridge helpers without requiring a real remote bucket just to iterate.
Browser-visible local file flows should go through your Worker routes or app routes. Devflare does not promise a stable browser-facing local bucket origin, and depending on one would make local behavior more brittle than the product boundary probably needs to be.
- Only connect local development to a real remote bucket when you intentionally need integration testing.
- Use separate development, staging, or preview buckets instead of production buckets when remote R2 access becomes necessary.
- Remote bindings touch real data, incur real costs, and add real latency.
- In production, use a custom domain, choose public versus private delivery intentionally, configure CORS deliberately, and consider Local Uploads when uploaders are globally distributed.
Remote dev is not a harmless toggle
If your local Worker talks to a remote bucket, it is touching real data and real billing surfaces. Prefer separate dev or preview buckets, and avoid pointing local workflows at production uploads unless the test truly requires it.
Serve a private object through the Worker in local dev and production
A sane default architecture
Binding guide
R2 binding guide
R2 mechanicsOpen this once the architecture choice is done and the next question is the exact binding shape, local runtime behavior, or testing posture.
Configuration
Preview-scoped bindings
Preview-owned resourcesOpen this when preview deployments should own separate buckets or other disposable infrastructure that can be cleaned up by scope later.
Testing
createTestContext()
Local harnessOpen this when the next question is how the local worker-shaped test harness exposes real R2 bindings and helper surfaces.
- Public assets → public bucket plus custom domain.
- User uploads → presigned upload plus object key stored in D1 or another app database.
- Private assets → private bucket plus Worker-gated reads.
- Internal assets → custom domain plus Cloudflare Access.
- Custom-domain expiring links → Worker token auth or WAF HMAC validation.
- Preview-owned buckets → pair the R2 binding with so preview cleanup can remove the preview bucket without touching production storage.
If you only remember one rule
Use presigned URLs for short-lived direct R2 access, but use a Worker or custom-domain auth layer for polished private media delivery.
Previous
Storage strategy
Use this page to choose between KV, D1, R2, and Hyperdrive. Once the shape is clear, open the binding-specific guide for authoring, testing, and examples instead of reading several smaller pages that all repeat the same decision badly.
Next
State & async patterns
Use Durable Objects when one identity should own state or coordination. Use queues when work should happen later, in batches, or with retries. Then open the specific binding guide once the pattern is clear.