These docs are a work in progress and may not be fully up to date. Some pages may contain internal notes for our team.
Skip to Content

External widget — CDN and pointer pattern

The widget never fetches a finder payload from a single, stable URL. Instead it follows a two-step indirection through Cloudflare R2: a short-lived pointer file (config.json) names the current content hash, and a permanently-cached versioned payload (v/{hash}.json) holds the actual data. This split lets CDN edge nodes cache the heavy payload for a full year while only the tiny pointer — ~200 bytes — must revalidate on every publish. See storage-and-cdn.md for the R2 bucket layout and CDN configuration, and data-flow-end-user.md for the full request sequence.

R2 file types

Key patternCache-ControlPurpose
finders/{key}/config.jsonpublic, max-age=60 (set by backend on publish)Pointer — contains hash and metadata
finders/{key}/v/{hash}.jsonpublic, max-age=31536000, immutableVersioned payload — all locations + design

Widget bundle files (snippet.js, ext/2/snippet.js) use a separate 5-minute TTL and are managed by the deploy script — see deploy.md.

The pointer file contains at minimum a hash field. A sample pointer response:

{ "hash": "a3f9c1d...", "variant": 2, "published_at": "2026-04-27T10:00:00Z" }

The versioned payload is named by that hash, so a new publish produces a new key and the old one stays cached forever — no purge needed for payload files.

Fetch flow in api.js

The full CDN fetch chain lives in src/lib/api.js.

Pointer fetch (fetchFinderPointer)

api.js:40–50 — Fetches https://cdn.locationfinders.com/finders/{key}/config.json. Returns the parsed object if a hash field is present, or null on any error. Failures are swallowed silently so the caller can fall through to the origin API.

// api.js:43 const res = await fetch(`${CDN_BASE}/finders/${finderKey}/config.json`);

Payload fetch (fetchFinderPayload)

api.js:54–95 — This is the primary function called by App.svelte. It implements the full four-stage pipeline:

  1. Hash resolution — uses the knownHash argument if provided (passed from App.svelte via pointer?.hash), otherwise calls fetchFinderPointer to retrieve it (api.js:59).
  2. localStorage cache check — key is lf-cdn-v2:{finderKey}. If the stored object’s hash matches the current hash the cached data is returned immediately without a network request (api.js:62–67).
  3. Versioned CDN fetchhttps://cdn.locationfinders.com/finders/{key}/v/{hash}.json. On a 200 response the payload is written back to localStorage and returned (api.js:69–78).
  4. Origin API fallbackGET {VITE_API_APP_URL}/ext/finders/details/{key}. Used in local dev and as a safety net when CDN infrastructure is unavailable. Throws a descriptive error on non-OK status (api.js:82–94).
// api.js:59 — hash resolution const hash = knownHash ?? (await fetchFinderPointer(finderKey))?.hash ?? null; // api.js:63 — localStorage read const cached = JSON.parse(localStorage.getItem(cdnCacheKey(finderKey)) ?? 'null'); if (cached?.hash === hash && cached?.data) { return cached.data; } // api.js:70 — versioned CDN fetch const versionRes = await fetch(`${CDN_BASE}/finders/${finderKey}/v/${hash}.json`);

The localStorage key format (api.js:5–7):

function cdnCacheKey(token) { return `lf-cdn-v2:${token}`; }

App.svelte call site

App.svelte:245 — Inside the boot() async function called by onMount, the widget passes pointer?.hash as the second argument to skip a redundant pointer fetch when the snippet loader already pre-fetched the hash:

// App.svelte:245 const nextPayload = await fetchFinderPayload(finderKey, pointer?.hash ?? null);

pointer is a Svelte prop (App.svelte:21) injected by the runtime snippet with the pre-resolved hash. If it’s null, fetchFinderPayload fetches the pointer itself.

localStorage caching layer

The cache entry stores { hash, data } under the key lf-cdn-v2:{finderKey}. On the next page load:

  • If the pointer’s hash matches the stored hash, the payload is served from localStorage with zero network requests.
  • If they differ (publish happened), the versioned CDN file is fetched and the cache is overwritten.
  • try/catch guards both the read (api.js:62) and the write (api.js:74) so a full or unavailable localStorage does not break widget load.

There is no expiry timer on the cache entry — staleness is detected entirely via hash mismatch.

Cache invalidation on publish

When a finder is published from the dashboard, the backend’s ExternalController::syncFinderDetails (documented in data-flow-builder.md) does three things:

  1. Builds the payload JSON, sha1-hashes it.
  2. Uploads finders/{key}/v/{hash}.json to R2 with a 1-year immutable cache header.
  3. Overwrites finders/{key}/config.json with the new {hash} value (60 s TTL).

Cloudflare serves the pointer’s next request from origin within 60 seconds. Widget instances that already hold the old hash in localStorage will detect the mismatch on their next pointer fetch and refetch from CDN.

The deploy script (scripts/deploy-r2.mjs) purges only snippet.js and ext/2/snippet.js via the Cloudflare API — finder payload files are never purged; they simply accumulate as immutable objects in R2. See deploy.md.

CDN fallback to origin

If both the pointer fetch and versioned CDN fetch fail (CDN unavailable, R2 outage), fetchFinderPayload falls through to GET {API_BASE}/ext/finders/details/{key} (api.js:82). This endpoint is rate-limited and not designed for high traffic, so sustained CDN unavailability would degrade widget load performance. There is no circuit-breaker; every widget instance independently falls back.