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>
Concept

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:

PackagePurpose
@tagtree/coreSchema definition, builders, adapter interface
@tagtree/nextNext.js storage adapter (revalidateTag / cacheTag)
@tagtree/shopifyShopify 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.

Tip

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.

Continue exploring

On this page