@storefront/client — the one door to the API, in every kit and every app we ship.
Developers / Headless
Your own storefront. Our commerce. Any framework.
One typed client to the API and two framework kits — Vue and React — carry the whole shop: catalogue, cart, checkout, gift cards, markets, reviews, search, and the customer's account. The website builder is one way to use them, not the way.
storefront-vue and storefront-react, kept in parity by a failing build.
Exported from the root of both kits today — the list below is checked against the barrels.
The builder is optional
Everything on this page runs with no builder site published. The hosted site and the code export are built from the same kits and the same client a headless app uses — the builder's sections are thin adapters over kit components, so a feature that ships for the builder ships for you, with the same test ids and the same API calls.
That is not a promise: the same end-to-end journeys — gift cards, markets, checkout rules, reviews, search — run against a published builder site and against a plain Nuxt app on the Vue kit, on every commit.
Install
Vue / Nuxt
npm install @storefront/client storefront-vue piniaReact / Next
npm install @storefront/client storefront-react zustandThe kits are peer-light: Vue needs Pinia, React needs one store library — zustand out of the box, Redux Toolkit through storefront-react/rtk. Stripe's JS loads only where a checkout mounts.
The client
A CustomerClient speaks the shopper half of the API: public reads, the cart, checkout, orders, reviews and gift cards. Every route the storefront kits call goes through it, so the surface you see here is the surface they use.
import { CustomerClient } from '@storefront/client';
const client = new CustomerClient({ baseUrl: 'https://api.your-platform.example' });
// The store's public data (settings, categories, markets) and its live catalogue.
const store = await client.store.getPublicData(STORE_ID);
const products = await client.store.getAvailableProducts(STORE_ID, { market: 'eu' });
// Everything else a shopper does goes through the same client:
// client.cart.*, client.checkout.*, client.orders.*, client.reviews.*, client.giftCards.*Anonymous shoppers are identified by a cookie the API sets on first contact; a sign-in upgrades the same cart. Nothing here needs an API key — that is the admin half, documented under API.
Vue and Nuxt
One plugin object fills the Pinia stores before first paint — the store's settings, the shopper's session, the active cart, the market the request resolves to — and every component on the kit reads from those stores unless you hand it props.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
runtimeConfig: {
// Server-side reads (SSR) — private, may point at an internal address.
storefrontPlugin: { baseUrl: 'http://127.0.0.1:9147' },
public: {
storefrontPlugin: {
baseUrl: 'https://api.your-platform.example',
storeOptions: { storeId: 42, autoGetActiveCart: true },
},
},
},
});
// plugins/storefront.ts — fetches the store, restores the shopper's session and cart,
// resolves ?market= / the wb_market cookie, and fills the Pinia stores before first paint.
import { nuxtClientPluginObject } from 'storefront-vue';
export default defineNuxtPlugin(nuxtClientPluginObject);A page is then composition: the catalogue, a summary that prints the numbers the API will charge, and a checkout that completes an order. This is the shape apps/noop.shop, our reference storefront, runs in production.
<script setup lang="ts">
import { ProductList, BasicShopProduct, Checkout, CheckoutOrderSummary, useCartStore, useClient } from 'storefront-vue';
const cart = useCartStore();
const client = useClient();
// The store is the page's state; the client is the one door to the API.
const onAdd = async (line) => { cart.addProduct(line); await client.cart.update(cart.cartDTO!); };
const onPlaced = (order) => navigateTo(`/order/${order.id}`);
</script>
<template>
<ProductList>
<template #product="attrs">
<BasicShopProduct v-bind="attrs" @add="onAdd" />
</template>
</ProductList>
<CheckoutOrderSummary />
<Checkout :provider="{ name: 'stripe' }" currency="usd" @payment-success="onPlaced" />
</template>React and Next
Two providers — the client and the stores — and one call that fills the stores. The components are the same ones, with the same props and the same test ids, so a journey written against the Vue kit reads the same against this one.
import { CustomerClient } from '@storefront/client';
import { createZustandStores } from 'storefront-react/zustand';
import {
StorefrontClientProvider, StorefrontStoreProvider, initializeStores,
ProductList, CheckoutOrderSummary, Checkout,
} from 'storefront-react';
const client = new CustomerClient({ baseUrl: 'https://api.your-platform.example' });
const stores = createZustandStores();
await initializeStores({ client, stores, storeOptions: { storeId: 42, autoGetActiveCart: true } });
export function Shop() {
return (
<StorefrontClientProvider client={client}>
<StorefrontStoreProvider stores={stores}>
<ProductList columns={3} />
<CheckoutOrderSummary />
<Checkout provider={{ name: 'stripe' }} currency="usd" onPaymentSuccess={(order) => location.assign(`/order/${order.id}`)} />
</StorefrontStoreProvider>
</StorefrontClientProvider>
);
}The stores are an interface: zustand is the default adapter, Redux Toolkit ships alongside, and either can be swapped for your own by implementing StorefrontStores.
Design tokens
A storefront paints from CSS custom properties: the page ground, text, fonts, form fields and the content column. A published builder site emits its theme as these names and reads them in its base stylesheet, and the kits read the same names, so setting them on :root themes both.
--wb-color-*—background,surface,text,muted,border,primary,on-primary,field,field-border,error,success--wb-font-*—body,heading,mono,size,line-height--wb-spacing-*—container,medium,narrow,gutter--wb-radius-*—sm,md,lg,full
Every name has a default, and every component keeps the value it always had as its fallback — a storefront that sets none renders exactly as it does today.
A published builder site also serves these families from its own origin, each with a metric-matched fallback face so text never changes size after it loads: Outfit, Manrope, Space Grotesk
What both kits export
Grouped by the job a shop has to do. Every name here is exported from the root of both kits — the page is built from the same list a test checks against the packages.
Catalogue
Listing, the product card with its option selection, the detail view, images and money.
ProductListBasicShopProductProductDetailImageViewerMoneyDisplay
Cart
The cart page, its lines, the header dropdown and drawer, subscriptions on a line.
CartViewerCartItemCartDropdownCartDrawerQuickCartViewCartSubscribeButton
Checkout
A provider-agnostic checkout, the summary with rules, tax and gift-card tender, the steps header, and the no-charge path when gift cards cover the order.
CheckoutCheckoutOrderSummaryCheckoutStepsOrderSummaryGiftCardInputGiftCardCoveredOrder
Commerce
Markets, reviews, search, the customer's gift cards, the rules an order carried, and shipping progress and updates.
MarketSwitcherProductReviewsSearchBoxSearchResultsMyGiftCardsOrderRulesAppliedShippingProgressShippingUpdate
Engagement
Back-in-stock notifications, the contact form, confirmation dialogues.
RestockNotifyContactFormConfirmationDialogue
Extensions
Renders extension-authored UI safely inside your own pages.
ExtensionUIRenderer
What the platform does for you underneath
- Checkout: Stripe today behind a provider seam, with tax estimated by the API for the address the shopper types and a total that is words, not a number, until it equals what will be charged.
- Checkout rules: the merchant's own discount, shipping and validation code — TypeScript authored through the admin API (POST /store/:storeId/checkout-rules) or the admin portal, compiled by the platform and run on the API at estimate and at order time, with or without the site builder. Your summary shows the rule's words, your order page prints what was applied. A validation rule can be stored with failClosed: true to block checkout when it cannot run — an outage then refuses what the rule exists to refuse (the estimate and the checkout's 400 carry code CHECKOUT_RULE_UNAVAILABLE per error) instead of selling past it; every other rule has no effect when it cannot run.
- Gift cards: balance checks, partial tender, the fully-covered order with no card form, and the customer's cards on their account page.
- Markets: a market resolves from ?market= or a cookie and rides every public read, so prices, currency and rounding follow the shopper.
- Reviews: guest submission, moderation on the merchant's side, and the aggregate rating in your product JSON-LD.
- Search: the store's own engine behind GET /store/:storeId/search — diacritic folding, typo tolerance, the merchant's synonym groups and products, collections, pages and stories ranked together, with hits addressed to YOUR routes. Want search-as-you-type without a request per keystroke? GET /store/:storeId/search/index hands you the whole corpus with a content-hash version, and `createSearchCache` in @storefront/client keeps its manifest in localStorage and its documents in IndexedDB, so the second page load asks for nothing at all. The corpus route is for catalogues that FIT — a document is about 310 bytes plus 400 characters of text, so a few thousand products is a megabyte or two and the api refuses to serve more than 8 MB; above that `createSearchCache` stays on the query route by itself. You pick the matching per store — fuzzy (prefixes plus typos), prefix, substring, or exact — and the api, the cached corpus and both kits apply the same rule. The cache degrades in the open: a storage tier that throws (quota, a private window, a closed database) is written off for the page and named in `status().degraded`, a corpus download that fails backs off for 30 s while the query route answers, a stale corpus answers first and revalidates behind it, and a superseded keystroke's request is aborted, not just ignored.
- Navigation: the merchant's menus and URL redirects are public reads (GET /store/:storeId/menus, GET /store/:storeId/redirects), so a headless site renders the navigation the merchant manages and honours the same redirects the builder-built site would.
- Account: orders, subscriptions, gift cards and addresses through the same client, with sign-in on the store's own customer auth.
Beyond the web kits
The same shopper surface ships as native clients for game engines and mobile — one package per language, each hand-written against the API and kept in sync by a build gate on the served OpenAPI surface.
- C++
- Go
- .NET
- Java
- Python
- Swift
- Godot
Every route, with the audience it serves, is on the API reference ; what happens after the order is on Webhooks .