Skip to content
v0.3

Storefront data cache

Every cached read the reference storefront makes through the port becomes an entry of the Next.js Data Cache, keyed by its URL — product pages, listings, and also filtered listings and search. Those stay cached on purpose: sending every filter combination a crawler can produce to the kernel is exactly what the cache is there to avoid.

Since v0.3.3 that cache has a ceiling and lives in a volume, because without either it grew in the container’s writable layer until the disk — the database’s disk — was full.

Set on the storefront service:

VariableDefaultWhat it does
FORGE_DATA_CACHE_DIR.next/cache/forge-data-cacheThe directory. The template fixes it at /var/cache/forge-storefront, where the storefront_cache volume is mounted.
FORGE_DATA_CACHE_MAX_MB2048The ceiling, in MiB. The least recently used entries are evicted before each write that would cross it.
FORGE_DATA_CACHE_MIN_FREE_PERCENT5Below this share of free disk — or below 1 GiB free, whichever is larger — the cache stops writing and says so once in the log.

Empty means the default. The full list of variables is the configuration reference.

⚠️ FORGE_DATA_CACHE_MAX_MB bounds one storefront process, not the box. Each process keeps its own index of what it wrote and evicts against that. Run three storefronts — a second brand, a second theme, a replica — and the cache can reach three times the ceiling: 3 × 2 GiB with the defaults.

  • Size the disk for the sum, or lower the ceiling on each front.
  • Give each front its own volume. Two processes writing one directory each count only their own entries, so together they can exceed the ceiling either of them believes it is keeping.
  • The disk guard (FORGE_DATA_CACHE_MIN_FREE_PERCENT) is the backstop that does see the whole disk: every process stops writing when the filesystem is nearly full.

The checkout image does not install this handler. The reads it caches (product names and photos in the summary, payment badges) stay in Next’s own cache inside its container.

force-dynamic does not keep a page out of it

Section titled “force-dynamic does not keep a page out of it”

A route marked export const dynamic = 'force-dynamic' is rendered on every request, but its fetches can still be written to the Data Cache. Next applies “don’t cache” under force-dynamic only to a fetch that states no caching of its own; a fetch that declares next: { revalidate } — every cached read of @forgeco/storefront-kit does — is cached and counted against the ceiling like any other. To keep a read out of the cache, the fetch itself has to say so (cache: 'no-store').

When a write changes what the storefront shows (a product published, a price changed, a block placed), the kernel tells the storefront which cache tags to drop. Two variables on the kernel service turn that on:

  • FORGE_STOREFRONT_URL — the storefront origins, as the kernel reaches them. A comma-separated list: one origin, or several (http://storefront:3000,http://storefront-b:3000). Every listed front receives every purge.
  • FORGE_REVALIDATE_SECRET — one secret, the same on the kernel and on every listed front.

So every front that keeps a cache of its own must be in that list, must answer the purge, and must hold the same secret. A front left out serves what it cached until the entry’s window passes — and a cached “not found” never recovers on its own.

The admin has its own FORGE_STOREFRONT_URL (also a list) for the purge it makes right after an operator saves; the template sets it to the one storefront it ships, so add your other fronts there too.

What a front has to answer. POST <origin>/api/revalidate?tag=<tag>[&tag=<tag>…] with the header x-revalidate-secret: <FORGE_REVALIDATE_SECRET>; drop each tag (revalidateTag) and answer 200. The reference storefront has exactly that route; a storefront of your own needs it too.

How to check. The kernel says which case it is in, once, at boot:

storefront cache invalidation ON — 2 target(s): http://storefront:3000, http://storefront-b:3000
storefront cache invalidation OFF — set FORGE_STOREFRONT_URL … + FORGE_REVALIDATE_SECRET to enable …
⚠️ storefront cache invalidation MISCONFIGURED — FORGE_STOREFRONT_URL names 1 target(s) … but FORGE_REVALIDATE_SECRET is empty …
Terminal window
docker compose logs kernel | grep 'storefront cache invalidation'

A comma lost in an env file shows up here as one target fewer than you meant.

⚠️ Replicas behind one address are not all purged. A purge reaches the process that answers the request, so of N replicas behind one origin, one is purged and the rest live on their window (FORGE_REVALIDATE_SECONDS, one hour by default). List fronts that are different by name; replicas of one front need an edge purge.

  • On the first write of each process: [forge-data-cache] writing to <dir>: <n> MiB in <k> entries, ceiling <t> MiB.
  • Disk nearly full, once: [forge-data-cache] disk almost full (…): writes to <dir> are paused; …
  • When there is room again, once: [forge-data-cache] disk has room again: writes to <dir> resumed.
  • A directory it cannot open: [forge-data-cache] <dir> could not be opened (…); serving without a data cache. The storefront keeps serving, uncached.

How much it holds:

Terminal window
docker compose exec storefront du -sh /var/cache/forge-storefront

The cache is disposable: everything in it comes back from the kernel on the next visit. The first visit to each page is slower, and the kernel takes that wave of reads — prefer a quiet hour.

Terminal window
docker compose stop storefront
docker compose run --rm --no-deps --entrypoint sh storefront -c 'rm -rf /var/cache/forge-storefront/*'
docker compose start storefront
# or, with the storefront stopped, remove the whole volume (it comes back empty on the next `up`):
# docker volume rm <project>_storefront_cache

Do it after a long rollback: the volume survives a change of version, and the newer front serves what the older one cached.

The handler ships in the kit, as @forgeco/storefront-kit/data-cache-handler. A storefront cut from the reference already has the two pieces that wire it — keep them. A storefront written from scratch adds them:

// data-cache-handler.mjs, beside next.config.mjs — a file of the app, because Next imports it untranspiled
// at server start, and the app's directory is the one that reaches `next` inside a standalone image.
import { createRequire } from 'node:module';
import { withPagesCache } from '@forgeco/storefront-kit/data-cache-handler';
const fileSystemCache = createRequire(import.meta.url)(
'next/dist/server/lib/incremental-cache/file-system-cache.js',
);
export default withPagesCache(fileSystemCache.default ?? fileSystemCache);
next.config.mjs
import { fileURLToPath } from 'node:url';
export default {
cacheHandler: fileURLToPath(new URL('./data-cache-handler.mjs', import.meta.url)),
// …
};

Then point FORGE_DATA_CACHE_DIR at a volume of the container. Without the handler, your front keeps Next’s built-in cache: one file per entry, never deleted, in the container layer.

  • Volumes & backups — storefront_cache is disposable; never back it up.
  • Disk alarm — the kernel warns before the disk fills, whatever fills it.