Core
Catalog
Catalog Caching

Catalog Caching

How the public storefront is cached between seller edits, so traffic spikes don't hammer Postgres and the IP rate limit doesn't lock out real shoppers.

1. Overview

Public catalog pages (/catalog/[slug], /catalog/[slug]/p/[productSlug], /catalog/[slug]/cart, /catalog/[slug]/checkout) are the only routes that anonymous traffic hits. The backend rate-limits /api/public/catalog/* to 60 req/min per IP, which is fine for browsers but fragile for bots, crawlers, and viral moments. To protect both Postgres and that rate limit, catalog reads are served from Next.js Data Cache (tagged fetch) on the frontend. There is no backend cache — every cache MISS goes straight to Prisma. Invalidation is push-based: product/store/catalog writes in the backend fire revalidateCatalog(slug), which calls a signed route on the frontend that runs revalidateTag("catalog-<slug>").

2. Architecture

The only cache layer that matters is the Next.js fetch cache keyed by URL + tag. The backend itself is stateless for public reads (the in-memory LRUCache in backend/lib/eziseller-parser/cache/catalog-cache.ts is for the WhatsApp/IG AI parser's tenant catalog index — not the public storefront).

3. Data model

Caching is not persisted — no DB tables involved. The tag catalog-<slug> is derived from Store.slug. See schema.prisma for Store, Product, CatalogSettings.

4. Key flows

4.1 Cache MISS (cold read)

4.2 Cache HIT + seller edit + invalidation

5. Lifecycle

There is no TTL on the tagged fetch — entries live until invalidated or the Next.js server restarts/redeploys.

6. Key files

Frontend (cache consumers — all tag reads with catalog-<slug>):

Frontend (invalidation endpoint):

Backend (cache miss handler):

Backend (invalidation producer):

Client cart state (separate, not cached server-side):

7. Env vars & config

VarRequiredPurposeWhat breaks
REVALIDATION_SECRETyes (prod)Shared HMAC-style token between Express and Next.js /api/revalidateIf unset on BE: edits never invalidate (stale catalog). If mismatched: frontend returns 401, still stale. Logged as warning, never throws.
FRONTEND_URLyes (prod)Where the backend POSTs the revalidation requestIf unset: revalidateCatalog logs a warning and no-ops
NEXT_PUBLIC_API_URLyesWhere the Next.js server fetches catalog data fromPage returns notFound() on failure

There is no TTL / revalidate / cache option passed to fetch() — the implicit Next.js behaviour is full caching until tag invalidation.

8. Gotchas & troubleshooting

  • Seller edits don't show up on the storefront → Cause: REVALIDATION_SECRET or FRONTEND_URL missing on the backend, or mismatched across services. Fix: check backend logs for [revalidateCatalog] FRONTEND_URL or REVALIDATION_SECRET not set or HTTP 401. Both services must share the exact same secret.
  • Revalidation is fire-and-forget on purposerevalidate-frontend.ts:L14-L19. The function returns void, not Promise<void>, so callers cannot await it. A slow or down frontend must never fail a product save. Cost: if the POST fails, the cache stays stale until the next successful write or deploy.
  • No backend cache for public reads → Every MISS hits Postgres with 5–6 Prisma queries (store, settings, products+variants, categories). The LRUCache in backend/lib/eziseller-parser/cache/catalog-cache.ts looks relevant but is only used by the WhatsApp/Instagram AI message parser pipeline, not the HTTP route.
  • Tag regex is strictrevalidate/route.ts:L24 only accepts ^catalog-[a-z0-9-]+$. Slugs with uppercase or underscores will silently fail to invalidate (400 response, logged as error in backend).
  • Store settings include JSON blobsorderSettings, paymentSettings, shippingSettings are read at cache-miss time (public-catalog.ts:L58-L66) and returned in checkoutConfig. Changes to these must call revalidateCatalog (store.ts does; check new settings routes too).
  • Rate limit is on the BE, not cache → A cache HIT bypasses Express entirely, so the 60 req/min/IP limit only applies to MISSes. Don't rely on it for hot catalogs.
  • No stale-while-revalidate → When a tag is busted, the next request is a full MISS (not SWR). First shopper after an edit pays the DB round-trip.

9. Extension points

  • Add a new write path that changes storefront output → must call revalidateCatalog({slug}) or revalidateCatalog({userId}) after the DB write. Pattern: see bulk product update at products.ts:L1319.
  • Add a new public storefront page → fetch with { next: { tags: [\catalog-${slug}`] } }` so it participates in the same invalidation.
  • Add a backend in-memory cache (e.g. for the DB query itself) → would sit inside GET /:slug in public-catalog.ts; needs an explicit invalidation hook wired into the same places that call revalidateCatalog. Keep LRU size small — one entry per store slug.
  • Migrate to Redis → replace the in-memory Set for refresh tokens first (auth.ts:68), then consider a shared cache layer. Today nothing in the request path uses Redis.

10. Related docs