Skip to content
weLabsweLabsStorefront Playbook
Live demoQuickstart

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.

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

Every amount travels as an integer count of the currency’s smallest unit, from the adapter to the last moment before rendering.

packages/commerce-core/src/types.ts
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
// Illustrative
const currency = await getStoreCurrency();
<span>{formatMinor(product.price, currency)}</span>
// Illustrative, in a "use client" component
const 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.

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 passes getLocale() to getAdapter(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 as lang) for WPML or Polylang, and declares multilingualContent only 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.

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.

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

Storefront Playbook · Built by weLabsFeaturesFAQTalk to us