> For the complete documentation index, see [llms.txt](https://mercure-technologies.gitbook.io/xprem/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mercure-technologies.gitbook.io/xprem/cdn/generic-cdn.md).

# Generic CDN

If you already have a CDN or an accelerated domain in front of your storage bucket, you can serve update assets through it without any provider-specific integration.

This works with the three cloud storage backends: set up [S3 Storage](/xprem/storage/s3-storage.md), [Google Cloud Storage](/xprem/storage/google-cloud-storage.md) or [Azure Blob Storage](/xprem/storage/azure-blob-storage.md) first, then put your CDN in front of the bucket or container.

{% hint style="warning" %}
The generic CDN only works with public endpoints: every asset behind the CDN domain must be reachable without a signature or token. If you need private delivery, use [CloudFront](/xprem/cdn/cloudfront.md) instead.
{% endhint %}

### Configure the base URL

Point `CDN_BASE_URL` at your CDN domain:

```dotenv
CDN_BASE_URL=https://cdn.example.com
```

{% hint style="info" %}
`CDN_BASE_URL` replaces the deprecated `S3_CDN_PREFIX` variable, named back when S3 was the only cloud storage backend. `S3_CDN_PREFIX` still works so existing deployments keep running, but the server logs a deprecation warning at boot and the variable will be removed in a future release. When both are set, `CDN_BASE_URL` wins.
{% endhint %}

From then on, the server answers each asset request with a redirect to the CDN. An asset is addressed by the hash of its content:

```
https://cdn.example.com/{appId}/cas/{hash}
```

Updates published before v3.2.0 keep their per-update layout and redirect to `https://cdn.example.com/{appId}/{branch}/{runtimeVersion}/{updateId}/{asset}`.

If your CDN expects the bucket name in the path, simply include it in the base URL:

```dotenv
CDN_BASE_URL=https://cdn.example.com/my-bucket
```

### Cloudflare R2

The most common use of the generic CDN is Cloudflare in front of an R2 bucket. R2 speaks the S3 API, so the storage side is the regular `s3` mode pointed at your R2 endpoint:

```dotenv
STORAGE_MODE=s3
S3_BUCKET_NAME=my-ota-bucket
AWS_BASE_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
AWS_REGION=auto
AWS_ACCESS_KEY_ID=<r2-access-key-id>
AWS_SECRET_ACCESS_KEY=<r2-secret-access-key>
CDN_BASE_URL=https://cdn.example.com
```

For the CDN side, do not point `CDN_BASE_URL` at the `r2.cloudflarestorage.com` endpoint: that endpoint is private and expects signed S3 requests. Instead, connect a custom domain to the bucket from the Cloudflare dashboard (R2, your bucket, Settings, Custom domains). The custom domain exposes the bucket publicly and serves it through the Cloudflare network, so update assets are cached at the edge. Point `CDN_BASE_URL` at that domain.

This combination works particularly well for OTA delivery:

* Objects are immutable and addressed by the SHA-256 of their content, so cached objects never go stale and you never need to purge. An asset that did not change between two updates keeps the same URL, so it stays a cache hit across releases. A "cache everything" rule on the domain is safe if you want to maximize the hit rate.
* R2 has no egress fees, and cache hits never reach the bucket at all, which makes this one of the cheapest ways to distribute updates.

The public-endpoint trade-off described above still applies: anyone holding a URL can download the assets behind the custom domain.

### Source maps

Source maps contain the app's source code. Store them in a dedicated private bucket or container that neither the CDN nor any other public endpoint serves. See [Source maps and symbolication](/xprem/observe/source-maps-and-symbolication.md).

When assets go through `CDN_BASE_URL`, the server refuses to start if `S3_BUCKET_SOURCEMAPS_NAME`, `GCS_BUCKET_SOURCEMAPS_NAME` or `AZURE_BLOB_SOURCEMAPS_CONTAINER_NAME` names the updates bucket or container:

```
UPLOAD_SOURCEMAPS: source maps cannot share the updates bucket when CDN_BASE_URL serves it; point them at a dedicated bucket
```

### Good to know

* If you use `BUCKET_KEY_PREFIX`, the server already adds it to the generated URLs, so don't repeat it in `CDN_BASE_URL` or it would be applied twice.
* CloudFront takes precedence over the generic CDN whenever its domain, key pair ID and private key are all configured.
* A configured `CDN_BASE_URL` takes precedence over pre-signed storage URLs: if you configure a CDN explicitly, the server assumes you want assets served through it. Keep in mind this requires the bucket to be public, while pre-signed storage URLs keep it private.
* The CDN only caches what the bucket serves; the server keeps writing updates directly to the bucket as usual.
* Bundle patches are the exception: the server serves them itself unless your CDN adds the response headers expo-updates requires. See [Bundle diffing](/xprem/features/bundle-diffing-bsdiff.md).
