Skip to content
Files SDK
Esc
navigateopen⌘Jpreview
On this page

Cloudflare R2

Cloudflare R2 over the S3-compatible HTTP API. Auto-loads R2_* env vars or accepts an R2Bucket binding inside Workers.

Installation

@aws-sdk/client-s3, @aws-sdk/s3-presigned-post, and @aws-sdk/s3-request-presigner are optional peer dependencies of files-sdk - install alongside the SDK so the adapter’s imports resolve at runtime.

npm install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner
pnpm add files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner
yarn add files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner
bun add files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner

Over the HTTP API, upload reports true byte-level progress via onProgress when the optional @aws-sdk/lib-storage package is installed (it’s loaded only when onProgress is used). Under the Workers R2Bucket binding the SDK reports progress generically instead — byte-level for stream bodies, start and finish for buffered ones.

Usage

Cloudflare R2 over the S3-compatible HTTP API. Auto-loads from R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY. Inside Cloudflare Workers you can pass an R2Bucket binding directly instead.

import { Files } from "files-sdk";
import { r2 } from "files-sdk/r2";

const files = new Files({
  adapter: r2({
    bucket: "uploads",
    accountId: process.env.R2_ACCOUNT_ID!,
    // accessKeyId / secretAccessKey auto-loaded
    // from R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY
  }),
});

publicBaseUrl - optional, an r2.dev subdomain or custom domain bound to the bucket. When set, url() returns `${publicBaseUrl}/${key}` and skips signing.

Options

R2AdapterOptions is a union of two shapes depending on whether you have a Workers R2Bucket binding available.

HTTP mode

PropType
bucketstring

R2 bucket name.

Typestring
accountId?string

Cloudflare account ID. Falls back to `R2_ACCOUNT_ID` env var; required if no env var is set.

Typestring
accessKeyId?string

R2 access key ID. Falls back to `R2_ACCESS_KEY_ID` env var; required if no env var is set.

Typestring
secretAccessKey?string

R2 secret access key. Falls back to `R2_SECRET_ACCESS_KEY` env var; required if no env var is set.

Typestring
publicBaseUrl?string

Origin used to build URLs from `url()` — typically an `r2.dev` subdomain or a custom domain bound to the bucket. When set, `url()` returns `${publicBaseUrl}/${key}` and skips signing. When unset, `url()` returns a presigned GetObject URL (default expiry: 1 hour).

Typestring
defaultUrlExpiresIn?number

Default expiry, in seconds, for `url()` when `publicBaseUrl` is unset. Defaults to 3600.

Typenumber

Binding mode (inside a Worker)

PropType
bindingR2Bucket

Workers `R2Bucket` binding. Reads and writes go through the binding.

TypeR2Bucket
bucket?string

R2 bucket name. Only used to label errors when reading via the binding.

Typestring
publicBaseUrl?string

Origin used to build URLs from `url()` — typically an `r2.dev` subdomain or a custom domain bound to the bucket. Without this (and without HTTP credentials below), `url()` throws because a Workers binding has no signing primitive.

Typestring
accountId?string

Hybrid mode: Cloudflare account ID, used alongside `accessKeyId` + `secretAccessKey` so `url()` and `signedUploadUrl()` can fall back to the S3-compatible HTTP signer instead of throwing. Reads and writes still go through the binding so they stay intra-Worker (no egress fees). Useful for Workers that need browser-facing presigned URLs without giving up the binding's I/O performance.

Typestring
accessKeyId?string

Hybrid mode: R2 access key ID. See `accountId`.

Typestring
secretAccessKey?string

Hybrid mode: R2 secret access key. See `accountId`.

Typestring
defaultUrlExpiresIn?number

Default expiry, in seconds, for `url()` when it falls back to HTTP signing (hybrid mode without `publicBaseUrl`). Defaults to 3600.

Typenumber

Hybrid: binding + HTTP credentials

Inside a Worker, you can pass both a binding and HTTP credentials. Reads and writes go through the binding (no egress, no extra round trip); url() and signedUploadUrl() route through the HTTP signer because a Worker binding has no signing primitive. The S3 client is lazy-loaded - bindings-only Workers don’t pull @aws-sdk/client-s3 into their bundle.

// Inside a Cloudflare Worker. The binding handles uploads/downloads
// (intra-Worker, no egress fees). The HTTP credentials let url() and
// signedUploadUrl() sign presigned URLs the binding alone can't produce.
const files = new Files({
  adapter: r2({
    binding: env.UPLOADS,
    bucket: "uploads",
    accountId: env.R2_ACCOUNT_ID,
    accessKeyId: env.R2_ACCESS_KEY_ID,
    secretAccessKey: env.R2_SECRET_ACCESS_KEY,
  }),
});

Signed uploads and maxSize

signedUploadUrl() returns a presigned PUT URL. Unlike S3, R2 does not implement the S3 POST Object API, so it has no content-length-range policy to enforce an upload size cap at the bucket. Passing maxSize throws a Provider error rather than handing back a POST form that R2 would reject with 501 Not Implemented at upload time.

// ✅ presigned PUT — the browser uploads with fetch(url, { method: "PUT", body: file })
const upload = await files.signedUploadUrl("avatars/abc.png", {
  expiresIn: 60,
  contentType: "image/png",
});

// ❌ throws: R2 has no server-enforced size limit
await files.signedUploadUrl("avatars/abc.png", {
  expiresIn: 60,
  maxSize: 5_000_000,
});

To cap upload sizes on R2, enforce the limit at your application gateway before issuing the URL.

Compatibility

HTTP mode

Method Status Notes
upload
download
delete
list
search
head
exists
copy
url
signedUploadUrl ⚠️ PUT URL only - Cloudflare R2 doesn’t implement the S3 POST Object API, so maxSize throws (no content-length-range policy; a presigned POST would 501 at upload time). Enforce upload caps at your application gateway instead.

Binding mode

Method Status Notes
upload
download
delete
list
head
exists
copy ⚠️ Read-then-write - Workers bindings have no native copy command, so the source is fetched and re-uploaded. Not server-side atomic; concurrent writes to the source between the get and put are not detected.
url Throws unless publicBaseUrl is set on the adapter (an r2.dev subdomain or a custom domain). For a presigned URL from a Worker, switch to hybrid mode by also passing accountId + accessKeyId + secretAccessKey.
signedUploadUrl Workers bindings can’t sign uploads - the secret access key is not available to the runtime. Use hybrid mode (binding + HTTP credentials) to issue presigned upload URLs.

Hybrid mode

Method Status Notes
upload
download
delete
list
head
exists
copy ⚠️ Read-then-write - copy goes through the binding (no native copy command on Workers).
url
signedUploadUrl ⚠️ PUT URL only - signing routes through the HTTP signer. R2 doesn’t implement the S3 POST Object API, so maxSize throws (no content-length-range policy; a presigned POST would 501 at upload time). Enforce upload caps at your application gateway instead.

Was this page helpful?