Devflare Docs
Guide Guides

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.

  • 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. 1

    The frontend asks your app for upload permission.

  2. 2

    Your Worker or backend authenticates the user and validates file type, size, and the target object key.

  3. 3

    Your backend returns a short-lived presigned URL.

  4. 4

    The browser uploads directly to R2.

  5. 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.

PatternUse it whenMain caveat
Public bucket on a custom domainImages, 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 readsAccess 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 endpointA 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 AccessOnly 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 validationYou 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

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.