Caching
Tenant-scoped, locale-qualified cache namespaces that ensure shop isolation and precise revalidation.
The cache is organized into two top-level namespaces: shopify for storefront reads (Shopify
Storefront/Admin API responses) and cms for content reads (Convex-backed CMS responses). Namespaces
are isolated — invalidating a CMS entry never touches Shopify-sourced data and vice versa.
A Shopify webhook can revalidate product data without evicting cached page content. A CMS publish can update authored blocks without touching prices or inventory. This separation is structural, not a convention — the tag schemas are declared in code, and the invalidation helpers and tag builders talk to the same schema object.
Tenant-scoped and locale-qualified
Within each namespace, every cached entity's tags include a shop key — this is the tenant-scoped cache invariant. Revalidating one tenant's data cannot touch another tenant's entries, even if both request the same logical resource.
Below tenant scope, entities are further keyed by locale. Locale is a qualifier under tenant, never above it:
cms.<tenantId>.<collection>.<key> // individual document
cms.<tenantId>.<collection> // collection-level sweep
cms.<tenantId> // whole-tenant CMS sweep
shopify.<shopId> // whole-tenant Shopify sweep
shopify.<shopId>.<domain> // domain-qualified tenant root
shopify.<shopId>.<domain>.product.<handle>
shopify.<shopId>.<domain>.collection.<handle>The cms.<tenantId> tag is the coarsest revalidation target for a single tenant's content.
Use revalidateTag('cms.<tenantId>') to flush all CMS-sourced data for one shop without
touching any other tenant or any Shopify-sourced data.
The @tagtree packages
Tag schemas are declared using @tagtree/core. The schema is a typed TypeScript object;
tag builders and invalidators are derived from it. There are no hand-rolled tag strings
anywhere in the codebase — every tag write and every revalidateTag call goes through a
builder generated from the same schema.
The @tagtree family covers three integration points:
| Package | Purpose |
|---|---|
@tagtree/core | Schema definition, builders, adapter interface |
@tagtree/next | Next.js storage adapter (revalidateTag / cacheTag) |
@tagtree/shopify | Shopify webhook → tag mapper (parseShopifyWebhook) |
The CMS side declares its tag schema once in @nordcom/commerce-cms's
cache-descriptor.ts — deliberately free of server-only so the Next.js read adapter
and the Convex revalidation pipeline derive identical tags from the same schema.
Where the cache helpers live
CMS namespace. Tag construction utilities and revalidation helpers for the CMS namespace
live in @nordcom/commerce-cms/cache. The exports cover tag schema access, per-tenant root
tags, and per-collection/per-document tag arrays. Use these helpers whenever you need to
construct or revalidate a CMS cache entry — never hand-format cms.* strings.
Shopify namespace. Shopify-namespace cache management is internal to the storefront's
src/cache.ts (built on the @tagtree/next adapter). The schema is the source of truth —
the /api/revalidate webhook handler, the entity loaders in _loaders.ts, and any future
storefront additions all import from it.
Invalidation flow
When Shopify state mutates, Shopify sends a webhook to /[domain]/api/revalidate. The
handler verifies X-Shopify-Hmac-SHA256, calls parseShopifyWebhook (from @tagtree/shopify)
to map the webhook topic to the affected cache tags, and calls revalidateTag for each tag.
It also evicts the per-tenant Apollo client pool entry so the InMemoryCache does not diverge
from Next.js's data cache.
CMS invalidation arrives from Convex. Publishing a document runs the deployment's
revalidation pipeline (packages/convex/convex/revalidate/), which computes the affected
tags from the shared schema and delivers signed events to the storefront's
/api/revalidate/convex route; the route verifies the CONVEX_REVALIDATE_SECRET HMAC,
then calls revalidateTag — a single content publish flushes exactly the per-tenant,
per-collection, per-handle tags the storefront read.
cacheTag only operates inside a 'use cache' boundary. Entity loaders use a
safeCacheTag wrapper that silently no-ops outside a 'use cache' scope — this keeps
generateStaticParams and other build-time callers from crashing. Never call cacheTag
directly from route code; use safeCacheTag instead.