Valcraven Docs
Features

Response Caching

Two independent caching layers — Cloudflare Workers Cache for public HTTP responses (with a hard credential bypass) and OpenNext's R2/D1 incremental cache for Next.js ISR/data.

Response Caching

Valcraven ships two independent caching layers. Understand which is which before you touch either — they solve different problems.

LayerWhat it cachesWhere it's configured
Cloudflare Workers CacheWhole HTTP responses, in front of the Workerwrangler.toml [cache] + lib/cache-policy.ts (applied in worker.js)
OpenNext incremental cacheNext.js's own ISR pages + fetch data cache + revalidateTag/revalidatePathopen-next.config.ts (R2 + D1 bindings)

Workers Cache is about caching the finished HTTP response; the incremental cache is about how Next.js renders pages. They are orthogonal.

1. Workers Cache (public responses)

Cloudflare Workers Cache is a tiered cache that sits in front of the Worker's fetch entrypoint. Two facts drive the entire design:

  • On a cache HIT the Worker does not run — the cached response is served directly.
  • Caching is decided entirely by the response Cache-Control. Cloudflare auto-bypasses the Authorization request header and Set-Cookie responses, but it does not bypass the Cookie request header.

Because Valcraven authenticates with a Better Auth session cookie, a naive "just enable caching" would risk serving one user's rendered page to another. All of the safety logic therefore lives in one small, auditable file — lib/cache-policy.ts — which worker.js calls on the way out of every request.

The load-bearing invariant

applyCachePolicy() runs a set of guards. The first is the one that makes caching safe:

GUARD 1: any request carrying a credential gets Cache-Control: private, no-store.

"Credential" here means a Better Auth session cookie, an x-api-key header, or an Authorization header (requestCarriesCredentials()). The Worker only runs on a cache MISS — the single place an entry is ever written — so this guarantees every response that reaches the cache store was generated for an anonymous request, which contains no per-user data. A HIT served to anyone therefore cannot leak another user's content, even under heuristic caching.

On top of that guard:

  • Only an explicit allowlist of anonymous, auth-invariant public paths (/, /blog, /changelog, /docs, /privacy-policy, /terms, plus their sub-paths) is marked public, s-maxage=300, stale-while-revalidate=86400 with a Cache-Tag.
  • A response that sets a cookie is never upgraded to public.
  • A header-less response defaults to no-store, so nothing is cached by accident.

Making a new route cacheable

Add its path to the allowlist in lib/cache-policy.ts (PUBLIC_EXACT / PUBLIC_PREFIXES) — but only if its rendered output is identical for every user (no cookies() or session read in the page). If in doubt, don't.

The security boundary is requestHasSessionCookie. If you ever change Better Auth's advanced.cookiePrefix, update the SESSION_COOKIE_RE regex in lib/cache-policy.ts to match, or authenticated responses could start being cached.

Purging

Cached public pages carry Cache-Tags — every page gets page plus a section tag (marketing, blog, changelog, or docs). Purge a section after a content change or on deploy via the CRON_SECRET-authed internal route, which calls purgeCache() in lib/cache-purge.tsctx.cache.purge():

curl -X POST https://your-app.example.com/api/internal/cache-purge \
  -H "Authorization: Bearer $CRON_SECRET" \
  -d '{"tags":["blog"]}'          # or {"everything": true}

Purging is best-effort and never throws — the database is the source of truth, so a stale entry self-heals on the next TTL even if a purge is missed.

Turning it off, and cost

Set [cache] enabled = false in wrangler.toml to disable the whole layer.

Cost note: enabling Workers Cache bills static-asset requests and worker-to-worker (service binding) calls at standard request rates, and cache MISS/BYPASS still bill CPU. This is called out in wrangler.toml.

Workers Cache is gated on a current compatibility_date, so wrangler.toml is pinned to a recent date (2026-07-06) to enable it.

2. OpenNext incremental cache (Next ISR/data)

open-next.config.ts backs Next.js's own cache with durable storage instead of the default in-memory cache (which lost everything on cold start):

import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
import d1NextTagCache from "@opennextjs/cloudflare/overrides/tag-cache/d1-next-tag-cache";

export default defineCloudflareConfig({
  incrementalCache: r2IncrementalCache,
  tagCache: d1NextTagCache,
  queue: "direct",
});
  • incrementalCache: R2 — ISR pages and the fetch data cache persist to the storage bucket under the incremental-cache/ key prefix (isolated from user uploads). Binding: NEXT_INC_CACHE_R2_BUCKET.
  • tagCache: D1 — backs revalidateTag() / revalidatePath(). Binding: NEXT_TAG_CACHE_D1 (the same D1 instance as your app; opennextjs-cloudflare deploy seeds its own tag table).
  • queue: "direct" — revalidation runs inline, no Durable Object queue. This is the simplest correct default for the template.

Both bindings point at the app's existing R2 bucket and D1 database — they're declared in wrangler.toml, so there's nothing extra to provision.

On this page