Skip to content

feat(tanstack): opt-in CDN caching for /_serverFn via a URL segment marker - #513

Merged
JonasJesus42 merged 1 commit into
mainfrom
feat/cdn-cache-serverfn
Sep 1, 2026
Merged

feat(tanstack): opt-in CDN caching for /_serverFn via a URL segment marker#513
JonasJesus42 merged 1 commit into
mainfrom
feat/cdn-cache-serverfn

Conversation

@JonasJesus42

@JonasJesus42 JonasJesus42 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Split out of #510 at review request. Stacked on #512 (private-route hotfix), which is this branch's base —
merge that one first.

The problem

Anything sitting in front of the Worker keys on the raw URL. The Worker keys on a synthetic Request
carrying __seg/__cf_device/__cf_geo/__bot/__fetch/__abf (buildCacheKey). That mismatch is what
forces CDN-Cache-Control: no-store on every response, and why 100% of traffic invokes the Worker.

The fix, for /_serverFn

Invert the problem: instead of expecting the layer in front to reproduce the key, the client puts the
segment in the URL itself
(?__cseg=<device>.<buildHash>), which makes the two keys equivalent.

// worker-entry.ts
createDecoWorkerEntry(serverEntry, {
  cdnCacheControl: "serverfn-segment",
  buildSegment: (request) => ({ /* ... */ }),   // required
});
// src/start.ts
import { decoServerFnFetch } from "@decocms/tanstack/sdk/serverFnFetch";
export const startInstance = createStart(() => ({ serverFns: { fetch: decoServerFnFetch } }));

Covers SPA navigation and prefetch — the volume Speculation Rules generates. Default unchanged.

Why it's safe

The marker is a hint, not a source of truth. The Worker recomputes the segment from the request itself and
only relaxes no-store on an exact match. These all keep today's behaviour:

marker absent · diverging · forged · stale build · logged-in · region · sales channel ≠ 1 · unknown custom
SegmentKey field · bot UA · A/B cohort cookie · geo-varying key

The worst case is not caching, never a wrong response.

isBot and the A/B cookie are checked with the same helpers buildCacheKey uses — keying and releasing
off different predicates is exactly how the two silently diverge.

The build hash is part of the token because deploying does not purge whatever is caching the response, so the
URL has to change on its own when the bundle does.

HTML stays no-store: the initial navigation is a browser request, with no client hook to attach a marker
to.

Also here: match-profile was a silent trap

Its JSDoc promised safety "without buildSegment and deviceSpecificKeys: false", but deviceSpecificKeys
defaults to true — the condition is false on every site. Following the docs would have cross-served segments
with no warning. Now refused with a warning.

Provenance

Ported from a site-level implementation running in production, with two gaps closed: that version verified
only the device, leaving bots (which execute JS — Lighthouse, PageSpeed) and A/B cohorts able to poison a
shared entry. On the site, this replaced ~230 lines of shim with 6 lines plus a 9-line start.ts.

Validation

Tested end-to-end against a real built site, not just unit tests: linked packages, bun run build, worker
under wrangler dev, SPA navigation driven by Chrome.

case Cdn-Cache-Control
GET /_serverFn with a correct marker (desktop and mobile) public, max-age=1800
no marker no-store
marker for another device (forged) no-store
stale build no-store
bot UA no-store
login cookie no-store
A/B cookie no-store
HTML document no-store

The released value matches exactly what the site-level implementation produces in production.

2573 tests passing. Typecheck clean.

The 4 draft preview failures are pre-existing on main.

What was dropped

The Cache Rules generator that was in #510 is gone. Review pointed out — correctly — that sites served by
Workers have no cache in front of them, and that the path is Workers Cache (cache: { enabled: true } in
wrangler), not zone-level Cache Rules. That changes the design: per-worker instead of the shared zone,
config-as-code, the worker version already in the key (so a deploy invalidates on its own), and Vary
respected.

I'll redo the HTML side on top of that, in its own PR. No point merging a generator I already know is the
wrong approach.

🤖 Generated with Claude Code

…r in the URL

Anything in front of the Worker — Workers Cache, a CDN rule — keys on the raw
URL. The Worker keys on a synthetic Request carrying `__seg`/`__cf_device`/
`__cf_geo`/`__bot`/`__fetch`/`__abf` (`buildCacheKey`). That mismatch is why
every response ships `CDN-Cache-Control: no-store`, and why 100% of traffic
comes back uncached.

This inverts the problem for `/_serverFn` (SPA navigation and prefetch data):
the client appends the segment to the URL itself, so the two keys become
equivalent and releasing the cache is safe.

    // worker-entry.ts
    createDecoWorkerEntry(serverEntry, {
      cdnCacheControl: "serverfn-segment",
      buildSegment: (request) => ({ /* ... */ }),   // required
    });

    // src/start.ts
    import { decoServerFnFetch } from "@decocms/tanstack/sdk/serverFnFetch";
    export const startInstance = createStart(() => ({
      serverFns: { fetch: decoServerFnFetch },
    }));

The default is unchanged, and the marker is a HINT, not a source of truth: the
Worker recomputes the segment from the request and only relaxes `no-store` on
an exact match. Absent, diverging, forged or stale-build markers, logged-in /
region / sales-channel requests, an unknown custom segment field, a bot UA, an
A/B cohort cookie, or geo-varying keys all keep today's behaviour. The worst
case is not caching, never a wrong response.

The build hash is part of the token because deploying does not purge whatever
caches the response, so the URL has to change on its own when the bundle does.

`isBot` and the A/B cookie are checked with the same helpers `buildCacheKey`
uses — keying and releasing off different predicates is exactly how the two
silently diverge.

HTML documents stay `no-store`: the initial navigation is a browser request
with no client hook to attach a marker to.

Also refuses `cdnCacheControl: "match-profile"` when the cache key is
segmented. Its JSDoc promised safety "without buildSegment and
deviceSpecificKeys: false", but `deviceSpecificKeys` defaults to true, so the
condition is false on every site — following the docs would have silently
cross-served segments.

Ported from a site-level implementation running in production, with two gaps
closed: that version verified only the device, leaving bots (which execute JS —
Lighthouse, PageSpeed) and A/B cohorts able to poison a shared entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@JonasJesus42
JonasJesus42 requested a review from a team August 27, 2026 23:19
@JonasJesus42
JonasJesus42 changed the base branch from main to fix/cache-private-routes-hardening August 28, 2026 14:35
@JonasJesus42 JonasJesus42 changed the title feat(tanstack): cache de CDN para /_serverFn via marcador de segmento na URL feat(tanstack): opt-in CDN caching for /_serverFn via a URL segment marker Aug 28, 2026
@JonasJesus42
JonasJesus42 deleted the branch main September 1, 2026 13:56
@JonasJesus42 JonasJesus42 reopened this Sep 1, 2026
@JonasJesus42
JonasJesus42 changed the base branch from fix/cache-private-routes-hardening to main September 1, 2026 13:57
@JonasJesus42
JonasJesus42 merged commit 0e8b9f9 into main Sep 1, 2026
1 check passed
@JonasJesus42
JonasJesus42 deleted the feat/cdn-cache-serverfn branch September 1, 2026 13:57
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 7.56.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant