Cache is not coordination
The first rule is a boundary, not a feature. Voyant does not use cache as the source of truth for locks, leader election, strongly consistent counters, transactional state, or concurrency control. Those belong in the database or a runtime coordination layer. A practical corollary: every cache usage must assume misses happen, entries expire, entries get invalidated, and backends differ in propagation timing. The code path behind the cache must still be correct when the cache is empty.A cache miss should be a performance event, not a product bug. The live database path has to remain correct on every miss, on every route, on every backend.
Backends through one small contract
Voyant exposes a narrow shared cache contract so the same code can run against different backends:| Backend | Good for | Default in |
|---|---|---|
| Cloudflare KV | Read-heavy, staleness-tolerant reference caching: storefront settings, market and config lookups, localization bundles, cacheable query results. | Cloudflare-first templates. |
| Redis | Fresher invalidation visibility, richer cache patterns, stronger write/read freshness expectations, Node and container runtimes. | Node and container deployments that need its semantics. |
| In-memory | Local development only. | Local and dev. |
What is cacheable
Cache read-heavy derived data, never primary mutable business state. Good candidates:- Public settings and deploy configuration.
- Catalog-derived reference payloads.
- Expensive but repeatable read models.
- Derived storefront and public query results.
- Locale-aware rendered fragments.
The public route cache policy
The Worker in front of/v1/public/* has a response cache, and it is conservative by design. It only stores a GET /v1/public/* response when both conditions hold:
- The route explicitly emits
Cache-Control: public, s-maxage=.... - The response has no
Set-Cookie.
env.CACHE KV binding in Cloudflare runtimes where caches.default is unavailable. Nothing is cached by accident: a route is cached only if it opts in with the right header and carries no cookie.
Policy classes
Every public route is classified into one cache policy before it is built.| Policy class | Meaning |
|---|---|
shared-response-cache | Non-personalized, stale-tolerant public GET. Emits Cache-Control: public, s-maxage=..., stale-while-revalidate=.... |
kv-read-model | Public read backed by a KV or document read model. Still emits shared response-cache headers unless it varies by request headers without a matching Vary. |
private-no-store | Bearer-like IDs, customer, session, payment, proposal, contract instance, signature, or PII, or any response with Set-Cookie. Emits Cache-Control: private, no-store. |
live-by-correctness | Volatile price quote, hold, booking, payment mutation, eligibility, or write flow where stale data changes correctness. Never cached. |
index-backed | Search and index reads. GET searches can use shared response cache when non-personalized; POST searches are not cached because the cache key is URL-only. |
How common routes are classified
- Public product browse and detail, categories, tags, destinations:
shared-response-cacheandkv-read-model. - Storefront departure browse and detail, itineraries, availability snapshots, offer reads:
shared-response-cache. Checkout and customer mutations stay live. - Cruise, charter, and commerce public GETs, transport requirements, published legal reference content:
shared-response-cache. Their quote POST routes arelive-by-correctness. - Operator public profile, public settings, payment-link config:
shared-response-cache. Payment sessions stay private. - Proposals, finance customer portal, document delivery, contract instances and signatures, payment sessions:
private-no-store. - Catalog POST search:
index-backed, and not response-cached today because the request body is outside the URL cache key.
Storefront settings are deliberately not shared-cached when deployments resolve variants from request headers such as
x-storefront. Likewise, catalog sourced content is kv-read-model rather than shared response cache while locale can fall back to Accept-Language. The rule below explains why.Cache headers and invalidation
The header you emit is the contract that decides caching. Three headers matter.s-maxageis what the shared response cache reads. Without it, a public GET is not cached at all.stale-while-revalidatelets the edge serve a slightly stale response while it refreshes in the background, which is ideal for browse and reference data.private, no-storeis the explicit opt-out for anything customer, session, or payment scoped.
s-maxage, tolerate staleness for that window, and let entries expire. This is why only stale-tolerant data is shared-cached in the first place. There is no general cache-purge guarantee on the public response cache, so do not cache something that must change the instant the underlying data changes; classify it live-by-correctness or private-no-store instead.
The mechanical guardrail for all of this is pnpm verify:public-cache-policy, which checks that public routes match their declared cache class.
Guidance for storefront performance
The storefront is the customer-facing booking experience, and it is read-heavy: discovery, product detail, departures, itineraries, availability snapshots. That profile is exactly what shared response caching is built for.Cache the browse and detail surface
Product, departure, itinerary, and reference GETs are
shared-response-cache. Emit public, s-maxage=... with stale-while-revalidate so the edge serves fast and refreshes in the background.Keep checkout and quotes live
Price quotes, holds, bookings, payments, and eligibility are
live-by-correctness. They re-verify against the live database on every request. Browse data can be stale; the money path cannot.Normalize variants into the cache key
If content varies by locale, market, or storefront variant, put the variant in the URL or a normalized cache key (or emit
Vary). Never let a header silently fork the response under one cached key.Read models through KV where it helps
Product detail and similar reads can read through
env.CACHE as a kv-read-model while still emitting shared response-cache headers, layering an application cache under the response cache.Next steps
API routes
The public surface and how routes emit their cache headers.
Storefront
The customer-facing surface that benefits most from caching.
Services
The live path behind every cache, correct on every miss.
Architecture
The per-organization runtime where the cache backend lives.