---
url: /en/docs/media/cloudflare-r2.md
description: >-
  Use Cloudflare R2 as an external media storage provider in Sveltia CMS for
  cost-effective, globally distributed asset management.
---

# Cloudflare R2 Integration

[Cloudflare R2](https://www.cloudflare.com/products/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](#credentials) below).
* A `public_url` configured for asset previews (see [Public Read Access](#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](#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](/en/docs/ui/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:

::: code-group

```yaml [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 [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 [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 [JavaScript]
{
  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

| Property | Required | Description |
| --- | --- | --- |
| `access_key_id` | Yes | R2 Access Key ID (64 hex characters). Safe to store in config. |
| `bucket` | Yes | The R2 bucket name. |
| `account_id` | Yes | Your Cloudflare account ID. Used to construct the S3 API endpoint. |
| `public_url` | Yes | Public URL for asset previews and downloads. Required because the R2 S3 API always requires authentication. |
| `prefix` | No | Path prefix within the bucket, e.g. `uploads/`. A trailing slash is added if missing. |
| `jurisdiction` | No | Cloudflare R2 [jurisdictional restriction](#jurisdictional-restrictions) 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:

| Jurisdiction           | S3 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](#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](/en/docs/ui/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](/en/docs/security#setting-up-content-security-policy) 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](/en/docs/fields/file) and [Image](/en/docs/fields/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](/en/docs/ui/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](/en/docs/ui/asset-library#folders-on-external-locations):

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