Skip to content

Cloudflare R2 Integration

Cloudflare R2 is an S3-compatible object storage service with zero egress fees. Sveltia CMS supports R2 as a media storage backend with direct browser-to-R2 uploads using AWS Signature Version 4 — no backend proxy is required.

Requirements

  • A Cloudflare account with an R2 bucket created.
  • An R2 API token with Object Read & Write permissions (see Credentials below).
  • A public_url configured for asset previews (see Public Read Access below).

CSP

If your site uses a Content Security Policy (CSP), you need to allow the R2 endpoint and your public URL. See Content Security Policy below for details.

Setup

Credentials

R2 uses its own API token system, separate from the Cloudflare global API key. Create a token via Cloudflare Dashboard > Account Home > R2 > Manage R2 API Tokens:

  • Permission: Object Read & Write
  • Scope: Restrict to the specific bucket (recommended)

The resulting Access Key ID (64 hex characters) goes in access_key_id in your config. The Secret Access Key is entered by users in the CMS UI when they access the media library for the first time — it is never stored in config.

Public Read Access

R2's S3 API endpoint always requires authentication, so a separate public_url must be configured for asset previews and downloads in the CMS. Without it, preview images will fail to load.

Two options are available:

Option A — Public development URL (non-production):

  1. In R2 Dashboard > [bucket] > Settings, under Public Development URL, click Enable and confirm.

  2. Copy the assigned pub-{hash}.r2.dev URL and set it as public_url in your config:

    yaml
    public_url: 'https://pub-abcdef1234567890abcdef1234567890.r2.dev'

Note: the r2.dev subdomain is rate limited and intended for development use only.

Option B — Custom domain (recommended for production):

  1. In R2 Dashboard > [bucket] > Settings > Custom Domains, click Add and follow the prompts to connect your domain.

  2. Set that domain as public_url in your config:

    yaml
    public_url: 'https://media.example.com'

In both cases, asset URLs are constructed as {public_url}/{key} (using the full object key, including any prefix).

CORS

Configure via R2 Dashboard > Bucket > Settings > CORS Policy. CORS is required because Sveltia CMS sends custom AWS Signature v4 headers that trigger a preflight request.

json
[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "PUT", "HEAD"],
    "AllowedOrigins": ["https://your-cms-domain.com"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3000
  }
]

Configuration

Here’s an example configuration for Cloudflare R2:

yaml
media_libraries:
  cloudflare_r2:
    access_key_id: abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890
    bucket: my-r2-bucket
    account_id: abcdef1234567890abcdef1234567890
    public_url: https://pub-abcdef1234567890abcdef1234567890.r2.dev
    prefix: cms-uploads/ # Optional
toml
[media_libraries.cloudflare_r2]
access_key_id = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
bucket = "my-r2-bucket"
account_id = "abcdef1234567890abcdef1234567890"
public_url = "https://pub-abcdef1234567890abcdef1234567890.r2.dev"
prefix = "cms-uploads/"
json
{
  "media_libraries": {
    "cloudflare_r2": {
      "access_key_id": "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
      "bucket": "my-r2-bucket",
      "account_id": "abcdef1234567890abcdef1234567890",
      "public_url": "https://pub-abcdef1234567890abcdef1234567890.r2.dev",
      "prefix": "cms-uploads/"
    }
  }
}
js
{
  media_libraries: {
    cloudflare_r2: {
      access_key_id: 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
      bucket: 'my-r2-bucket',
      account_id: 'abcdef1234567890abcdef1234567890',
      public_url: 'https://pub-abcdef1234567890abcdef1234567890.r2.dev',
      prefix: 'cms-uploads/', // Optional
    },
  },
}

WARNING

Do not write your Secret Access Key in the configuration file, as it should be kept confidential and not exposed in client-side code. Users will be prompted to enter the key when they use the storage first time, which will be stored securely in the browser’s local storage.

Configuration Properties

PropertyRequiredDescription
access_key_idYesR2 Access Key ID (64 hex characters). Safe to store in config.
bucketYesThe R2 bucket name.
account_idYesYour Cloudflare account ID. Used to construct the S3 API endpoint.
public_urlYesPublic URL for asset previews and downloads. Required because the R2 S3 API always requires authentication.
prefixNoPath prefix within the bucket, e.g. uploads/.

Content Security Policy

The hosts to allow depend on your public_url setting.

r2.dev subdomain — allow the exact pub-{hash}.r2.dev host:

connect-src https://abcdef1234567890abcdef1234567890.r2.cloudflarestorage.com;
img-src     https://pub-abcdef1234567890abcdef1234567890.r2.dev;

Custom domain — allow your custom domain, plus the S3 API endpoint for listing and uploading:

connect-src https://abcdef1234567890abcdef1234567890.r2.cloudflarestorage.com;
img-src     https://media.example.com;

See the CSP documentation for more details.

Accessing the Storage

The Cloudflare R2 media storage can be accessed through the File and Image fields in Sveltia CMS. Enter your Secret Access Key in the CMS UI when prompted, and you’ll be able to upload new media directly to R2 or select existing media from your bucket.

When uploading media, files will be stored in your R2 bucket, and you can take advantage of R2’s capabilities directly from the CMS. You can also select existing media from your R2 storage.

Future Plans

You’ll be able to manage your R2 files directly from the Asset Library in future releases.

Released under the MIT License.