Architecture
This page explains how a storefront is composed on top of Centra. It is written for solution architects and tech leads planning a build. It is intentionally technology-agnostic — the patterns apply whether your storefront is a website, a native mobile application, or an in-store / clienteling device.
Commerce backend
Centra owns commerce: the segmented catalog, segmented pricing, real-time segmented inventory, the cart, checkout, order, customer, and the session that ties them together. Centra does not own the experience around commerce — content, search, analytics, experimentation, personalization, reviews, feature flags. Those live in specialized systems. A working storefront integrates Centra with those systems and presents them as one experience.
The split between commerce and experience leaves room for highly bespoke client experiences. A luxury footwear brand can layer a made-to-order configurator on top of Centra's cart and checkout, letting clients design fully bespoke footwear without Centra needing to know what made-to-order means. A football club selling merch can integrate Centra into a broader fan experience, letting fans add a stadium seat to a scarf order in a single checkout.
Throughout this page, "storefront" means any client surface that presents commerce to a shopper. Web is the most common shape and most of the examples are web-flavored, but the architecture applies to native apps and in-store devices with only minor adjustments — covered toward the end.
Best-of-breed commerce
In a composable architecture each capability is a separate system, chosen on its own merits, integrated through APIs. Something has to assemble those systems into a single shopper experience. That something is the composition layer.
On the web the composition layer is most often a server-side rendering application — sometimes called a "backend for frontend" (BFF) — that runs on the edge or in a regional cloud, talks to Centra and to every other source, composes a cacheable page shell, and hands it off to the CDN. Per-shopper, dynamic data follows a different path: the client fetches it directly from the relevant origin, bypassing the composition layer. The full pattern is in The page shell pattern. On a native app the same composition role can be played by a thin server-side BFF, by an on-device composer, or by a managed mobile backend. The shape varies; the responsibility is the same.
A typical Centra storefront pulls from these categories:
- Centra — segmented catalog and pricing, real-time inventory availability, cart, checkout, order, customer, session, vouchers, campaigns.
- CMS — editorial pages and reusable content modules.
- Product discovery — search, merchandising, and recommendations. Often delivered by a single vendor; the architecture is the same regardless of how they are packaged.
- Reviews and other user-generated content — ratings, written reviews, Q&A.
- Analytics and tracking — behavioral analytics, web/app analytics, marketing tags.
- Experimentation / A-B testing — variant assignment and outcome measurement.
- Feature flags — gradual rollout, kill switches, environment-specific configuration.
The composition layer is also where the architectural decisions that matter most live: which system answers which question, how the four segmentation parameters propagate to each downstream call, and how caching is layered to keep the experience fast and the bill reasonable.
What Centra uniquely provides
The Storefront API is the only Centra API that exposes real-time segmented availability. Centra's Integration API is designed for backend integrations. Only the Storefront API is designed for the DTC shopper-facing path: it returns the catalog filtered to what a particular shopper can see and buy, at the price they will pay, with the inventory currently available to them.
For any shopper-facing path, use the Storefront API. The Integration API is for system-to-system integrations only.
That segmented, real-time output is the contract between Centra and the rest of the stack. Every other system has to align to it, because the moment the storefront promises something Centra cannot fulfill — a price the pricelist doesn't recognize, a product the market does not sell, a country the order cannot ship to — the experience breaks and conversions follow.
Segmentation: Market, Ship-to country, Pricelist, Language
Centra segments the catalog along four parameters. Understanding all four is essential because every downstream system needs to align to the same four.
Market (controlled by system logic)
Market is Centra's main segmentation mechanism. A Market is a segment, not a country. A Market controls which products are visible and buyable, which campaigns and vouchers apply, and which shipping options are offered. Markets are commonly used for geographic regions, but they are equally used for customer segments — VIP customers, employees, wholesale, B2B — that have nothing to do with geography. A brand that wants to segment offering based on customer tier (gold member, silver member, not member), continent (North America, Europe, ...) and channel (mobile app vs. web) will have markets created by the relevant combinations of those. A highly segmented site can run hundreds of active Markets.
Country (selected by shopper)
Ship-to country is the country an order will physically ship to. Within the active Market, warehouses are further narrowed per country, so it influences available inventory. It also controls taxes and how they are displayed, available payment methods, available shipping options, and size-chart localization, and it drives which country-specific legal disclosures need to show — return policies, shipping terms — usually authored in the CMS and selected based on the active country. It is independent of Market: a single Market can ship to many countries.
Country-State is an optional refinement that applies where Centra is configured with states for a country. It is model-driven — any country can have states, but US is the canonical case. State is set via the same setCountryState mutation that sets country (state is optional on the mutation), and remains null on the session until a state is chosen. State refines several things further within the country:
- Warehouse-level inventory. Inside a Country, the warehouse list can narrow again per state. An item can be in stock in New York and simultaneously out of stock in Hawaii. This is the architectural consequence that matters most: anything that promises availability — PDPs, PLPs, recommendation rails — must respect state, or the storefront promises stock it cannot ship.
- Tax rates (US state sales tax).
- Available shipping options.
- Available payment methods in some configurations.
- State-specific legal disclosures in CMS-authored copy.
State can be unknown for part of a shopper's session. How it becomes known is a product decision and can change over time — geo-IP can resolve to state with reasonable accuracy in some regions, a landing-page picker can ask up front, an account profile can carry it, or it can fall out of a checkout address as a last resort. The architecture has to work both with and without state, and switch cleanly when state arrives at any point in the session.
Pricelist (controlled by system logic)
Pricelist is the list of prices in a single currency. It controls displayed prices, available campaigns and vouchers that depend on price, and currency.
Language (selected by shopper)
Language controls all translatable user-visible text: product names and descriptions, category names, custom attributes, and the language sent to integrations. All text in Centra that is shopper visible is translatable. A language can be localized (American English for American audience) or non-localized (English).
Country leads localization in fashion ecom
Most websites — and most CMS tooling — assume language leads localization. You pick a language and that determines what you see. In global fashion ecom this works differently. Ship-to country leads. Two shoppers reading the same language can need entirely different content because they want to ship to in countries with different product assortments, different inventory, different currencies, different prices, different legal disclosures, and so on. An English shopper wishing to ship to their vacation home in Spain should see Spanish inventory and prices in euro (while browsing the site in English), not UK inventory and prices in pound sterling.
Session and state ownership
Centra owns the session. The session holds the cart, the selected Market / Ship-to country / Pricelist / Language, the customer linkage if the shopper has logged in, and any session-scoped state Centra needs to honor the cart correctly through checkout. Centra issues a session token in the extensions.token field of every response. The token is opaque and may change depending on the action taken, the first session mode request should block to resolve one session for the shopper. Once you have received the token for the shopper's session you should store it securely and pass it in the Authorization: Bearer {token} header for subsequent SESSION MODE requests.
When the shopper does something that changes segmentation — switches country, resolves a state, switches language, logs in to an account that maps to a different Market — the client pushes the change into the Centra session via a direct, session-mode Storefront API call. Centra updates the session, the cart is reconciled against the new segment (items unavailable in the new segment are removed and reported back in userErrors so the storefront can surface them to the shopper), and the next response reflects the new segmentation. The client typically then navigates to a URL that encodes the new segmentation, picking up a fresh shell from the CDN. From that moment, every other origin the storefront talks to has to receive the updated segmentation parameters too, or the experience drifts out of sync.
A practical consequence: the Storefront API operates in two distinct modes. Catalog reads can be answered without a session — given only the segmentation parameters, Centra returns the same catalog for every shopper in that segment, which means those responses are safe to cache. Cart, checkout, customer, and any session-scoped query require the session token and must never be cached across shoppers. Most implementations use two separate Storefront API integrations: a no-session credential used by the composition layer for cacheable catalog reads while it composes shells, and a per-session credential used by the client for direct calls to cart, checkout, and other session-bound endpoints.
Changing segmentation
The segmentation parameters in Centra are controlled very differently.
The shopper should be able to change their ship-to Country / Country-State, and their preferred Language / localized Language.
The shopper should not be able to change their Market or Pricelist. Market and Pricelist are determined by Centra or by other logic they you may want to implement, but never by the shopper.
Never ever implement a BFF that allows a storefront client to change Market or Pricelist by reaching a route, passing URL parameters, or headers. This opens up for both technical issues and fraud. Let the shoppers select where they are shipping to and what language they prefer, and rely on backend logic (in Centra) to assign the right Market and Pricelist.
Caching, in five layers
Caching is where most storefront architectures succeed or fail at scale. Five distinct caches sit between Centra and the shopper, each solving a different problem. One is a property of Centra itself; the other four are configured by the integrator: an origin response cache and a composed-output cache in the composition layer, a CDN edge cache that makes scale affordable, and the shopper's browser cache that eliminates unnecessary network traffic altogether.
The Centra Storefront API cache
The Storefront API is heavily cached on Centra's side. Catalog reads are eventually consistent; update webhooks fire after the cache has been refreshed, so re-fetching on a webhook reliably yields the new state. Session-bound data — cart, checkout, customer — bypasses this cache and is authoritative on every call.
Layer 1 — Origin response cache: protects the origins
The three server-side caches in this guide (Layer 1, Layer 2, and Layer 3) are assumed to run on edge hosting infrastructure geographically close to shoppers. "Origin-side" refers to what is cached — responses from the origin systems Centra, the CMS, and others — not to where the cache itself lives. In a typical deployment the composition layer and its origin response cache both run at edge points of presence; the CDN cache (Layer 3) sits in front of that, caching the composition layer's output. A cache hit at the origin layer is fast because both the cache and the composition logic execute close to the shopper, not in a single distant data center.
The composition layer caches responses from Centra (and from the CMS, and from other relatively stable sources) so the same query is not repeated against the origin for every page view. Without this, a popular product page hits Centra once per view; with it, Centra is hit once per cache lifetime per segment combination.
Three properties make this work:
- Cache key includes the segmentation tuple. A catalog query for
{market: 1, pricelist: 1, country: us, language: en}is a different cache entry from{market: 13, pricelist: 2, country: us, language: en}. The same product page produces different cache entries per segment. - Webhook-driven invalidation. Centra emits webhooks when underlying data changes (display items, categories, markets, languages, and so on). The composition layer listens, verifies the signature, and invalidates the affected cache entries — usually by tag (
product:123,category:42) rather than by key. Tag-based invalidation lets one event evict every entry that referenced the changed entity, across all segments. - TTL as a safety net. Webhooks can be missed — network failures, deploys, expired credentials. Every cached entry needs a maximum lifetime as a fallback so stale data eventually drains out even when invalidation does not fire. Typical values are hours for product data and days for slower-moving reference data (countries, languages, root categories).
The same three properties — segmented cache key, change-driven invalidation, TTL fallback — apply to the CMS and to any other relatively stable source. The exact mechanics vary by tool: most CMSes emit content-change webhooks the composition layer can subscribe to; some only offer polling. The pattern is the same regardless: cache responses keyed by segmentation, invalidate on change, fall back to TTL when invalidation fails or is unavailable.
Cart, checkout, customer, and any session-bound response must opt out of this cache entirely. The reference implementation in the Centra Storefront Accelerator demonstrates the Centra-facing mechanics: the webhook receiver, HMAC signature verification, tag-based invalidation of the framework cache, and explicit no-store on session-bound calls. It is intentionally narrow — it has no CMS integration, no analytics, no experimentation, no CDN-edge cache layer, and it does not encode Market and Pricelist in the URL the way this guide recommends. Treat it as a working starting point for the Centra integration, not as a production storefront.
Layer 2 — Composed-output cache: absorbs thundering herd at the BFF
Layer 1 caches the raw responses from Centra, the CMS, and other sources. It does not cache the composed response that the BFF returns. Re-composition still runs on every request, even when every origin call is a hot Layer 1 hit.
That matters in one specific failure mode. A popular page is purged from the CDN; within a fraction of a second, hundreds of edge points of presence all miss for the same URL and slam the BFF in parallel. Layer 1 protects the origins, but the BFF still has to compose hundreds of identical responses. A composed-output cache at the BFF, keyed by URL, lets the BFF compose the response once and serve the cached copy to every other PoP asking for the same thing.
Two implementation paths are common, and they substitute for each other more than they stack:
- Use what your framework already provides. Modern web frameworks — Next.js (full route cache), Remix, SvelteKit, and similar — implement composed-output caching natively. If you are on one of these, you already have Layer 2 by default; the work is configuring it (cache key, revalidation, tags) rather than building it.
- Build it yourself. If your stack does not give you this for free, cache the composed response keyed by URL, invalidate it on the same webhook events that drive Layer 1, and serve from cache when the key is warm.
A related option lives at the CDN rather than the BFF: origin shielding. Fastly, Cloudflare (Argo Tiered Cache), and Akamai (Tiered Distribution) let you designate one PoP as a "shield" between the edge fleet and your origin. A cold miss anywhere in the network goes through the shield first; the shield either serves from its own cache or makes one request to the BFF. This solves the same thundering-herd problem from the other side, and many production storefronts use it instead of a dedicated BFF output cache.
Whichever you pick, the invalidation rules mirror Layer 1: URL keys reflect segmentation, webhook-driven invalidation, TTL as a safety net. The purge sequence extends one step — composed-output cache before CDN purge, so the CDN never re-warms from a stale composed entry.
Layer 3 — CDN edge cache: makes scale affordable
Origin caching does not solve cost. The composition layer is itself expensive compute — running rendering, calling out to multiple origins, composing a response. Every request that reaches the composition layer costs real money, even on a hot origin cache. A CDN edge cache, by contrast, serves responses from points of presence at a fraction of the cost. The economic difference between "served by your BFF" and "served by your CDN" is typically one to two orders of magnitude.
For the CDN to cache anything, the response must be the same for any shopper hitting the same cache key. CDN cache keys are derived from the URL. The URL must therefore encode every parameter that changes the response — or the CDN serves the wrong content to someone. The HTTP spec also offers Vary to split the cache by a request header; for HTML page loads it is more trouble than it solves, covered in its own section below.
Three properties make Layer 3 work:
- Segmentation in the URL. The URL is the cache key, and the four segmentation parameters must be visible inside it. The exact shape is covered in the next section.
- Webhook-driven invalidation, sequenced after Layers 1 and 2. When Centra emits a change webhook, the composition layer invalidates the affected Layer 1 and Layer 2 entries first, then purges the corresponding URLs from the CDN. The order matters: if the CDN purge fires before the upstream caches are fresh, the next edge miss hits a still-stale upstream entry and that stale response gets re-cached at the edge. Tag-based purges (Fastly surrogate keys, Cloudflare cache tags, Akamai cache tags) let one webhook handler invalidate all three layers from a single tag, which simplifies the sequencing.
stale-while-revalidateon cacheable responses. SetCache-Control: public, s-maxage=<TTL>, stale-while-revalidate=<longer>. The CDN serves the cached entry untils-maxage; after expiry it keeps serving the stale entry to shoppers while refetching a fresh copy in the background. Shoppers never wait for a revalidation, and expiries spread out instead of converging into a stampede against the origin.
The three properties above describe how invalidation works. Just as important is the discipline of not invalidating when nothing meaningful changed. CDN purges and the re-renders that follow are real cost — both in CDN purge requests at vendor pricing, and in the composition-layer compute that has to rebuild every cold entry the next time a shopper asks for it. A composition layer that purges on every Centra webhook is treating the CDN as a write-through cache, which defeats the point of having a CDN at all. The discipline is to ask, on each webhook, does this change actually alter what we send to a shopper?
Concrete examples of webhooks that often should not trigger a purge:
- A stock tick that does not cross the displayed-availability threshold. Centra fires an update when an item goes from 38 in stock to 37; the rendered page still says "in stock" and looks identical. Only flip cache state when the rendered state actually changes — into or out of stock, or across a "low stock" band the site actually displays.
- A translation change that does not affect the segments you would purge. If only the Japanese description was edited, the English, Swedish, and German variants of the same product render identically to before. Purge the Japanese variants and leave the rest cached.
Two implementation patterns help in practice:
- Compare against a render hash. The composition layer keeps a short hash of the rendered output per cache entry, keyed by
{entity, segment}. On a webhook, it re-fetches the entity from Centra, recomputes what the rendered output would be, and compares hashes. No diff, no purge. This catches stock ticks, irrelevant admin updates, and unaffected segments in one rule. - Coalesce bursty webhooks. A bulk catalog import or a campaign launch can fire hundreds of webhooks in a few seconds. Coalesce them in a short window (a few seconds) and de-duplicate by tag before issuing purges, so one logical change produces one purge wave rather than hundreds. This also smooths the rebuild load on Layers 1 and 2, and on the origins behind them.
Done well, the difference between brute-force invalidation and disciplined invalidation is a step-change in CDN cost and in steady-state hit rate. Most Centra storefronts that struggle with CDN cost are over-invalidating, not under-caching.
Layer 4 — Browser cache: free, but uninvalidatable
Every shopper's browser keeps its own private cache, governed by the same Cache-Control header as the CDN. A hit here costs zero network — even faster than the edge — and removes load from the CDN entirely. The catch: you cannot invalidate it remotely. Once a response is in a shopper's browser cache, it stays there until its max-age expires.
This shapes the values you set:
- HTML responses — keep the browser TTL short or zero (
max-age=0) and lean on the CDN for the shared cache. Combine withstale-while-revalidateand anETagso the browser can revalidate cheaply (a 304 with no body) when entries expire. Modern browsers honorstale-while-revalidateat the browser cache level, somax-age=0still feels instant on back/forward — the browser serves the cached copy immediately and revalidates in the background. Long browser TTLs on HTML mean shoppers can see stale prices, stale stock, or old campaign content even after you have purged the CDN — and there is no shared cache to fix it in a hurry. - Static assets (JS bundles, CSS, fonts, images) — the inverse. Hash the filename (
main.abc123.js), setCache-Control: public, max-age=31536000, immutable, and let browsers keep them forever. Any change ships under a new filename, so invalidation is implicit. - Per-shopper responses (cart, customer, session-bound calls) —
Cache-Control: private, no-store.privatekeeps the response out of any shared cache;no-storekeeps it out of disk as well.
This layer is mostly set-and-forget once the defaults above are in place — but the wrong default (long max-age on HTML) is a common storefront bug that takes weeks to surface and is impossible to fix in a hurry.
Encoding segmentation in the URL
Centra's four segmentation parameters all change the response, including Country-State where it applies. To make a page CDN-cacheable, all of them must be in the URL. The naive shape used throughout this guide (more about production routing below):
/{marketId}/{pricelistId}/{country}/{state}/{language}/...
We use this simple URL shape in this guide as it is easy to follow.
The {state} segment is always present. When no state applies — either the country has no states configured, or no state has been resolved yet for this session — the segment is a single dash (-). This keeps every URL the same shape and the CDN cache key the same length.
For example:
/1/1/us/-/en/products/shirt-123— Market 1, Pricelist 1, US shopper, state not yet resolved, English./1/1/us/ny/en/products/shirt-123— same, shopper in New York./1/1/us/hi/en/products/shirt-123— same, shopper in Hawaii. May see different in-stock status than the New York variant./1/1/de/-/en/products/shirt-123— Germany, no states configured, English./13/2/us/ny/en/products/shirt-123— VIP Market 13, Pricelist 2, New York shopper, English.
A few design notes on this URL shape:
- Lowercase everywhere — including state codes — the URL convention, and what most CDNs and analytics tools normalize to anyway.
- Numeric IDs for Market and Pricelist map directly to Centra's identifiers, are stable, and avoid slug-collision and rename problems.
- Country, state, and language use ISO 3166 alpha-2, ISO 3166-2 (the part after the dash), and BCP 47 respectively. The hreflang tags emitted in the page head use BCP 47 regardless of the URL shape — the two are independent.
- The
-sentinel is deliberately a single character with no semantic meaning of its own, so the routing layer can treat it as "no value" without ambiguity. Two URLs that differ only by-vs a real state code are two distinct cache entries, as they should be.
State-aware caching does not meaningfully hurt cache hit rates. Edge points of presence serve regional traffic — Pacific PoPs serve Hawaii, mainland PoPs serve Alabama, European PoPs serve Germany — so each PoP only ever warms the state variants its local population requests. The state axis multiplies the total cache footprint across the network, but does not fragment any single PoP's working set the way Vary: Cookie would.
When a shopper loads a URL whose segmentation does not match their session — for example, the session is on Market 1 but the URL says /13/, or the session is on sv but the URL says /en — there is a route-vs-session mismatch that must be resolved. The handling depends on which parameter mismatched:
- Ship-to country or Language mismatch. Show a pop-up letting the shopper confirm ship-to and language. This is the common case when a shopper follows a link from abroad and the URL implies a different country than their session.
- Market or Pricelist mismatch. Silently redirect to the URL matching the session's Market and Pricelist. A URL alone never grants the shopper access to a non-default Market or Pricelist — the session must already be on it (assigned by Centra via login or other Centra-side mechanism) before that URL renders the corresponding content. A non-employee following an employee-Market link is moved to the default URL for their country before the page renders.
The storefront accelerator demonstrates both behaviors.
Why URL, not Vary
Vary: <header> is the HTTP mechanism for splitting a CDN cache by request header. It is real, widely supported, and works perfectly for low-cardinality stable headers like Accept-Encoding. For segmenting commerce HTML it does not work cleanly, for one specific reason.
Browsers do not send custom headers on top-level navigations. When a shopper types a URL or clicks a link, the browser sends the URL, standard headers (Cookie, Accept-Language, etc.), and nothing else. There is no way to make the browser attach an X-Centra-Market header to a page load. The segment value isn't in any request header the CDN can Vary on — unless you put it there.
The two ways to put it there both carry real costs:
Varyon a standard header.Vary: Cookiesplits the cache by cookie value, which is usually unique per visitor and effectively disables shared caching.Vary: Accept-Languagesplits by the raw browser string (en-US,en;q=0.9,sv;q=0.8), which fragments the cache into too many variants to be useful.- Edge compute to inject a normalized header. A Cloudflare Worker, Fastly VCL, Lambda@Edge or Akamai EdgeWorker reads the segment cookie and injects clean header values before the cache lookup. This works, at the cost of vendor-locked code, per-request compute fees, and a hidden cache key that is harder to debug, share, and selectively invalidate. At that point the system is doing URL-encoded caching internally and hiding it from the shopper.
For commerce HTML there is no portable, cheap way to get high CDN hit rates without putting segmentation in the URL. This guide recommends URL-encoded segmentation accordingly.
Vary is genuinely useful in one place: client-side API calls. Browser fetch() and XHR can set custom headers. A storefront that ships a thin HTML shell and fetches segmented data from /api/... endpoints can Vary those responses on X-Centra-Market, X-Centra-Pricelist, X-Centra-Country, X-Centra-Language and get clean per-segment caching with no edge compute needed.
URL patterns in production
The shape this guide uses — /{marketId}/{pricelistId}/{country}/{state}/{language}/... — makes every segmentation parameter visible in the path. That is the most explicit option, easiest to reason about while learning, and a perfectly valid production shape. In real-world deployments you have more choices. The architectural constraint is the same regardless of shape: the cache key must reflect every parameter that changes the response. How the URL achieves that is up to you.
Several patterns are worth knowing about. One concerns country-level scoping (ccTLDs); the others concern how Market and Pricelist are encoded within a country (default omission, query parameters, word aliases). Most production storefronts combine one from each group.
Country-code top-level domains (ccTLDs). Country-specific top-level domains (brand.us, brand.de, brand.jp) are the strongest geographic signal you can give to shoppers and to search engines. Google treats ccTLDs as the canonical surface for their country, which is hard to match with any path-based scheme. Inside each ccTLD, segmentation still has to live somewhere — typically the path — but the country segment becomes implicit in the domain. The cost is operational: a separate DNS zone, certificate, deployment target, and analytics property per ccTLD, plus per-ccTLD content workflows if regulators or marketing teams want them. ccTLDs work best for brands that already operate per-country legal entities and per-country marketing teams, where the operational overhead matches an existing structure.
Default omission for canonical URLs. Each ship-to country in Centra has a configured default Market and default Pricelist. Drop both from the URL when they match the country's defaults. So /us/ca/en/products/shirt-123 is the canonical URL for a shopper in California reading English on the US default Market and Pricelist — the URL carries only the parameters a shopper would recognize. Non-default Markets and Pricelists reach the same surface through a longer URL shape, covered in the next two paragraphs. This is the most production-common pattern for two reasons: it produces clean indexable URLs that match what shoppers and search engines expect, and it aligns directly with the canonical-URL strategy in Search engine optimization below — exactly one indexable URL per country and language, with non-default Markets carrying noindex. The pattern locks in one indexable storefront per country — fine for most brands, awkward if a brand later wants two equally-indexable defaults (a consumer and a small-business retail experience, for instance), at which point one of them has to take a non-default URL shape and its own canonical strategy.
Query parameters for non-default Markets and Pricelists. When the shopper's session is on a non-default Market or Pricelist, the URL must still encode them — the CDN keys on the URL and would otherwise serve the wrong content. A query string is the lightest option: /us/ca/en/products/shirt-123?market=13&pricelist=2 is what a VIP shopper sees for the same surface. The encoding is for cache-key correctness only; the URL never drives Market or Pricelist assignment (see Changing segmentation). This connects directly to the SEO mitigations: non-default Markets carry noindex anyway, so they do not belong in the indexable path.
The catch with query parameters is CDN handling. Default behavior varies by vendor — some include the entire query string in the cache key (which makes the cache thrash on UTM and tracking params), others ignore the query string entirely (which serves the wrong content to a VIP shopper). Either way, you have to configure the CDN to include a whitelist of segmentation params in the cache key, normalize their ordering, and strip everything else. Every major CDN supports this (Cloudflare's Cache Key rules, Fastly's VCL, Akamai's Cache ID Modification); it is a one-time setup, not a per-deployment chore. Without it, query-param segmentation either does not cache or caches incorrectly.
Word aliases for non-default Markets and Pricelists. For shoppers whose session is on a non-default Market, the URLs in their browser bar can read more nicely with a short word alias than with numeric IDs or a query string. /employees/us/ca/en/products/shirt-123 is more memorable than /13/2/us/ca/en/products/shirt-123 or the query-string variant above. The composition layer resolves employees to a Market+Pricelist tuple while parsing the URL, the same way it resolves the country's default. Aliases are purely a cosmetic choice for how the URL is shaped — they do not drive Market or Pricelist assignment, and a shopper following an aliased link without a matching session is silently moved to their own session's URL like any other route-vs-session mismatch. They are an alternative to the query-parameter shape, not a layer on top of it, and they carry trade-offs the numeric form does not:
- Aliases drift. Numeric IDs are stable; aliases get renamed by marketing.
vip-usbecomesgold-usafter a loyalty rebrand, and the composition layer is left maintaining redirects from every retired alias. - Collision surface. A word alias shares the path namespace with categories, CMS slugs, and country codes. The routing layer needs an explicit precedence rule for which slot wins — a place bugs hide.
- The alias map is config. The alias-to-Market mapping lives in the composition layer and has to stay in sync with Centra as Markets and Pricelists evolve.
These pages are noindex regardless, so the benefit is purely a more memorable URL in the browser bar for shoppers already on that Market — a real but smaller win than the default-omission case. Reach for word aliases when marketing wants those URLs to read nicely; otherwise the query-parameter shape is simpler.
In practice, most production storefronts combine the patterns above. A typical layout: ccTLD or a single path-based root for country-level separation, path segments for state and language, the default Market and Pricelist implicit from the country, and query parameters or word aliases for non-default Markets that should not be indexed. The all-path shape used in this guide is a teaching shape — it makes every parameter explicit. Once you understand which parameter sits where, you can reshape the URL however your SEO strategy and operational structure demand.
What should not be cached at the edge
Markets and Pricelists meant to stay secret (employee discount programs, undisclosed VIP tiers) must not be CDN-cached. The route-vs-session redirect that keeps non-matching shoppers out runs in the BFF, not the CDN — a CDN cache hit serves the cached HTML directly to the client and exposes the page to anyone who knows the URL. Mark secret-Market responses with Cache-Control: private so they never enter the shared cache.
What cannot be cached at the edge
Some data is per-shopper and cannot live in a shared CDN entry:
- Cart and checkout — bound to a specific session.
- Real-time inventory checks at the moment of add-to-cart — must hit Centra to avoid overselling.
- Personalized recommendations — different for every shopper.
- Search results when query-dependent — driven by user input.
- Variant assignment for active experiments — see the experimentation section below.
- Heavily filtered product listing pages — popular filter combinations (one category, sorted by newest) cache well; long-tail combinations (three rare filters intersected) cache poorly because density falls off. Either accept the long tail goes to origin, or pre-cache the popular subset.
These fill in as islands within the cached page shell — typically fetched by the client directly from the relevant origin — covered in The page shell pattern below.
The page shell pattern
A working storefront looks at first glance like one rendered page, but it is two assemblies stitched together: a shell that is identical for every shopper in the same segment, and a small number of islands that vary per shopper. The shell is fully CDN-cacheable; the islands are filled in after delivery. Naming this pattern explicitly is useful because most of the caching, segmentation, and integration rules in this guide presuppose it.
The two assemblies travel two different paths. The shell is composed once by the composition layer and served from the CDN — after the initial cache warm, the overwhelming majority of page views are delivered straight from the CDN without the composition layer running at all. The islands are fetched by the client directly from the relevant origin: Centra's Storefront API using a per-session credential for session, cart, checkout, and dynamically filtered PLPs; the discovery vendor's client-facing API for search, merchandising, and personalization. The composition layer is not in the path for these calls. This split is what makes a well-built Centra storefront cheap to run at scale and very fast — the cacheable surface is delivered by the CDN, the per-shopper surface is delivered by origins built to handle that traffic, and the composition layer is only invoked on a CDN miss.
What lives in the shell:
- PDP and PLP layout, product attributes and copy from the Storefront API catalog, category navigation, footer.
- CMS modules that depend only on the segmentation tuple.
- Structured data (JSON-LD), Open Graph, canonical and hreflang tags.
- Anything that depends only on
{market, pricelist, country, state, language}and the URL.
What lives as an island:
- Cart bubble and cart preview (session-bound).
- "You may also like" and other recommendation rails (per-shopper).
- "Welcome back" or personalized hero variants.
- Real-time availability check inside the buy-box ("only 2 left").
- Variant assignment outcomes for active experiments that target individual shoppers.
There are three common fill mechanisms; pick per island, not per site:
- Client-side fetch directly to the origin. Browser-side JS calls Centra's Storefront API in session mode for Centra-backed islands (session, cart, checkout, filtered PLPs), and the discovery vendor's client-facing API for search, merchandising, and personalization. These calls bypass the composition layer entirely — the BFF is not in the path. Simplest, fully cache-agnostic, and the recommended default for most islands. The trade-off: the island is not visible until JS runs.
- Edge-side includes (ESI). The CDN composes the cached shell with dynamic fragments before sending. Works without client JS; supported by some CDNs (Fastly, Akamai) but not all.
- Server-side streaming. The composition layer flushes the shell first and streams island content as later chunks of the same response. Modern frameworks (Next.js with Suspense, etc.) implement this natively.
Three traps to watch for:
- LCP and above-the-fold personalization. If the personalized element is the Largest Contentful Paint target, client-side hydration measurably hurts Core Web Vitals. Use ESI or streaming for above-the-fold islands.
- Cumulative Layout Shift. Reserve space for islands; otherwise the page jumps when they arrive.
- Crawlers that do not execute JS. Indexable content — product info, descriptions, prices in structured data — must live in the shell, never in islands.
Integrating each category to stay in sync
Propagating the four segmentation parameters — plus Country-State where applicable — into every downstream call is the central integration job. The composition layer carries them on the calls it makes while composing the shell; the client carries them on the calls it makes directly to origins. Each category has slightly different mechanics.
CMS — the most important to get right
A Centra storefront's CMS holds two things: editorial pages (campaign landing pages, brand stories, lookbooks, legal, FAQ) and reusable content modules (hero blocks, promo banners, editorial product grids, image-with-text, etc.) that are placed into pages or into product/category templates.
Both pages and modules must be segmentable on Market, Ship-to country, and Pricelist, translatable on Language, and further segmentable on Country-State where state matters. The reason is direct: editorial content references things that vary by segment. A campaign banner displays prices (Pricelist), promotes products (Market), promises free shipping over a threshold (Ship-to country), and reads in a specific language (Language). If any of those dimensions can drift between Centra and the CMS, the storefront promises one thing and delivers another. State enters when editorial copy needs to vary inside a country — a "shop today to get it by Christmas" banner that shows for continental US shoppers but hides for Hawaii and Alaska, a shipping-terms paragraph that reads differently in California than in Texas, a checkout disclosure that varies by state.
Many CMS tools are simply too basic to support advanced global ecommerce. We don't recommend specific CMSes in this guide, but a good CMS for a Centra ecommerce build needs to a) technically support the required segmentation and b) do so while offering a productive experience for the editors who live in it every day.
Most CMS tools default to language-leads localization — content is forked per language, then optionally per region. In fashion ecom this default is wrong. The CMS needs to be configured so that country leads: the same English page can have a US version, a UK version, and a JP version (in English) that differ in featured products, prices, and legal copy, before any language fork happens. If the CMS cannot model this — country as a top-level segmentation axis, language nested under it — pick a different CMS or build the layer in the composition layer that makes it behave this way.
Integrated editing experience
The principle: editors should feel like they are building a website in one world-class CMS, not stitching together three half-connected systems with manual cross-references and screenshots. Building this integration is where most Centra CMS implementations either succeed or quietly fail. The investment pays back every time an editor launches a campaign in a new Market without filing a support ticket and without showing wrong prices on launch day.
Centra is the master data source for segmentation and catalog. Markets, Ship-to countries, Pricelists, Languages, product Displays, categories, campaigns, vouchers — all of these are configured in Centra and only in Centra. The CMS must pull these values from Centra rather than maintain its own hardcoded lists or stale local copies. When marketing creates a new Market — a country expansion, a VIP segment, an employee Market — it appears in Centra first and the CMS picks it up automatically as a new segmentation axis available on every page and module. No second admin to update, no risk of CMS and Centra disagreeing on which Markets exist.
This master-data integration unlocks the experience editors actually feel:
- The editor is always working in a segment. The CMS UI makes the editing context explicit — "Editing the US Market, USD Pricelist, English version of the home page" — and every subsequent query to Centra is scoped to that segment. Switching context updates the canvas to reflect what that audience will actually see. The natural filter hierarchy is Market → Country → Country-State: editors reach for Market first because it is Centra's primary segmentation axis, narrow to Country when copy or assets vary by destination, and narrow further to a specific state only when needed (a Christmas-delivery banner that should not show in Hawaii, a state-specific legal disclosure).
- Product pickers query Centra live, filtered by the editing segment. When an editor adds a product hero, the picker only offers products available in this Market, with this Market's images, names, and prices straight from Centra. Products outside the Market are not selectable. Search inside the picker hits Centra's catalog scoped to that Market. The same applies to category pickers, voucher pickers, and any other Centra reference.
- The CMS stores references, not product data. When the editor saves a module, the CMS persists only the Centra reference — the
displayItemID, the category ID, the voucher ID — never a snapshot of the name, image, price, or availability. Those fields are fetched live from the Storefront API while the page is being edited, so the editor always sees current data; the same live fetch happens at render time through the composition layer. There is no local copy of the product in the CMS to drift from Centra in the first place. - Previews come from Centra at edit time. The hero block in the CMS canvas shows the actual product image, the actual translated name, and the actual price for the segment being edited — not a placeholder, not a stale snapshot, not "lorem ipsum until publish."
- Cross-system references are validated continuously. If the editor narrows a page's Market visibility, modules inside the page that target Markets no longer covered surface a warning. If a referenced product is removed from a Market in Centra, every CMS module that featured that product flags the broken reference. If a Pricelist is renamed or retired in Centra, pages that referenced it surface the impact before they go live. The CMS knows what it references and reflects Centra's reality back to the editor before it becomes a shopper-facing bug.
- Preview is real. "Preview as US Market, English shopper" renders the page through the actual composition layer with the actual segmentation, so what the editor sees in preview is exactly what the shopper will see in production. Preview is not a simplified render path; it is the live storefront with one parameter changed.
The destination is editors who feel they are working in a single world-class CMS that happens to know about every product, Market, and price the brand sells — not stitching together three systems with spreadsheets and Slack messages.
In sync at runtime
The composition layer takes {market, pricelist, country, state, language} from the request URL when composing a shell, sends them as filter parameters on every CMS query, and the CMS returns the variant that matches. Modules placed inside pages are resolved with the same filters. Centra IDs referenced inside content (product IDs, category IDs) are hydrated from Centra at composition time, never from CMS-cached snapshots, so prices and availability stay live. State is passed through as null when none has been resolved for the session yet — content authored without a state filter applies in both cases, and state-targeted variants light up only when state is known.
Personalized content modules. Some content modules are designed to vary by shopper — a hero block that swaps based on browsing history, a "welcome back" greeting, a personalized promo. These render as islands using the page shell pattern. The personalization decision typically comes from a separate service (often the same tool that powers recommendations); the CMS supplies the candidate variants.
Caching. CMS responses are statically cacheable per segmentation tuple just like Centra catalog responses, with their own webhook-driven invalidation if the CMS supports it, and TTLs as a fallback. Page shells composed from a Centra catalog response plus a CMS response are CDN-cacheable at the edge, keyed by the URL.
Product discovery — search, merchandising, recommendations
Search, merchandising, and recommendations are three angles on the same problem: presenting the right products to the right shopper. They are often delivered by a single vendor and share the same underlying data — an index of products from Centra, augmented with searchable attributes, scoring rules, and shopper signals. The architecture is the same for all three: feed Centra data into the tool, propagate segmentation, return ordered product IDs, hydrate from Centra for display.
Search is shopper-query driven — the shopper types something and the tool returns ranked results, with autocomplete and faceting. Merchandising applies curated rules (boost, demote, pin, slot) to category and search result orderings. Recommendations are algorithmic and often per-shopper — "you might also like", "frequently bought together", "trending now". A single platform typically does all three.
Market is THE most important segmentation here, because Market controls which products (or, more precisely, which Displays) the shopper can find at all. A product not in the shopper's Market must not be returned by search, must not be merchandised onto a category page, and must never appear in a recommendation rail. Pricelist and Language matter too — Pricelist for the displayed price on each result, Language for translated text — but Market gates what enters the result set in the first place.
Picking a tool. A discovery tool for a Centra build must support deep segmentation — not just by language or country, but by Centra's Market and, where states drive warehouses, by Country-State. Many off-the-shelf tools were built assuming country-leads-localization (one index per country) and struggle with the higher-cardinality Market dimension common on Centra sites — hundreds of active Markets is normal on a deeply segmented brand. Confirm the tool can hold per-Market visibility, per-Pricelist pricing, and per-state inventory facets as first-class index dimensions, and that querying by Market and state is a first-class filter, not an attribute hack.
The state requirement matters because the most basic merchandising rules depend on accurate per-shopper availability. Demoting out-of-stock products to the bottom of a PLP and excluding sold-out items from recommendation rails is table-stakes merchandising — and it only works if the discovery tool sees the same per-state stock that Centra does. A jacket that is in stock in New York and out in Hawaii must surface near the bottom for the Hawaii shopper, not for the New York shopper. If the tool only knows market-level availability, those rules either misfire (recommending items the shopper cannot buy) or have to be turned off, and the storefront loses one of the most reliable conversion levers it has.
In sync. The index is fed from Centra — typically a periodic full sync plus incremental updates driven by the same webhooks that invalidate the origin cache. Each indexed document carries the segmentation that determines its visibility: which Markets it belongs to, which Pricelists carry it, which Languages have translations, and stock by warehouse so per-state availability can be computed at query time. Static PLPs in the page shell are queried by the composition layer with the active segmentation as filters, including state where it applies. Dynamic queries — search-as-you-type, faceted PLP filtering, personalized recommendation rails — are issued by the client directly to the vendor's client-facing API with the same segmentation tuple. Inventory either lives in the index (updated frequently) or is fetched live from Centra at render time when accuracy matters more than result speed. Merchandising rules are authored per segment and applied at query time. Recommendation requests also include the shopper's identifier (or anonymous session token); returned product IDs are hydrated from Centra so prices and availability are real-time and segment-correct.
Caching. Search responses are typically too query-dependent to share aggressively across shoppers. Autocomplete and "popular searches" can be cached; specific query results usually cannot. Category PLPs rendered from segmented index queries can be cached per segmentation at the origin, and at the edge if the segmentation is in the URL — see the filtered-PLP caveat above for the long-tail constraint. Recommendation rails are per-shopper and render as islands via the page shell pattern.
Reviews and other UGC
Reviews are typically attached to products and translated. They are mostly stable — written by shoppers, moderated, then read by many — which makes them cacheable.
Segmentation can usually be simplified for reviews: product and language are enough. Reviews don't change based on Market or Pricelist — a review of a product is a review of that product, regardless of who is reading it. Market visibility is already handled by the product itself: if the product is not in the shopper's Market, the product page never renders, and the reviews on it never surface. The reviewer's own segment (gold member, employee, anonymous browser) doesn't matter at read time either.
In sync: the composition layer fetches reviews per product, filtered by language. Some implementations also filter by country for narrow legal or moderation reasons (return-policy claims in reviews, country-specific moderation rules) — but most do not need this layer.
Caching: highly cacheable per product and language. Webhook-style or polled invalidation when new reviews are approved.
Analytics and tracking
Analytics receive events about what shoppers do; marketing tags fire pixels and conversion events. The composition layer (and the client) is responsible for emitting events.
In sync: every event needs to carry the segmentation dimensions — Market, Pricelist, Ship-to country, Language, currency — as event properties. Without this, every analytics report is one-dimensional: revenue by country is fine, revenue by Market is impossible. Cart events, checkout events, and order events must include the same dimensions, sourced from the same session that drove the order through Centra, so reporting matches Centra's source-of-truth numbers.
Caching: not applicable — analytics is an outbound stream, not a cached response. The events are typically fire-and-forget from the client, with server-side reinforcement for revenue events that need to survive ad-blockers.
Experimentation and A/B testing
Experimentation platforms assign shoppers to variants and measure outcomes. Variant assignment must happen before the page composes, so the right variant of every module renders.
In sync: the composition layer (or the edge, before composition) calls the experimentation platform to resolve variant assignments for the current shopper, then passes the assignment into the CMS query (to load the right module variant), into Centra (if testing alternate product Displays), and into analytics (so outcomes can be attributed). For CDN cacheability the variant must become part of the cache key. A URL segment for the variant is the cleanest option; the alternative — folding a variant cookie into the cache key via vendor-specific CDN configuration — carries the same trade-offs as covered in Why URL, not Vary.
Testing Centra-side content — A/B testing the product Display, swapping image sets on a PDP, even running an empirical A/B test on Pricelist — is technically straightforward in Centra: set up the A and B variant inside Centra and have the composition layer load whichever variant the experimentation platform selected. In practice, fashion brands rarely test at the individual PDP level because traffic to any one PDP is usually too low to reach statistical significance in a reasonable time window. Higher-traffic surfaces — home page, category pages, checkout funnel — are where experimentation tends to pay off. PDP and Pricelist tests remain possible and occasionally worth it (a top-traffic hero product, a sustained price-elasticity test) but should be approached with sample-size math first.
Feature flags
Feature flags are the engineering cousin of A/B tests: same delivery mechanism, different intent. Flags gate rollouts, kill switches, and environment-specific behavior; experiments measure outcomes. The composition layer evaluates flags server-side (or at the edge) and renders accordingly. Flag values can themselves be segmented — a flag rolled out only in one Market, or only for one Ship-to country — and the same segmentation parameters apply.
Caching: flag-dependent responses need the flag value in the cache key, exactly like A/B variants. Flags expected to change frequently or be evaluated per-shopper should not be baked into edge-cached responses.
Discoverability for AI agents and search engines
Two audiences read storefront pages beyond shoppers: AI agents that answer shopping questions and complete purchases on shoppers' behalf, and search engines that index pages for organic discovery. Both rely on the same foundations — clean segmented URLs, accurate per-segment structured data, a defined canonical surface — and both reward storefronts that treat discoverability as a first-class concern. The channel-specific tuning differs, and the specific conventions on the AI side are still solidifying while traditional SEO is well established.
AI agents
AI agents — chatbots that answer shopping questions, browser-embedded assistants, agentic-shopping flows that complete purchases on a shopper's behalf — read and act on storefront pages as a routine audience alongside human shoppers and search engine crawlers. The discoverability foundations overlap heavily with traditional SEO, with a few commerce-specific additions.
Structured data on every page. Emit JSON-LD using schema.org vocabularies wherever structured data is meaningful. The high-value types for commerce are Product and its nested Offer (with price, priceCurrency, availability, itemCondition), AggregateRating and Review, BreadcrumbList, and Organization / Brand at the site level. AI agents read structured data to ground their answers — "What's in stock?", "How much is it?" — and cite it back to shoppers. Accuracy matters more than for traditional SEO because agents quote the data verbatim.
Structured data is per-segment. This is the trap. The price, availability, and currency in the JSON-LD must match exactly what the shopper would see and pay. A page served at /1/1/us/-/en/... emits USD prices and US availability; the same page served at /1/3/de/-/de/... emits EUR prices and DE availability. If structured data is generated once and shared across segments, agents will quote prices that are wrong for the shopper they are helping. Treat structured data as just another piece of segmented content — it travels through the same composition, the same cache layers, the same segmentation tuple as the visible page.
Robots policy for AI crawlers. The major AI crawlers respect robots.txt with their own user-agent strings — GPTBot (OpenAI training), ChatGPT-User (OpenAI live retrieval), Claude-Web and ClaudeBot (Anthropic), Google-Extended (Google's AI products), CCBot (Common Crawl, used by many models), PerplexityBot, and others. Each brand decides: allow them all (for visibility in AI answers), block training but allow live retrieval (a common middle ground), or block all (to protect proprietary content). The decision lives with marketing and legal more than engineering; the implementation is one file.
llms.txt. A proposed convention for sites to publish a curated, agent-friendly summary of structure (catalogs, categories, brand voice). Adoption is early; cheap to add if you have content worth summarizing.
Search engine optimization
URL-encoded segmentation creates a real SEO concern: the same product is reachable at multiple URLs (different market and pricelist combinations). Search engines see this as duplicate content unless told otherwise.
The standard mitigation pattern:
- Pick one default Market and Pricelist per country. That URL — for example
/1/1/us/-/en/products/shirt-123— is the canonical, indexable URL. - Emit
<link rel="canonical">on every non-default segment URL pointing back to its country's default. - Apply
noindexon non-default Market URLs (VIP, employee Markets, and any other segmented storefront that is not meant for organic search). - Use
hreflangalternates between country and language variants of the default Market — not between Markets. Search engines should see one canonical storefront per country, not many.
With these in place, search engines see a clean per-country indexable surface, while shoppers on non-default segments still get CDN-cacheable pages.
There are further SEO tricks to be made, consolidating link power to a fewer number of countries than the actual number of countries and then using the session mechanism to redirect visitors. Such technical SEO optimization is beyond the scope of this guide.
Beyond the web
Everything above applies to native apps and in-store / clienteling devices with three adjustments.
The composition layer takes a different shape. On the web it is typically a server-side application running at the edge. In a native app it can be a thin server-side BFF dedicated to mobile, an on-device composer that talks to each origin directly, or a managed mobile backend. The responsibility — assembling Centra and the other sources into one experience, propagating segmentation — is identical.
The cache layer takes a different shape. Web gets a CDN. A native app uses HTTP cache or a higher-level app-level cache; an in-store device may use a local cache that survives offline periods. The layered pattern still holds: cache origin responses to protect the sources, cache composed responses to keep the experience fast. URL-encoded segmentation matters less when there is no shared CDN — the segmentation can travel as request parameters or headers — but the cache key still has to include it.
Session lifecycle changes. On the web, sessions tie to a browser. In a native app, sessions persist across launches and survive longer; the token lives in secure storage. On a clienteling device, the session is often driven by the in-store associate, attaches to a specific shopper or anonymous interaction, and ends when the associate moves on to the next shopper. Centra's session model accommodates all of these — the difference is in how the client stores and rotates the token, not in what the session contains.
Where to go next
- Plugin setup — configure the Storefront API plugin and credentials.
- First interaction to order — the minimal happy-path query sequence from session creation to order.
- Catalog — segmented product, category, and pricing reads.
- Session — session creation, segmentation switches, and customer login.
- Cart and checkout — cart operations and order finalization.
- Migration guide — moving an existing storefront to the Storefront API.