Skip to content

Latest commit

 

History

History
160 lines (120 loc) · 9.03 KB

File metadata and controls

160 lines (120 loc) · 9.03 KB

CDN & Caching

Related: Publish Flow · Billing & Cost · Auth Tokens

Overview

The public API (/api/v1/**) is served via Firebase Hosting rewrites to the publicv1 Cloud Function. All CDN endpoints use a cache-version redirect pattern to allow Firebase Hosting's edge CDN to cache responses long-term while still supporting instant invalidation after publish.


The cv (Cache Version) Pattern

Every CDN endpoint follows this flow:

Client request (no cv or stale cv)
  → Function reads Storage cache.json metadata
  → 302 redirect to same URL + ?cv=<generation>    ← cached 60s by default (per-token override)
  → Client follows redirect
  → Function returns actual JSON                    ← cached 7 days (browser and shared CDN)

The cv value is the Firebase Storage generation number of the cache marker file:

  • Content: spaces/{spaceId}/contents/cache.json
  • Translation: spaces/{spaceId}/translations/cache.json

Whenever the marker is rewritten, its generation increments. The content marker is rewritten by the content.onwrite trigger on every content write (not only publish); the translation marker is rewritten on translation publish and on translation draft generation. See Publish Flow. All existing cv values become stale — the next request from any client triggers a redirect to the new cv.


Cache-Control TTLs

Scenario max-age s-maxage Who respects it
Redirect (cv missing or stale) 60s (default) 60s (default) CDN edge + browser
Content/Translation/Links response 7 days 7 days browser / CDN
Asset response (incl. /original, /download) 365 days 365 days browser / CDN
Asset 304 Not Modified 365 days 365 days browser / CDN
Asset canonical-size redirect (w/h above source) 365 days 365 days browser / CDN
400 — invalid asset transform param 1 hour 1 hour browser / CDN
404 — space not found 7 days 7 days browser / CDN
404 — content cache marker missing / content not found on disk (content routes only) 10 min 10 min browser / CDN
404 — translation/links cache marker missing, translation file missing, slug not found (no Cache-Control header sent)
404 — asset: Firestore doc missing (asset genuinely does not exist) 7 days 7 days browser / CDN
404 — asset: Firestore doc exists but Storage object missing (upload in progress) no-cache no-cache browser / CDN

Redirect TTL is a flat default (CACHE_REDIRECT_MAX_AGE_DEFAULT), not split by published/draft. It can be overridden per-token via the cacheTtl field on TokenV2 — see Auth Tokens — where cacheTtl: 0 disables caching entirely (Cache-Control: no-cache).

"404 responses" is not a single behavior — it depends on which lookup fails (see rows above); some 404s carry no Cache-Control header at all.

Constants are defined in functions/src/config.ts:

CACHE_MAX_AGE                   = DAY * 7       // 604800s
CACHE_SHARE_MAX_AGE             = DAY * 7       // 604800s
CACHE_ASSET_NOT_FOUND_MAX_AGE   = DAY * 7       // 604800s — 404 for an asset with no Firestore doc
CACHE_ASSET_MAX_AGE             = DAY * 365     // 31536000s
CACHE_REDIRECT_MAX_AGE_DEFAULT  = MINUTE        // 60s — default redirect TTL, overridable per-token via `cacheTtl`
CACHE_BAD_REQUEST_MAX_AGE       = HOUR          // 3600s — cached 400 for rejected asset params

Endpoints

Endpoint Auth cv source
GET /api/v1/spaces/:spaceId/translations/:locale Token (TRANSLATION_PUBLIC or DRAFT) translations/cache.json
GET /api/v1/spaces/:spaceId/links Token (CONTENT_PUBLIC, CONTENT_DRAFT or DEV_TOOLS) contents/cache.json
GET /api/v1/spaces/:spaceId/contents/slugs/*slug Token (CONTENT_PUBLIC or DRAFT) contents/cache.json
GET /api/v1/spaces/:spaceId/contents/:contentId Token (CONTENT_PUBLIC or DRAFT) contents/cache.json
GET /api/v1/spaces/:spaceId/assets/:assetId None N/A (no cv)
GET /api/v1/spaces/:spaceId/assets/:assetId/original None N/A (no cv)
GET /api/v1/spaces/:spaceId/assets/:assetId/download None N/A (no cv)

Asset transform bounds

One URL, one output. Every transform parameter is either honoured exactly as given or rejected with 400 — nothing is silently adjusted. That rule exists for the cache, not for tidiness: any value the function quietly rewrites means two URLs resolving to identical bytes, and the CDN keys on the URL it was handed, so each alias is a separate edge entry and a separate run of sharp.

Concretely:

  • ?w=/?h= above the source redirect to the size the source can produce. ?w=5000 on a 400 px asset returns 302 → ?w=400. No upscaling, and every oversized spelling collapses onto one canonical URL rather than returning identical bytes under many — the same trick cv uses. With both dimensions the box shrinks proportionally, so fit semantics survive. An asset with no recorded dimensions is served as requested, since the source size is unknown.
  • MAX_OUTPUT_DIMENSION (8192 px) rejects rather than clamps. ?w=9000 is a 400, checked before the redirect. The ceiling bounds the decoded bitmap sharp must hold — an 8192 px edge is ~200 MB of raw pixels — so raising it means revisiting memory/concurrency in functions/src/v1.ts too.
  • Only a canonical decimal integer is accepted for w/h/q. w=400.9, w=0400 and w=4e2 are all rejected, because each would render identically to w=400 under a different cache key.

The stored original stays reachable via the /original route. See Assets — Parameter Validation for the full matrix.

No format conversion happens implicitly, but quality is normalised. A request without ?f= keeps the stored format and still re-encodes a still raster at that format's default quality, so a bare URL is a rendition rather than the stored file. Animations, GIF, SVG and video are served as stored. Passing ?f=webp or ?f=avif cuts transfer size further, and is opt-in. See Assets — Output Format for the full matrix.

Responses carry an ETag derived from the object's md5Hash plus a suffix describing the effective encode — target format, quality, dimensions and fit. A matching If-None-Match returns 304 before any download or re-encode. The suffix is built from the resolved encode rather than the raw query so that two spellings producing identical bytes share an entry, while a rendition can never collide with the orig tag the passthrough routes use.


Response Compression

functions/src/v1.ts mounts compression() across the whole API, so every JSON response is gzipped when the client sends Accept-Encoding: gzip. Measured against the demo dataset, a translation locale file goes from ~490 KB to ~93 KB and the OpenAPI document from ~40 KB to ~5 KB — around 80% off the wire for the CDN endpoints overall.

The filter is the middleware's default, which decides purely from the response Content-Type via compressible. That is what keeps the asset route out of it without naming it: image/*, video/*, application/zip and application/pdf are already marked incompressible, so re-encoding an already-compressed JPEG never happens, and a new asset MIME type cannot accidentally opt in. image/svg+xml is the deliberate exception — it is text, so it does get compressed.

Two consequences worth knowing:

  • Compressed responses carry Vary: Accept-Encoding, so the CDN keys a separate edge entry per encoding. In practice that is two (gzip and identity), since every browser and every mainstream HTTP client advertises gzip.
  • The 1 KB default threshold means small bodies — most notably the HttpsError 404s — are sent uncompressed, where gzip framing would only add bytes.

Thundering Herd Problem

When content is published all consumers have a stale cv. Without a cached redirect, every consumer simultaneously invokes the Function → Storage → Firestore chain. The redirect cache (60s default, per-token tunable) limits the stampede to one wave per CDN edge node.

See Billing & Cost for impact analysis and Publish Flow for when invalidation happens.


Draft vs Published

Published Draft
version query param absent version=draft
Storage path {id}/{locale}.json {id}/draft/{locale}.json
Redirect TTL 60s default (overridable via token cacheTtl) 60s default (overridable via token cacheTtl)
Required permission *_PUBLIC or *_DRAFT *_DRAFT or DEV_TOOLS

Implementation Files

  • functions/src/v1/cdn.ts — all CDN route handlers
  • functions/src/config.ts — cache TTL constants
  • functions/src/v1/middleware/query-auth.middleware.ts — token auth per request
  • firebase.json — Hosting rewrite rules