How CMS Assets Works
CMS Assets is built on Cloudflare Workers. The primary flow is to proxy read-only CMS API requests through the same project edge domain that also serves assets:
https://your-project.cmsassets.com/~api/...for read-only CMS API callshttps://your-project.cmsassets.com/...for assets
That gives you one rollout model for hidden credentials, edge caching, and proxy-ready asset URLs in API responses. If you only need media delivery, the asset proxy can still be used independently.
API request flow
Here's what happens when your app sends a read-only CMS API request:
1. Your app requests:
https://your-project.cmsassets.com/~api/...
2. Cloudflare Worker receives the request
3. Worker extracts the project identifier from the hostname
4. Worker loads project config → apiOrigin, auth mode, cache TTL, previewBypassParam, transformApiUrls
5. Worker validates:
- Request method is GET
- API proxy is configured for the project
6. Worker normalizes the upstream URL:
- strips /~api
- removes proxy-only params like preview bypass and parsed toggle
- sorts query params for stable cache keys
7. Worker checks API cache:
- Cache HIT → return cached response immediately
- Cache MISS → fetch upstream CMS API
- Cache BYPASS → fetch upstream without reading or writing cache
8. On cache miss or bypass, worker injects the stored token server-side
9. If response parsing is enabled, worker rewrites CMS asset URLs in JSON responses to proxy URLs
10. Worker returns the response with cache headers like X-Cache and X-Parsed
Asset request flow
Here's what happens when a browser requests an asset:
1. Browser requests:
https://your-project.cmsassets.com/path/to/image.png
2. Cloudflare Worker receives the request
3. Worker extracts the project identifier from the hostname
4. Worker loads project config → origin, cache TTL, allowed types, etc.
5. Worker validates:
- Request method is GET or HEAD
- User-Agent is not a blocked bot
6. Worker checks edge cache:
- Cache HIT → return cached response immediately
- Cache MISS → fetch from origin
7. Worker fetches asset from origin CMS CDN
8. Worker checks the response Content-Type against allowedTypes (if configured)
9. Worker caches the response at the edge
10. Worker returns the asset to the browser
11. Usage is recorded asynchronously — no latency added to the response
Architecture overview
┌──────────┐ ┌─────────────────────┐ ┌──────────────┐
│ Browser │ ───→ │ Cloudflare Worker │ ───→ │ Origin CMS │
│ │ ←─── │ (Edge Proxy) │ ←─── │ CDN │
└──────────┘ └─────────────────────┘ └──────────────┘
│ │ │
│ │ │
┌─────┘ │ └──────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ Edge │ │ KV │ │ Nuxt API │
│ Cache │ │ (Config │ │ (Usage + │
└──────────┘ │ + Domain │ │ Billing) │
│ Lookup) │ └──────────────┘
└──────────┘ │
▼
┌──────────────┐
│ Cloudflare │
│ D1 Database │
└──────────────┘
- Cloudflare Worker — The edge proxy that handles all asset requests. Deployed globally.
- Edge Cache — Cloudflare's built-in cache. Assets and parsed API responses are cached with separate keys and configurable TTLs.
- Cloudflare KV — Stores project proxy configuration (
proxy:{slug}) and custom domain mappings (domain:{hostname}). This is the only data source the worker reads in the hot path. - Nuxt API — Server-side API for project management, billing (Stripe), and usage tracking. Usage data is stored in D1, not KV.
Edge caching
Assets are cached at the Cloudflare edge closest to the user. This means:
- First request — Cache miss. The worker fetches the asset from the origin CMS CDN, caches it, and returns it.
- Subsequent requests — Cache hit. The asset is served directly from edge cache with no origin request. Sub-10ms latency.
Cache key
The cache key is built from the full URL path with query parameters sorted alphabetically (so ?w=300&fm=webp and ?fm=webp&w=300 produce the same cache key).
If stripQueryParamsFor is configured for specific file extensions, query parameters are removed from the cache key for those types. This prevents CMS-generated query params from creating duplicate cache entries. When not configured, query parameters are always included.
Cache TTL
Default cache TTL is 2 days (172,800 seconds). You can customize this per project from 60 seconds up to 30 days.
API cache behavior
API responses use a separate cache flow from assets:
- API cache is
GETonly - default API cache TTL is 60 seconds
- preview requests can bypass cache entirely
- parsed and raw API responses use different cache keys
- cache refresh works by rotating cache version keys instead of purging the edge directly
Parsed vs raw API responses
When transformApiUrls is enabled, CMS Assets can rewrite asset URLs inside JSON API responses before they are cached.
- default behavior: parsed response with rewritten asset URLs
?parsed=false: raw proxied response without URL rewritingtransformApiUrls=false: disables rewriting at the project level even if?parsed=falseis omitted
This is useful when some consumers want proxy-ready URLs while others need the original upstream payload.
Preview bypass
When the configured previewBypassParam is present, CMS Assets skips the API cache and fetches directly from the upstream CMS API. This keeps editorial preview flows fresh without changing your default cache strategy.
Token injection and JSON URL rewriting
For API proxy, tokens are stored encrypted and injected server-side only when CMS Assets actually needs to fetch the upstream API.
- tokens are never returned to the browser
- tokens are decrypted only on cache miss or bypass
- auth mode can be
bearer,basic,token, ornone - transformed JSON responses can already contain
https://your-project.cmsassets.com/...asset URLs before they are cached
That means clients can receive proxy-ready CMS data without carrying CMS credentials or manually rewriting every asset URL in application code.
Bot protection
The edge worker blocks known scrapers and crawlers by default:
- ahrefs, semrush, uptimerobot
- python, curl, wget, libwww, node-fetch
You can customize the blocked bots regex per project in your project configuration.
Allowed file types
The allowedTypes setting lets you restrict which Content-Types the proxy will serve. When configured, responses with a Content-Type that doesn't match any allowed type return a 403 Asset type not allowed error.
If allowedTypes is not configured for a project, all content types are served. Common allowed types include svg, pdf, mp4, jpeg, jpg, png, webp, avif.
Video support
Video files (mp4, webm, mov, m4v) are handled with special consideration:
- Range requests are fully supported for seeking and streaming
- If your project has a separate
videoOriginconfigured, video requests route to that origin - Range requests bypass the edge cache to ensure correct partial content delivery
For Contentful, the video origin is automatically set to https://videos.ctfassets.net/{spaceId}.
CORS
CORS behavior depends on Cloudflare Workers defaults. The edge worker does not explicitly add CORS headers to responses. If your frontend requires specific CORS headers, verify that the origin server or Cloudflare's built-in handling provides them for your use case.
Usage tracking
Every request (both cache hits and cache misses) is tracked for usage. The worker sends a fire-and-forget POST to the usage ingest endpoint with:
- Number of requests (1)
- Number of bytes (from
Content-Lengthheader)
This data is aggregated per project per month and used for billing and quota enforcement. Usage tracking never blocks the response — if the ingest endpoint is unavailable, the asset is still served.
Quota enforcement
Usage data is tracked asynchronously and aggregated server-side by the Nuxt API. Quota enforcement is handled at the API/billing layer, not per-request in the edge worker. The worker does not perform quota checks before fetching from origin — it always serves the request and records usage via fire-and-forget ingest events.
Plan limits (requests, bandwidth) are enforced when usage is reconciled at the billing level. See Limits & Usage for plan details.