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", "DELETE", "HEAD"],
    "AllowedOrigins": ["https://your-cms-domain.com"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3000
  }
]

The DELETE method and the wildcard AllowedHeaders are needed to delete and rename files from the Asset Library. If you configured CORS before these operations became available, add them to your existing policy. Otherwise, the browser will block those requests during the preflight check.

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
    jurisdiction: eu # Optional; 'default' | 'eu' | 'fedramp'
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/" # Optional
jurisdiction = "eu" # Optional; 'default' | 'eu' | 'fedramp'
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/",
      "jurisdiction": "eu"
    }
  }
}
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
      jurisdiction: 'eu', // Optional; 'default' | 'eu' | 'fedramp'
    },
  },
}

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 for the first time. It’s then 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/. A trailing slash is added if missing.
jurisdictionNoCloudflare R2 jurisdictional restriction for the bucket. One of 'default', 'eu', 'fedramp'. Defaults to 'default'.

Jurisdictional Restrictions ​

Cloudflare R2 supports two jurisdictional locations — EU and FedRAMP — for data residency compliance. Buckets created in one of these jurisdictions use a dedicated endpoint and will return an error if accessed via the global endpoint.

If your bucket was created in the EU or FedRAMP jurisdiction, set jurisdiction accordingly:

yaml
media_libraries:
  cloudflare_r2:
    # ...
    jurisdiction: eu # 'eu' for EU buckets, 'fedramp' for FedRAMP buckets

The jurisdiction also determines the S3 API endpoint hostname used for uploads and listings:

JurisdictionS3 API endpoint
default (or omitted)<account_id>.r2.cloudflarestorage.com
eu<account_id>.eu.r2.cloudflarestorage.com
fedramp<account_id>.fedramp.r2.cloudflarestorage.com

Remember to update your Content Security Policy to allow the correct endpoint if you use a non-default jurisdiction.

Content Security Policy ​

The hosts to allow depend on your public_url setting and jurisdiction.

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

connect-src https://abcdef1234567890abcdef1234567890.r2.cloudflarestorage.com
            https://pub-abcdef1234567890abcdef1234567890.r2.dev;
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
            https://media.example.com;
img-src     https://media.example.com;

Non-default jurisdiction — replace the S3 API endpoint in connect-src with the jurisdiction-specific one. For example, with jurisdiction: eu:

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

The public URL host must be in connect-src because the Download and Copy buttons and the text preview in the Asset Library fetch the file from it. For the same reason, it must also send CORS headers that allow the CMS origin. Otherwise, downloads fail.

See the CSP documentation for more details.

Accessing the Storage ​

There are two ways to use Cloudflare R2 in Sveltia CMS:

File and Image Fields ​

When editing content entries, users can use File and Image fields to upload and select media on Cloudflare R2 directly within the entry editor. After entering the Secret Access Key in the CMS UI when prompted, users can upload new media directly to R2 or select existing media from the bucket.

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

Asset Library ​

Cloudflare R2 also appears under External Locations in the Asset Library. There, users can browse, search, sort and filter the files in your bucket, and upload, rename, replace, download or delete them without leaving the CMS.

The files and folders are browsed folder by folder:

  • Subfolders are listed ahead of the files.
  • Uploads go to the folder being viewed.
  • Folders can be created, renamed and deleted.