Skip to content
weLabsweLabsStorefront Playbook
Live demoQuickstart

Developers

Capabilities

Every flag in CommerceCapabilities, what it unlocks in the UI, whether the WooCommerce and Demo adapters provide it, and how the storefront degrades without it.

A Capability is one segment of the Port. Each adapter declares a capabilities object of explicit booleans, and the storefront leaves out whatever a backend does not provide: no dead buttons, no half-built pages, no fallback that pretends. The contract suite rejects any flag that is not a boolean, because “unknown” is not a state the UI can degrade from.

CommerceCapabilities (in packages/commerce-core/src/commerce.ts) extends DiscoveryCapabilities (in DiscoveryAdapter.ts). That makes 22 flags. The WooCommerce and Demo columns are read from the two adapters’ constructors.

Capability What it unlocks in the UI WooCommerce adapter Demo Mode (Fake Adapter)
liveSearch Grouped live search (products and categories) as the Shopper types Yes Yes
upsells The product page’s curated upsell rail (WooCommerce “Upsells”) Yes. Ids come from a Companion Store API extension; without it the rail is empty and hides Yes
crossSells The product page’s cross-sell section and the cart’s “You may also like” rail Yes, same as upsells Yes
productPreview /api/preview/enter and the /preview/product/{id} route for editors Yes (Companion preview routes) Yes (demo-preview-<id> links)
cart Cart, mini cart, coupons, delivery rates Yes Yes
embeddedPayment Card and PayPal paid on the checkout page; otherwise offline methods only Yes, for gateways that have a Gateway Dialect Yes; off with FAKE_EMBEDDED_PAYMENT_OFF=1
expressCheckout Apple Pay and Google Pay buttons on product, cart and checkout (Link is wired but pending verification) Yes, where the Stripe plugin has wallets switched on Yes (demo wallet); off with the same flag
orders Order history and order detail in the account Only with WC REST keys Yes
accounts Account profile reads and writes Only with WC REST keys Yes
customerStats Lifetime order count and spend on the account overview Signed-in requests only (Companion JWT route) Yes
addressBook Saved addresses (up to 10) in the account and the checkout picker Signed-in requests only (Companion JWT route) Yes; off with FAKE_ADDRESS_BOOK_OFF=1
reviews Listing and submitting product reviews Yes Yes
reviewMedia Photo and video uploads on reviews Yes; an older Companion answers no policy and the picker hides Yes
reviewThreads Replies, helpful votes, reports and Seller Reply badges Yes Yes
wishlist A wishlist persisted on the backend for signed-in Shoppers Yes (guests keep a local list) Yes
stockNotifications “Notify me” back-in-stock sign-up and the account’s alert list Yes; the Store Owner’s master switch arrives with the theme Yes
geography Sell-to and ship-to country lists that filter the address forms Only with WC REST keys Yes
locations Country and state reference data for address forms Only with WC REST keys Yes
theme Backend-managed colours, header, footer and page settings Yes (Companion public settings) Yes
content Blog (Journal) and the home page’s journal highlights Yes (WP REST) Yes
identity Sign-in, registration, password and social flows Yes (Companion JWT endpoints) Yes (demo@retail.store)
multilingualContent Catalog and content answered in the request Locale Only when WOO_MULTILINGUAL_PARAM is set (WPML or Polylang behind it) Yes (Arabic overlay)

Some flags depend on the request, not only on the backend. On WooCommerce, customerStats and addressBook are true only for an adapter built with a signed-in customer’s token, and orders, accounts, geography and locations need the store’s WC REST consumer key and secret.

There are three mechanisms, and a feature uses whichever matches what it is.

  1. A flag is false. Code that would offer the feature checks the flag first and renders nothing. This excerpt is from features/cart/actions.ts:

    export async function getCartCrossSellsAction(productIds: string[]): Promise<ProductSummary[]> {
    if (productIds.length === 0) return [];
    const adapter = await getLocalizedAdapter();
    if (!adapter.capabilities.crossSells) return [];
    try {
    return await adapter.getCrossSellProducts(productIds, CART_CROSS_SELL_LIMIT);
    } catch (err) {
    console.warn(`[cart] cross-sell read failed: ${(err as Error).message}`);
    return [];
    }
    }
  2. A read answers null or empty. Where the answer depends on the item rather than the backend, there is no flag at all. Product.videos is an empty list for a product without videos (ADR 0011). Cart.freeShipping is absent whenever no truthful number exists (ADR 0009, free shipping). A Companion that predates a route answers null and the section hides.

  3. No fallback, by design. Without embeddedPayment the checkout offers the backend’s offline methods only; it never falls back to a redirect to a backend-hosted payment page (ADR 0022). Without expressCheckout there is no express button anywhere.

The app branches on these flags today: embeddedPayment, expressCheckout, addressBook, geography, customerStats, stockNotifications, reviewMedia, reviewThreads, productPreview, upsells, crossSells, identity and content. The others (liveSearch, cart, orders, accounts, reviews, wishlist, locations, theme, multilingualContent) gate sections of the contract suite. In the UI they degrade through the adapter’s answers.

Absence is tested, not assumed. Two CI jobs run the smoke suite with a capability switched off in the Fake Adapter:

CI job Environment Playwright tag What it proves
offline-only-checkout-smoke FAKE_EMBEDDED_PAYMENT_OFF=1 @offline-only Checkout offers offline methods only, and no express button appears
address-legacy-smoke FAKE_ADDRESS_BOOK_OFF=1 @legacy-addresses The legacy one-billing, one-shipping address pair still works

The contract suite is capability-gated the same way: a section an adapter does not declare is skipped, never failed, so a partial adapter runs a partial suite honestly.

Additive plugins activate on data presence

Section titled “Additive plugins activate on data presence”

Not every third-party plugin becomes a Capability. ADR 0009 splits them by what the plugin is:

Exclusive, or enriches existing reads Purely additive, competes with nothing
Example A Multivendor Plugin such as Dokan (designed, not built) PDF invoicing (“PDF Invoices and Packing Slips for WooCommerce”)
Packaging A separate package injected as a strategy (ADR 0007) A separate pure dialect package that adapter-woocommerce depends on directly
Activation Deployment configuration Data presence: no flag, no configuration
Capability flag Yes None

So an Order carries an invoice number and issue date only when the backend actually holds one, and the storefront shows a download only then. The document is never linked by URL from a page: it is fetched through the Port and streamed by the storefront. packages/adapter-woocommerce-invoice maps payloads and builds strings; the WooCommerce adapter owns the transport.

  1. Add the flag (with a doc comment saying what absence means) to CommerceCapabilities or DiscoveryCapabilities, and the operations to the matching Capability interface in commerce-core.
  2. Declare it in every adapter: the type checker refuses an adapter that leaves it out.
  3. Add a contract-suite section gated on the flag. The Fake Adapter must pass it; the WooCommerce adapter where it can be recorded.
  4. Branch on the flag where the UI would offer the feature, and add a smoke test for both the present and the absent case if absence changes what a Shopper sees.
  5. Name the concept in CONTEXT.md, and write an ADR if the decision is hard to reverse.
Storefront Playbook · Built by weLabsFeaturesFAQTalk to us