Skip to main content

CDN settings

CDN settings

cmpatch selfhost install walks through a CDN when you want one. Cloudflare asks for an API token. CloudFront prints each console value and checks the result. This page is the settings for a step the wizard could not finish, a CDN you turn on after the first install, or a later run that no longer has those console answers.

Install is the path that does this for you.

Cloudflare​

For external storage, choose R2 for automated Cloudflare storage/domain/cache setup, or S3/GCS plus Cloud Connector (Beta) for the guided console path. The external download record is proxied before storage verification, and never points to the API server. The grey-cloud-first instructions below apply to bundled Silo.

For S3/GCS, use Rules → Cloud Connector: choose the matching provider, a dot-free bucket endpoint (BUCKET.s3.REGION.amazonaws.com or BUCKET.storage.googleapis.com), and only http.host eq "downloads.example.com". Accept the offered proxied DNS record; otherwise add a proxied CNAME to that endpoint. Add a separate Cache Rule for the same hostname, eligible for cache, respecting origin Cache-Control and Browser TTL with no Edge TTL override. Keep other hostnames outside both rules. No bucket prefix or rewrite is added by this flow. Public-read restrictions must already permit the origin. The wizard checks file content, cache hit, purge freshness and privacy before deploying.

Set this up while installing

For bundled Silo, on an uninterrupted cmpatch selfhost install, the Cloudflare step opens a pre-scoped token form, finds the zone id for you, and walks you through the proxy switch and Cache Rule after the server is up, checking each one. It is offered when your download domain is already on a Cloudflare account. Keep the download record DNS only (grey cloud) while the install runs: the wizard waits until the record resolves to the server directly, and switches the proxy on with you afterwards.

If you also let the wizard add your DNS records through Cloudflare, it uses the same temporary DNS setup token to finish these steps itself: it adds the Cache Rule, checks that SSL/TLS is Full or Full (strict), and turns the proxy on. A scripted run does this only with --dns-setup cloudflare. Anything it cannot do, such as a conflicting Cache Rule or Flexible SSL/TLS, is left for you with the steps below.

If you resume or repair after the server becomes healthy, the closing summary links back here because the CLI no longer has the original setup answers. Complete the proxy and Cache Rule checks below by hand. This page is also the reference for adding Cloudflare to a running server.

info

The default install works without Cloudflare (DELIVERY_ADAPTER=base-url), which is fine for trying Patch. For production we strongly recommend a CDN, since otherwise every device downloads from the single storage host: front the storage domain only (the API domain stays direct) so artifacts and manifests are served from the edge, and Patch can purge stale meta.json / manifest.json URLs after releases.

Front the storage domain with Cloudflare when you want edge caching and automatic cache purge. After every release, promotion, rollback, and deployment clear, the server purges the affected manifest URLs so clients do not keep stale manifests. Deleting a deployment also purges its public artifact URLs.

You can also use Cloudflare R2 as S3-compatible object storage (STORAGE_ADAPTER=s3). That is separate from putting Cloudflare's CDN in front of downloads. See Infrastructure. Automatic R2 setup uses a disposable setup token, then saves a different runtime token. Do not reuse the setup token as CLOUDFLARE_API_TOKEN. The runtime token includes account-level Workers R2 Storage Read, which can also read objects in other buckets in that account.

Follow this page in order: prepare credentials → install with flags → finish DNS and Cache Rules after the stack is up.

1. Prepare Cloudflare (before install)​

Collect the Zone ID and a Cache Purge API token (below). Pass both to the installer in the next step, then finish DNS proxying and Cache Rules after the stack is healthy.

Zone ID​

The storage domain must live in a Cloudflare zone. On the zone Overview page, copy Zone ID from the API section on the right. This becomes CLOUDFLARE_ZONE_ID. See Cloudflare's docs on finding your Zone ID.

Cache-purge API token​

Create a token at My Profile → API Tokens (not the account Global API Key). Account-owned tokens (cfat_…) live under <account> → Manage Account → API Tokens.

Use Create Token → Create Custom Token with a single permission:

FieldValue
PermissionsZone → Cache Purge → Purge
Zone ResourcesInclude → the zone that contains your storage domain

Copy the token once. This becomes CLOUDFLARE_API_TOKEN.

The installer verifies the token by calling Cloudflare's purge_cache API for the zone (requires Zone → Cache Purge). Pass --skip-cloudflare-check only if you need to bypass verification temporarily.

Free Cloudflare plans​

This guide uses a sibling storage name (storage-updates.example.com) because on Cloudflare Free, Universal SSL covers example.com and *.example.com only. A nested name such as storage.updates.example.com sits two levels below the apex and needs Advanced Certificate Manager. The installer suggests the sibling form for the same reason. Nested names still work if your plan covers them.

2. Install with Cloudflare flags​

Run the installer and add these flags on first install (delivery config is written into .env.selfhost only when that file is created):

export CLOUDFLARE_API_TOKEN=<cf_cache_purge_token>

cmpatch selfhost install ubuntu@203.0.113.7 \
--cloudflare \
--cloudflare-zone-id <cf_zone_id>

The guided install still asks for anything else it needs (the domains, the OAuth app, the admin email). Passing the same flags to scripts/selfhost/install.sh on the host, together with --cloudflare-api-token, does the same for an on-server install.

The installer sets DELIVERY_ADAPTER=cloudflare plus your token and zone ID. Leave MANIFEST_CACHE_CONTROL unset, and on an existing install, delete the no-cache, must-revalidate line a pre-CDN installer wrote, because an explicit value wins and would keep manifests out of the edge cache entirely. The server derives it from the delivery adapter, so selecting Cloudflare is what turns the mutable-JSON policy into public, max-age=0, s-maxage=300, must-revalidate: browsers and the Patch SDK revalidate immediately (max-age=0), while shared caches may retain a manifest for five minutes (s-maxage=300) that a release purge cuts short.

Certificate order matters

Caddy obtains Let's Encrypt certificates over HTTP on port 80 during install. Keep the storage DNS record DNS only (grey cloud) until the installer reports storage HTTPS as ready. Proxying too early can break the HTTP-01 challenge.

3. Finish the Cloudflare setup​

After install with the Cloudflare flags, the server side is already active: releases request edge purges. Complete the Cloudflare-side steps next.

Switch the DNS record to proxied​

  1. Keep the storage domain DNS only until storage HTTPS is ready (DNS).
  2. Switch the storage record to Proxied (orange cloud). Leave the API domain direct unless you have a separate reason to proxy it.
  3. Set SSL/TLS → Overview to Full (strict) (SSL/TLS). The origin serves a valid Let's Encrypt certificate, so strict validation works. Flexible would connect to the origin over HTTP, which Caddy redirects to HTTPS and can cause a redirect loop.

If a later certificate renewal fails while proxied, temporarily switch the record back to DNS only, let Caddy renew, then re-enable the proxy.

Add Cache Rules​

Create one rule on Cache Rules (<zone> → Caching → Cache Rules → Create rule):

Rule expressionCache eligibilityEdge TTLBrowser TTL
http.host eq "storage-updates.example.com"Eligible for cacheUse cache-control header if present, bypass cache if notRespect origin TTL

The rule is still required because Cloudflare does not cache JSON by default. Once the hostname is eligible, Cloudflare Origin Cache Control (enabled by default on Free, Pro, and Business) respects the origin headers: bundles and patches are content-addressed and immutable for one year, while meta.json / manifest.json use s-maxage=300. The server requests a purge for mutable URLs after releases, and the five-minute shared-cache TTL bounds staleness if a purge fails. Do not add an Edge TTL override; the Free-plan two-hour minimum applies to that override, not to the respected s-maxage origin directive.

Browser TTL must be set to Respect origin TTL as well. The zone-wide default (four hours on the Free plan) otherwise rewrites the origin's max-age=0 to max-age=14400, and iOS devices would then keep a stale manifest for up to four hours after a release; a purge cannot reach a device cache. Setting Caching → Configuration → Browser Cache TTL to Respect Existing Headers for the whole zone has the same effect.

Upgrading an existing zone

Earlier versions of this page had you add a second rule matching .json paths with Ignore cache-control header and use this TTL: 2 hours. Delete that rule. An Edge TTL override takes precedence over the origin s-maxage, so leaving it in place keeps meta.json / manifest.json stale at the edge for up to two hours after a failed purge instead of five minutes.

Verify​

# Second request should return cf-cache-status: HIT
DEPLOYMENT_KEY=your-deployment-key
URL="https://storage-updates.example.com/codemagic-patch/${DEPLOYMENT_KEY}/meta.json"

curl -sI "$URL" | grep -i cf-cache-status
curl -sI "$URL" | grep -i cf-cache-status

# Must print max-age=0 exactly as the origin sent it; max-age=14400 means
# the Browser TTL setting is missing
curl -sI "$URL" | grep -i cache-control

After publishing a release, a successful purge makes the same URL briefly report MISS again. Purging is best-effort: a failed purge never fails the release. It is logged as a delivery cache purge completed with failures warning in the server logs. Watch for that warning if clients report stale updates.

Also confirm basic health:

curl -fsS https://updates.example.com/health
curl -fsS https://storage-updates.example.com/minio/health/ready

Enable Cloudflare on an existing install​

Rerunning the installer with --cloudflare flags does not change an existing install. Delivery configuration is only written on initial install, and the rerun prints a warning instead. To enable it later, edit .env.selfhost:

DELIVERY_ADAPTER=cloudflare
CLOUDFLARE_API_TOKEN=<cf_cache_purge_token>
CLOUDFLARE_ZONE_ID=<cf_zone_id>

Then rerun scripts/selfhost/install.sh. It re-reads the file, verifies the credentials, and restarts the stack. Finish with Switch the DNS record to proxied and Add Cache Rules.

Cloudflare R2 (optional storage)​

R2 is S3-compatible. To use it instead of bundled Silo, set STORAGE_ADAPTER=s3 and point the S3 variables at your R2 bucket and endpoint. Full variable list: Configuration reference. R2 does not replace the delivery adapter: you can still front public downloads with Cloudflare CDN (DELIVERY_ADAPTER=cloudflare) while objects live in R2.

The installer can create those buckets. See Infrastructure.

CloudFront​

For external S3/GCS, the install wizard offers a separate bucket-origin walkthrough. It guides ACM DNS validation in us-east-1, a distribution using the public bucket origin, a distribution-scoped invalidation key and viewer DNS. Use minimum cache TTL 0 and respect origin Cache-Control. GCS uses the custom HTTPS origin storage.googleapis.com with Origin path /PUBLIC_BUCKET; S3 uses its regional public bucket endpoint. No API-host origin hostname or verification secret is used. Public-access policy must allow reads; this flow does not create an OAC exception. The wizard verifies content, cache hit and purge freshness before installing.

Set this up while installing

For bundled Silo, on an uninterrupted cmpatch selfhost install, the CloudFront step prints the exact values for each AWS Console screen, generates the origin secret, watches the certificate validation record, and runs equivalent pre-cutover probes before it tells you to move the DNS record. It creates nothing in AWS and never asks for AWS credentials beyond the scoped purge key. If an earlier run stopped after you had created the distribution, the next run asks whether one exists and keeps the origin header value it already sends instead of generating a new one.

If you resume or repair after the server becomes healthy, the closing summary links back here because the CLI no longer has the AWS Console answers from the original run. Complete every pre-cutover check below by hand before changing DNS. This page is also the reference for adding CloudFront to a running server.

CloudFront can sit in front of either the bundled Silo storage or an external S3/GCS-compatible bucket. Patch keeps manifest invalidation scoped and best-effort: publishing still succeeds if a purge fails, while the s-maxage=300 that this adapter selects for mutable JSON bounds stale edge content.

Choose one complete track below. The bundled track uses a separate protected origin hostname on your Patch host. The external-storage track points CloudFront at the bucket and does not use the Caddy origin settings.

Bundled Silo​

Start with three distinct hostnames. storage. and origin-storage. are easy to transpose in the CloudFront console, so write down the mapping first:

RoleExampleDNS before installDNS after cutover
API and dashboardupdates.example.comA/AAAA → Patch hostunchanged
Viewer/downloadstorage-updates.example.comA/AAAA → Patch hostCNAME → CloudFront
CloudFront originorigin-storage-updates.example.comA/AAAA → Patch hostunchanged
DNS ordering is mandatory

Keep the viewer/download hostname pointed at the Patch host for the entire install. Move it to CloudFront only after the distribution and protected origin pass the pre-cutover checks. Pointing it at CloudFront early prevents Caddy from completing HTTP-01 certificate issuance and leaves neither path ready to serve downloads.

1. Prepare DNS, certificate, and origin secret​

  1. Create the three DNS records shown above, all initially pointing at the Patch host. Lower the viewer record TTL to 60 seconds and let its old TTL expire before the final cutover.
  2. In AWS Certificate Manager, switch to US East (N. Virginia), us-east-1, request a public certificate for the viewer hostname, and complete DNS validation. CloudFront cannot use an ACM certificate from another region. See AWS certificate requirements.
  3. Generate a random origin-header value and keep it available for both the distribution and the installer:
openssl rand -hex 32

2. Create the distribution​

Create a standard CloudFront distribution with these settings:

SettingValue
Origin domainorigin-storage-updates.example.com
Origin protocolHTTPS only
Origin custom headerX-Codemagic-Patch-Origin-Verify: <generated-secret>
Alternate domain namestorage-updates.example.com
Viewer certificatethe ACM certificate from us-east-1
Viewer protocol policyRedirect HTTP to HTTPS
Allowed methodsGET, HEAD
Cache policyManaged CachingOptimized
Origin request policyNone; do not forward the viewer Host header

The origin Caddy site answers only for the origin hostname. Forwarding the viewer Host header makes origin requests hit the wrong Caddy site and defeats the protected-origin check. With no viewer Host forwarding, CloudFront sends the origin hostname as Host; see AWS origin request policy behavior.

CloudFront's managed cache policy applies Patch's origin cache lifetime within the policy's TTL bounds: mutable JSON uses public, max-age=0, s-maxage=300, must-revalidate, while content-addressed artifacts use a one-year immutable policy.

3. Create a purge-only IAM identity​

Create an IAM policy scoped to this one distribution and attach it to the IAM user or role the Patch server will use:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "cloudfront:CreateInvalidation",
"Resource": "arn:aws:cloudfront::YOUR_AWS_ACCOUNT_ID:distribution/YOUR_DISTRIBUTION_ID"
}
]
}

Create an access key for that identity if the host does not use an EC2/ECS IAM role. It must be the IAM user's permanent key (the ID starts with AKIA). Temporary credentials from AWS SSO, an assumed role, or CloudShell (IDs starting with ASIA) only work with a session token that nothing here can carry, and the installer rejects them. Put only this scoped key in .env.selfhost; do not copy administrator or general AWS credentials onto the Patch host.

4. Run the first install​

Use the same origin secret that is already configured on the distribution:

export CLOUDFRONT_ACCESS_KEY_ID=<purge_access_key_id>
export CLOUDFRONT_SECRET_ACCESS_KEY=<purge_secret_access_key>

cmpatch selfhost install ubuntu@203.0.113.7 \
--storage-origin-domain origin-storage-updates.example.com \
--cloudfront \
--cloudfront-distribution-id <distribution_id> \
--cloudfront-origin-verify-secret <generated-secret>

The guided install still asks for anything else it needs (the domains, the OAuth app, the admin email). scripts/selfhost/install.sh on the host takes the same flags, plus --cloudfront-access-key-id / --cloudfront-secret-access-key, for an on-server install.

Omit both access-key flags to use the AWS SDK default credential chain. The installer writes SELFHOST_STORAGE_ORIGIN_MODE=cdn-origin, brings up the additive viewer and protected-origin Caddy sites, and submits one synthetic invalidation to verify the distribution ID and IAM permission. That check uses one CloudFront invalidation path; --skip-cloudfront-check bypasses it. A rejected permission fails the install, while a throttled or busy distribution is only a warning, that answer does not disprove the credentials, so the install continues and you confirm a real purge afterwards.

To adopt CloudFront on a running bundled install, create the AWS resources first, then edit .env.selfhost instead of passing first-install flags:

SELFHOST_STORAGE_ORIGIN_MODE=cdn-origin
CODEMAGIC_PATCH_STORAGE_ORIGIN_DOMAIN=origin-storage-updates.example.com
DELIVERY_ADAPTER=cloudfront
CLOUDFRONT_DISTRIBUTION_ID=<distribution_id>
CLOUDFRONT_ACCESS_KEY_ID='<purge_access_key_id>'
CLOUDFRONT_SECRET_ACCESS_KEY='<purge_secret_access_key>'
CLOUDFRONT_ORIGIN_VERIFY_SECRET='<generated-secret>'

Also delete any existing MANIFEST_CACHE_CONTROL line. Installs created before CDN delivery pinned no-cache, must-revalidate there, and an explicit value wins over the adapter default, leaving it in place makes CloudFront revalidate every manifest request, so releases purge a cache that never fills. The installer warns when it sees this. Keep the line only if an origin-only policy is deliberate.

Rerun scripts/selfhost/install.sh with no delivery flags. Existing installs deliberately ignore those flags and keep .env.selfhost authoritative.

5. Verify before changing DNS​

Publish at least one release, then run all three checks against the same manifest path. The first proves CloudFront can reach the origin. The second connects to the distribution while keeping the final viewer hostname for TLS SNI and the HTTP Host header, proving the alternate domain name and viewer certificate work before DNS moves. The third proves a direct origin request without the secret is denied.

DEPLOYMENT_KEY=<deployment-key>
DISTRIBUTION_DOMAIN=d111111abcdef8.cloudfront.net
VIEWER_DOMAIN=storage-updates.example.com
ORIGIN_DOMAIN=origin-storage-updates.example.com
MANIFEST_PATH="/codemagic-patch/${DEPLOYMENT_KEY}/meta.json"

# Expect HTTP 200 and x-cache: Miss from cloudfront.
curl -fsSI "https://${DISTRIBUTION_DOMAIN}${MANIFEST_PATH}" \
| grep -Ei '^(HTTP/|x-cache:)'

# Expect HTTP 200 and x-cache from CloudFront again. TLS verification must pass.
# --connect-to tests the viewer hostname against CloudFront without changing DNS.
curl -fsSI \
--connect-to "${VIEWER_DOMAIN}:443:${DISTRIBUTION_DOMAIN}:443" \
"https://${VIEWER_DOMAIN}${MANIFEST_PATH}" \
| grep -Ei '^(HTTP/|x-cache:)'

# Expect exactly 403. Do not add the origin verification header.
curl -sS -o /dev/null -w '%{http_code}\n' \
"https://${ORIGIN_DOMAIN}${MANIFEST_PATH}"

Do not add -k or --insecure: a certificate error is a failed pre-cutover check. Do not cut over if any expectation fails.

6. Cut over and confirm a cache hit​

Change only the viewer/download DNS record to a CNAME targeting the distribution domain. Then request the manifest twice:

VIEWER_DOMAIN=storage-updates.example.com
MANIFEST_PATH="/codemagic-patch/${DEPLOYMENT_KEY}/meta.json"

curl -fsSI "https://${VIEWER_DOMAIN}${MANIFEST_PATH}" | grep -i '^x-cache:'
curl -fsSI "https://${VIEWER_DOMAIN}${MANIFEST_PATH}" | grep -i '^x-cache:'

Expect Miss from cloudfront followed by Hit from cloudfront (allow a short propagation delay). Restore the normal DNS TTL after verification.

After cutover, Caddy can no longer complete HTTP-01 renewal for the old viewer site because that DNS name now reaches CloudFront. Renewal warnings for that viewer certificate are expected and harmless; the origin certificate still renews through the unchanged origin DNS record. The custom header protects the origin hostname, not the host IP itself: a caller that reaches the IP and forges the old viewer Host header can still reach Silo, so retain normal host-level network controls.

Rotate the bundled origin secret​

The Caddy origin accepts a second, temporary secret so rotation has no 403 window while a CloudFront distribution update deploys. Outside a rotation CLOUDFRONT_ORIGIN_VERIFY_SECRET_PREVIOUS is simply absent:

  1. Generate a new secret. In .env.selfhost, set CLOUDFRONT_ORIGIN_VERIFY_SECRET to the new value and add CLOUDFRONT_ORIGIN_VERIFY_SECRET_PREVIOUS with the old value. Rerun scripts/selfhost/install.sh.
  2. Change the distribution's X-Codemagic-Patch-Origin-Verify custom header to the new value and wait until the distribution status is Deployed.
  3. Delete the CLOUDFRONT_ORIGIN_VERIFY_SECRET_PREVIOUS line and rerun the installer. The old value is no longer accepted.

Skipping step 3 leaves the old secret valid indefinitely.

External S3 or GCS storage​

First determine what PUBLIC_BASE_URL already is:

  • If it is a custom domain you control, keep it unchanged and point that name at CloudFront. Existing binaries and published manifests move together.
  • If it is a raw bucket URL, you need a new viewer hostname and a new PUBLIC_BASE_URL. Existing binaries keep downloading directly from the public bucket until a new native release, and existing manifests keep their old absolute artifact URLs until that deployment is released, promoted, rolled back, or cleared again.

Changing PUBLIC_BASE_URL does not rewrite existing manifests immediately. Keep the bucket public-read during this transition.

1. Create the certificate, distribution, and purge identity​

Request the viewer certificate in ACM us-east-1 and create the same distribution behavior and purge-only IAM policy described in the bundled track. Do not forward the viewer Host header. Select the origin shape for your backend:

BackendCloudFront origin
AWS S3Select the bucket as an S3 origin and use Origin access: Public initially. Do not configure it as a custom or S3 website origin.
Google Cloud StorageCustom HTTPS origin storage.googleapis.com, Origin path /<public-bucket-name>. GCS supports this path-style public endpoint.
S3-compatible (S3_ENDPOINT set)Custom HTTPS origin at the provider endpoint; add the provider-specific bucket path if its URL format requires one.

For GCS, the public bucket must allow anonymous object reads; Public Access Prevention cannot be enforced on that bucket. The internal GCS bucket is never an origin.

2. Install with external storage​

Point --public-base-url at the CloudFront viewer domain. Example for S3:

scripts/selfhost/install.sh \
--api-domain updates.example.com \
--email admin@example.com \
--github-oauth-client-id Iv1.xxxxxxxxxxxxxxxx \
--github-oauth-client-secret <github_client_secret> \
--storage-mode s3 \
--s3-bucket my-patch-public-bucket \
--s3-internal-bucket my-patch-internal-bucket \
--s3-region eu-west-1 \
--public-base-url https://cdn.example.com \
--cloudfront \
--cloudfront-distribution-id <distribution_id> \
--cloudfront-access-key-id <purge_access_key_id> \
--cloudfront-secret-access-key <purge_secret_access_key>

For GCS, replace the storage flags in that command with:

--storage-mode gcs \
--gcs-public-bucket my-patch-public \
--gcs-internal-bucket my-patch-internal \
--gcs-credentials-file /secure/path/service-account.json \
--public-base-url https://cdn.example.com

For an S3-compatible provider, keep --storage-mode s3 and add its endpoint and addressing mode:

--s3-bucket my-patch-bucket \
--s3-endpoint https://objects.provider.example \
--s3-force-path-style true \
--s3-access-key-id <storage_access_key_id> \
--s3-secret-access-key <storage_secret_access_key> \
--public-base-url https://cdn.example.com

The storage credentials and CloudFront purge credentials are separate. The former read/write objects; the latter can only invalidate one distribution.

External storage always uses SELFHOST_STORAGE_ORIGIN_MODE=direct. Do not set CODEMAGIC_PATCH_STORAGE_ORIGIN_DOMAIN or either origin verification secret; the installer rejects those bundled-only values.

Verify x-cache: Miss from cloudfront then Hit from cloudfront with the viewer URL and a real manifest path, as in the bundled track. There is no Caddy origin-bypass check for an external public bucket.

Optional S3 Origin Access Control​

Origin Access Control (OAC) can make an AWS S3 bucket private so only one CloudFront distribution reads it. Treat this as a later hardening step, not the default. It is safe only when PUBLIC_BASE_URL has always been the custom viewer domain. All three hazards matter:

  1. A standard OAC allow grants the CloudFront service principal s3:GetObject. The public-bucket policy's account-conditioned _internal/* deny does not cover that service principal, so a naive OAC policy can expose staged uploads through CloudFront. Replace it with deploy/selfhost/aws-s3-bucket-policy.cloudfront-oac.example.json, which explicitly denies CloudFront access to _internal/*.
  2. Making the bucket private breaks already-shipped binaries whose CodemagicPatchDownloadBaseUrl is the raw bucket URL.
  3. It also breaks every previously published manifest whose absolute artifact URLs still point at the raw bucket, including manifests fetched by new clients.

If either a binary or a manifest has ever carried a raw bucket URL, keep the bucket public until that population and all such manifests have aged out. AWS requires an S3 bucket origin (not a website endpoint) and a distribution-scoped bucket policy for OAC; see Restrict access to an Amazon S3 origin.

Purge behavior and cost​

Patch invalidates mutable manifest paths. Deployment/app deletion collapses all artifact paths below a deployment key into one trailing-wildcard path, so a large delete does not submit thousands of invalidation paths. CloudFront counts a wildcard as one path and provides the first 1,000 invalidation paths per AWS account each month at no charge; see AWS invalidation pricing.