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.
The flags
Section titled “The flags”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.
How the storefront degrades
Section titled “How the storefront degrades”There are three mechanisms, and a feature uses whichever matches what it is.
-
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 [];}} -
A read answers null or empty. Where the answer depends on the item rather than the backend, there is no flag at all.
Product.videosis an empty list for a product without videos (ADR 0011).Cart.freeShippingis absent whenever no truthful number exists (ADR 0009, free shipping). A Companion that predates a route answers null and the section hides. -
No fallback, by design. Without
embeddedPaymentthe checkout offers the backend’s offline methods only; it never falls back to a redirect to a backend-hosted payment page (ADR 0022). WithoutexpressCheckoutthere 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.
Proving absence
Section titled “Proving absence”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.
Adding a capability
Section titled “Adding a capability”- Add the flag (with a doc comment saying what absence means) to
CommerceCapabilitiesorDiscoveryCapabilities, and the operations to the matching Capability interface incommerce-core. - Declare it in every adapter: the type checker refuses an adapter that leaves it out.
- Add a contract-suite section gated on the flag. The Fake Adapter must pass it; the WooCommerce adapter where it can be recorded.
- 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.
- Name the concept in
CONTEXT.md, and write an ADR if the decision is hard to reverse.