Decision: CDN pointer pattern
Status: accepted
Context
Each published finder has a payload — a JSON blob of locations, design config, and settings — that the widget loads on every page view. This payload can be large (hundreds of locations × field data). Caching it at the CDN matters directly for embed load time.
The straightforward approach is a single URL per finder (cdn.../finders/{token}/config.json) with a short TTL so updates propagate quickly. The problem: a 5-minute TTL on a large payload means every CDN edge re-fetches the full payload every 5 minutes even when nothing changed. Cache hit rates are low for infrequently-visited finders.
The goal is: fast propagation after publish, high cache hit rate between publishes.
Decision
Two files per finder, with different TTLs:
| File | Key pattern | TTL | Contents |
|---|---|---|---|
| Pointer | finders/{token}/config.json | 5 minutes | { "hash": "sha1hexstring" } |
| Payload | finders/{token}/v/{hash}.json | 1 year (immutable) | Full finder JSON |
The widget loads the pointer first, reads the hash, then checks its localStorage cache. If the cached hash matches, the local copy is used directly (no CDN round-trip for the payload). If the hash differs — or there is no cache — the widget fetches the versioned payload URL. Since the payload URL contains the hash, it is content-addressed and can be cached forever.
When a finder is published, ExternalController::syncFinderDetails computes a new SHA-1 hash of the payload, uploads v/{hash}.json to R2, then overwrites config.json with the new hash. Old payload files are not deleted — they are orphaned but serve ongoing sessions that loaded the previous pointer before the update.
Propagation time = pointer TTL = 5 minutes. A visitor who loaded the old pointer will use the old payload until their pointer cache expires.
Alternatives considered:
- Single URL with
?v=hashquery string — CDN varies on query string but many edge configs don’t, leading to unpredictable behavior. - Origin-only serving — eliminates CDN benefits entirely; unacceptable latency for international embeds.
Consequences
Benefits:
- Payload is cached at CDN edges for up to a year between publishes. High-traffic finders pay one origin round-trip per 5-minute window.
- The widget can serve stale-but-valid payloads from localStorage even when the CDN is unreachable.
- Publish never breaks in-flight sessions: old versioned URLs remain valid indefinitely.
Costs:
- One extra round-trip (pointer fetch) on every cold load.
- R2 storage grows linearly with publish history. No cleanup job exists for orphaned payload files — this is an open maintenance gap.
- The 5-minute propagation window means a just-published update is not immediately live everywhere.
Related pages
- Architecture: Storage and CDN
- Architecture: Data flow — end user — full pointer → payload fetch chain
- Architecture: Data flow — builder — publish trigger and R2 upload
- External: CDN and pointer — widget-side fetch implementation