Developers
Canonical model
The one commerce model every layer of the storefront speaks — its types, money in integer minor units, Locale and time-zone threading, Identity versus Session, and the Cart-Token.
The canonical model is the storefront’s own commerce vocabulary. It lives in @welabs/retail-commerce-core and it is the app’s
only model: components, Server Actions and routes import its types directly from @/lib/commerce. No backend shape crosses into
it, and there is no separate “UI type” layer (ADR 0002). Adapters translate their platform’s payloads into it, and nothing else does.
Where the types live
Section titled “Where the types live”packages/commerce-core/src/index.ts re-exports every module below; import from @/lib/commerce in the app, never from a file path.
| Module | Holds |
|---|---|
types.ts |
Catalog shapes: Money, Image, Product, ProductSummary, ProductVariant, ProductAttribute, Category, Brand, Tag, Review, ReviewMedia, ReviewReply, ProductVideo, TrustBadges, PaymentSecurity, SeoFields, Paginated |
queries.ts |
Query inputs: ProductQuery, ProductFilters, ProductSort, BrandQuery, BrandFacets, SearchResults |
DiscoveryAdapter.ts |
Catalog reads, Product Preview, DiscoveryCapabilities |
commerce.ts |
Cart, checkout and payment, orders and invoices, account and Address Book, reviews, wishlist, stock notifications, locations, Currency, CommerceCapabilities, CommerceAdapter |
IdentityAdapter.ts |
IdentityAdapter, IdentityUser, TokenBundle, sign-in and token results |
ThemeAdapter.ts |
StorefrontTheme: colours, header and footer documents, product page settings, FAQ, contact page, mobile tab bar, custom scripts, SEO settings |
ContentAdapter.ts |
Blog articles, terms and comments; Policy Pages |
GeographyAdapter.ts |
Sell-to and ship-to country lists |
seoPattern.ts, seoResolve.ts |
Filling SEO Patterns and resolving field, pattern and default |
The Port’s operations, by Capability
Section titled “The Port’s operations, by Capability”| Capability interface | Key operations |
|---|---|
| Discovery | getProduct(handle), listProducts(query), search(), getRelatedProducts(), getUpsellProducts(), getCrossSellProducts(), listCategories(), listTags(), listBrands(), queryBrands(), brandFacets(), openProductPreview(), getProductPreview() |
| Cart | getCart(), addItem(), updateItem(), removeItem(), applyCoupon(), removeCoupon(), setShippingAddress(), selectShippingRate() |
| Checkout | listPaymentMethods(), placeOrder(), preparePayment(), confirmPayment(), findPlacedOrder(), getExpressCheckout() |
| Order | listOrders(), getOrder(), getOrderByKey(), getInvoiceDocument() |
| Account | getCurrentCustomer(), getCustomerStats(), updateProfile(), listAddressBook() and its writes, the legacy listAddresses() and updateAddress() |
| Content | listArticles(), getArticle(), listComments(), createComment(), getPolicyPage(key) |
| Theme | getTheme() |
| Composed adapter | capabilities, getCurrency(), getStoreTimezone(), and the identity sub-adapter |
Reviews, Wishlist, StockNotifications, Geography and Location have their own interfaces in the same files.
Money is integer minor units
Section titled “Money is integer minor units”Every amount travels as an integer count of the currency’s smallest unit, from the adapter to the last moment before rendering.
export interface Money { /** Minor units, e.g. £12.34 → 1234. Never a float. */ amount: number; /** ISO 4217 code, e.g. "USD", "GBP", "EUR". */ currencyCode: string;}Formatting happens only at the display edge, with the store’s own currency settings (symbol, decimals, separators and symbol
position) read from the backend through getCurrency():
| Where you are | Use | Defined in |
|---|---|---|
| Server Component | formatMinor(money, currency), with currency from getStoreCurrency() |
lib/domain/currency.ts, lib/server/currency.ts |
| Client component | useFormatMinor(), from the CurrencyProvider context |
@/components/providers/currency-provider, which re-exports it from @welabs/retail-storefront-ui |
// Illustrativeconst currency = await getStoreCurrency();<span>{formatMinor(product.price, currency)}</span>
// Illustrative, in a "use client" componentconst format = useFormatMinor();<span>{format(line.total)}</span>Never divide by 100: a store’s currency may have zero or three decimals. Currency is a store property, not a Locale property, so
the separators come from the store, while dates and numbers follow the active Locale. The adapter converts at the edge of the
backend too: a few Companion routes answer in major-unit strings (for example total_spent on /account/stats), and the adapter
turns those into minor units before they cross the Port.
Locale is threaded through the Port
Section titled “Locale is threaded through the Port”A deployment declares an ordered Locale list (STOREFRONT_LOCALES). The first is unprefixed; the others live under /{locale}
(ADR 0005). The active Locale reaches the backend like auth does:
- Page code resolves the adapter with
getLocalizedAdapter()(lib/server/adapter.ts), which passesgetLocale()togetAdapter(auth, locale). - The Composition Root memoises one anonymous adapter per Locale, so one language’s cached content never answers another’s request.
- The WooCommerce adapter forwards the Locale as a query parameter (
WOO_MULTILINGUAL_PARAM, such aslang) for WPML or Polylang, and declaresmultilingualContentonly when that parameter is configured. A backend without translations serves its single content language for every Locale.
UI copy is a different thing: it comes from message catalogs, not from the backend. See Conventions.
Dates render in the store’s time zone
Section titled “Dates render in the store’s time zone”getStoreTimezone() returns the backend’s IANA zone (or a fixed offset such as +02:00), or null. The request config uses it as
next-intl’s timeZone, and the root layout hands the same zone to the client provider, so both sides of hydration agree (ADR 0010).
Every date the storefront shows is a store event: an order accepted, a sale scheduled, a post published. Time-boxed features such
as Today’s Deals anchor “today” to that zone. Where a shopper abroad could be misled, as on the sale countdown, the offset is
printed. siteConfig.timezone is only the fallback when the backend reports no zone.
Identity versus Session
Section titled “Identity versus Session”ADR 0003 put identity behind the Port and kept the session out of it.
| Identity | Session | |
|---|---|---|
| What | Backend identity operations: sign-in, registration, token refresh, revocation, social sign-in, password flows, profile | The app-side Auth.js state: encrypted session cookie, TTLs, remember-me, refresh scheduling |
| Speaks | Tokens in, tokens out (TokenBundle: accessToken, refreshToken, user) |
Cookies and the Auth.js session |
| Lives in | adapter.identity (IdentityAdapter), implemented by each adapter |
apps/storefront/lib/auth/ |
| Knows about the other | Nothing: the Port does not know Auth.js exists | Calls adapter.identity |
IdentityAdapter covers login, socialLogin, refresh, logout, register, forgotPassword, resetPassword, verifyEmail,
resendVerification, changePassword, setPassword, unlinkProvider, logoutAllDevices, me and getSocialProviders. On
WooCommerce it calls the Companion’s /auth/* and /account/* routes; in Demo Mode it is in memory.
Per-request customer context reaches the adapter as an AuthContext, { customerId?, token? }. The token is the backend’s
bearer token (the Companion’s JWT on WooCommerce), sent as Authorization on authenticated calls. It never reaches browser code:
the app’s /api/auth/* route handlers and Server Actions call the backend server-side.
Because the seam cuts both ways, Auth.js could be replaced without touching an adapter, and a new backend implements one more Identity capability instead of re-plumbing the app’s auth.
The Cart-Token
Section titled “The Cart-Token”The Cart-Token is the opaque token that keys a Shopper’s cart across requests. The storefront keeps it in an httpOnly cookie
(cart_token, 30 days) and passes it to the Port as CartRef.cartId. It is platform-neutral: the cookie holds whatever handle the
adapter issued as Cart.id. On WooCommerce that is the Store API Cart-Token header, which the Companion also uses to authorise
the PayPal preparation and placed-order routes.