Visit project on GitHub
Set theme to dark

Easily make your R2 bucket publicly-readable on your own domain

R2 has an S3-compatible API , but does not yet support the public-read ACL, although it is on their roadmap.

We made an open-source utility worker available to do this prior to official support. It's a single one-time denoflare push command to deploy it to your own account and zone (via Custom Domains for Workers ), no git commands or any other tools necessary.

This worker might still be useful for use cases where you want to run custom logic or auth over standard public buckets.


This open-source worker makes a single R2 bucket available via public-read with the following features:

  • Supports conditional requests, range requests, and objects stored with pre-existing content-encoding
  • (optional) Input flag to enable directory listing as html, with a configurable page limit
  • (optional) Input flag to enable routing similar to Cloudflare Pages , index.html served for directories, root level 404.html etc
  • (optional) Allow/deny ip lists
  • (optional) Enable CORS access to all or specific request origins, and optionally further restrict CORS access to specific file types


You'll need:

  1. A Cloudflare account, with R2 billing enabled (see the official Purchase R2 instructions).
  2. An R2 bucket with data you want to make publicly available.
  1. A desired target domain (or subdomain) on an active Cloudflare zone in your account.
  2. A Cloudflare custom API token with permissions to manage Workers Scripts and Custom Domains for Workers for your target zone.
Minimal permissions needed for Custom Domains for Workers

Note: you'll need "Read Stream" permissions as well for some reason

You can limit these permissions to the target zone(s) for this worker.

  1. A working installation of Deno and denoflare (see installation)

Deploy it to your own account

You can deploy our public-read example worker like any other worker with denoflare push.

Let's say your your Cloudflare account id is f2601bf4d2d5ddcb17981afe4db16dd2, your API token secret is ABCDEFGHIJKLMNOPQRSTUVWXYZ, and your bucket name is my-bucket.

You can make this bucket available (for reading) at with the following command:

denoflare push \
   --name my-bucket-public-read \
   --r2-bucket-binding bucket:my-bucket \
   --text-binding flags:listDirectories \
   --text-binding allowCorsOrigins:* \
   --custom-domain \
   --account-id f2601bf4d2d5ddcb17981afe4db16dd2 \

That's it! 🎉

Your bucket will be available at

Your worker will be listed under your account, named my-bucket-public-read.


The worker takes five environment variables

  • bucket: (required) Your r2 bucket name
  • flags: (optional) Comma-separated flags:
    • listDirectories: Display an html listing for directories
    • emulatePages: Cloudflare Pages-like routing for directories using index.html
    • disallowRobots: Serve a robots.txt that disallows all robots, regardless of bucket contents
  • denyIps: (optional) Comma-separated ip addresses to deny (applied first)
  • allowIps: (optional) Comma-separated ip addresses to allow (applied second)
  • directoryListingLimit: (optional) Page limit (in listDirectories mode)
    • Currently defaults to max (1000) due to a known R2 listing bug
  • allowCorsOrigins: (optional) Comma-separated request origins for which CORS is allowed. e.g. * or,
  • allowCorsTypes: (optional) Comma-separated file extensions (.mp4, .m3u8, .ts) or content types (video/mp4, application/x-mpegurl, video/mp2t) to further restrict CORS, provided the origin is also allowed


As with any Denoflare script, you can specify the environment variable bindings to denofare push using the command line, or in your .denoflare config file.

The following are equivalent:

denoflare push \
   --name my-bucket-public-read \
   --r2-bucket-binding bucket:my-bucket \
   --text-binding flags:disallowRobots,emulatePages \
   --text-binding allowIps: \
   --text-binding directoryListingLimit:20 \
   --text-binding allowCorsOrigins:* \
   --custom-domain \
   --account-id f2601bf4d2d5ddcb17981afe4db16dd2 \


denoflare push my-bucket-public-read

With the following ~/.denoflare.jsonc

    // For auto-completion!
    "$schema": "",

    // Named worker script configurations
    "scripts": {

        // worker name
        "my-bucket-public-read": {

            // path can also be a local file path if you've modified the worker locally
            "path": "",

            "bindings": {
                "bucket": { "bucketName": "my-bucket" },
                "flags": { "value": "disallowRobots,emulatePages" },
                "allowIps": { "value": "" },
                "directoryListingLimit": { "value": "20" },
                "allowCorsOrigins": { "value": "*" },
            "customDomains": [ "" ],

    // Cloudflare account credentials
    "profiles": {
        "token-1": {
            "accountId": "f2601bf4d2d5ddcb17981afe4db16dd2",

See the denoflare push documentation for more info