Repository navigation
perf(sites): optional, vendor-neutral edge cache with tag purges (#73) - #144
Merged
Merged
Conversation
clbotdev
force-pushed
the
perf/sites-edge-cache
branch
from
October 25, 2026 07:48
11dd1a8 to
a50b225
Compare
Any HTTP cache or CDN can now sit in front of apps/sites using standard
headers. It stays optional: FrontLit's own cache does the real work.
- EDGE_CACHE_MODE (read at runtime): none (default) keeps Next's headers;
ttl sets CDN-Cache-Control / Surrogate-Control s-maxage=EDGE_CACHE_TTL;
purge keeps pages until purged and adds Surrogate-Key / Cache-Tag with
the Layer 1 tag names, computed from the path. Browsers always get
max-age=0. Ticket threads, previews and the chat embed are private in
every mode.
- A request whose rsc header and _rsc parameter disagree is never
cacheable. Behind a cache that ignores Vary, Next's redirect for such a
request was stored at the page's URL, and one crafted request sent
every visitor to an RSC payload. skipProxyUrlNormalize gives the proxy
both signals.
- API: an EdgePurgeAdapter interface and a generic http adapter (signed
POST { teamId, tags }). Both outboxes deliver sites → edge →
edge-repeat (ten seconds later) and retry failures; a docs purge reports
deliveredAt once every cache acknowledged.
- deploy/edge-cache: Varnish (xkey) VCL, a purge receiver and a compose
override for the self-hosted recipe. It isn't part of the default stack.
- Optional ASSET_PREFIX for /_next/static.
compose passes EDGE_CACHE_MODE / EDGE_CACHE_TTL to frontlit-sites and ASSET_PREFIX as a build arg; .env.example documents them with the purge settings and the Varnish recipe's EDGE_PORT / VARNISH_SIZE.
clbotdev
force-pushed
the
perf/sites-edge-cache
branch
from
October 25, 2026 08:15
a50b225 to
39893aa
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #75. Closes #73. Stacked on #143: review and merge that first, then retarget this PR to
main.Any HTTP cache or CDN can now sit in front of
apps/sitesusing standard headers. It stays optional. Correct delivery never depends on it, because Layer 1 (#143) does the real caching. Design:docs/site-delivery.md§5, updated, with measurements in §9.1. Recipe:deploy/edge-cache/README.md.Results (Varnish 7.6 recipe in front of the #143 origin, same sample tenant)
purgemodettlmode (EDGE_CACHE_TTL=5), no purge integrationEdge purges are scoped by tag, measured at the edge (
X-Edge-Cache):/blog./changelog./docs/*.The second purge lands about 10 s after the event.
Client navigation / RSC
Vary(VCL variant withunset beresp.http.Vary): the same two rounds pass.?_rsc=…payloads are cached astext/x-component, separate from the HTML.rsc: 1header and no_rscgot Next's 307 redirect stored at the page's own URL. Every later visitor was redirected to an RSC URL. Responses whoserscheader and_rscparameter disagree are nowprivate, no-storein every mode. Next strips both signals before the proxy by default, soskipProxyUrlNormalizeis on (App Router only, no/_next/data). Retested: the poisoning attempt is refused and visitors get HTML.Changes
EDGE_CACHE_MODE(runtime), set inproxy.tsvialib/edge-cache.ts:none(default): Next's own headers.ttl:CDN-Cache-Control/Surrogate-Controls-maxage=$EDGE_CACHE_TTL, stale-while-revalidate=1d, stale-if-error=7d.purge: one-years-maxageplusSurrogate-KeyandCache-Tag, using the Layer 1 tag names derived from the path.public, max-age=0, must-revalidate./embed/chatareprivate, no-storein every mode.EdgePurgeAdapterinterface with a generichttpadapter (signedPOST { teamId, tags }).sites → edge → edge-repeat.edge-repeatis held until 10 s after the event, and failures retry with backoff.deliveredAtonly once every cache has acknowledged.deploy/edge-cache: Varnish VCL usingxkey,purge-receiver.mjs, and a compose override. The VCL never caches 5xx, private responses or cookies, keys on host + full URL, and keeps 7 days of grace for stale-on-error. Not part of the default stack.ASSET_PREFIXfor/_next/static(build time).Testing
edge-purge.test.ts: signing, 10 s repeat, failure retry, misconfiguration). Sites: 66 tests pass (newedge-cache.test.ts: tags per path, headers per mode, private routes, mixed RSC requests).private, no-store).