Visit project on GitHub
Set theme to dark

Easily make your R2 bucket readable via presigned URLs

A common use-case for cloud storage is to make files available, but only to authenticated users via expiring URLs that are presigned.

This feature will eventually be supported in R2 directly - but in the meantime, we've made an open-source utility worker available to support this.

Set up access with denoflare r2 generate-credentials, then a 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 open-source worker makes a single R2 bucket available via presigned URLs with the following features:

  • Supports conditional requests, range requests, and objects stored with pre-existing content-encoding
  • Use privately-generated credentials for your users, not your actual Cloudflare credentials
  • (optional) Allow/deny ip lists
  • (optional) Configurable max skew for authenticated requests and maximum expiration time for the presigned URLs


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 available via presigned URLs.
  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)

Generate credentials

Urls are presigned using a private accessKeyId and secretAccessKey. You can generate or regenerate these privately using denoflare r2 generate-credentials

$ denoflare r2 generate-credentials
  "accessKeyId": "b7fa983c14b8acaa85ed761edce0d1e1",
  "secretAccessKey": "859c95572351d6bac3f53a0431b3be33d5833aa39a9928176e008d60be340e70"

Deploy it to your own account

You can deploy our presigned-url 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-presigned-urls \
   --r2-bucket-binding bucket:my-bucket \
   --secret-binding credentials:b7fa983c14b8acaa85ed761edce0d1e1:859c95572351d6bac3f53a0431b3be33d5833aa39a9928176e008d60be340e70 \
   --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-presigned-urls.

Generate presigned urls using standard S3 tools like presign

$ AWS_ACCESS_KEY_ID=b7fa983c14b8acaa85ed761edce0d1e1 AWS_SECRET_ACCESS_KEY=859c95572351d6bac3f53a0431b3be33d5833aa39a9928176e008d60be340e70 \
    aws s3 presign s3://b/video.mp4 --endpoint-url


The worker takes seven environment variables

  • bucket: (required) Your r2 bucket name
  • credentials: (required) Comma-separated user credentials, where each credential is a colon-delimited keypair of accessKeyId:secretAccessKey.
  • flags: (optional) Comma-separated flags:
    • (None supported at the moment)
  • denyIps: (optional) Comma-separated ip addresses to deny (applied first)
  • allowIps: (optional) Comma-separated ip addresses to allow (applied second)
  • maxSkewMinutes: (optional) Number of minutes allowed before the authenticated request skew is considered too much (default: 15 minutes)
  • maxExpiresMinutes: (optional) Maximum amount of time allowed for presigned urls to expire (default: 7 days)


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-presigned-urls \
   --r2-bucket-binding bucket:my-bucket \
   --text-binding allowIps: \
   --secret-binding credentials:b7fa983c14b8acaa85ed761edce0d1e1:859c95572351d6bac3f53a0431b3be33d5833aa39a9928176e008d60be340e70 \
   --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-presigned-urls": {

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

            "bindings": {
                "bucket": { "bucketName": "my-bucket" },
                "allowIps": { "value": "" },
                "credentials": { "secret": "b7fa983c14b8acaa85ed761edce0d1e1:859c95572351d6bac3f53a0431b3be33d5833aa39a9928176e008d60be340e70" },
            "customDomains": [ "" ],

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

See the denoflare push documentation for more info