Build a storefront — the runbook
This runbook takes you from a blank Next.js app to a completed checkout against your Cartbase store. It is written to be followed by a developer or handed to a coding agent as-is. Each step names the domain doc that carries the full contracts (shapes, curls, error codes, settings). Follow the steps in order — later steps assume earlier wiring exists. If anything here is unclear or wrong, it's a documentation bug — report it.
What you need before starting (all three are in your Cartbase admin under Settings):
| Input | Example | Where it goes |
|---|---|---|
| API origin | https://admin.bitfar.co |
NEXT_PUBLIC_BARTER_URL |
| Store client id | 1e7a4c02-9b31-4f7e-8d2a-5c6f90ab12cd (uuid) |
NEXT_PUBLIC_BARTER_CLIENT_ID |
| Publishable API key | pk_… (optional, channel scope) |
NEXT_PUBLIC_BARTER_PUBLISHABLE_KEY |
Sanity-check the store before writing any code:
# The regions listing is the cheapest liveness + auth probe.
curl -sf "$BASE/api/store/regions" -H "x-client-id: $CLIENT_ID" | grep -q '"regions"'Step 1 — Scaffold + package
Create the Next.js app (App Router) and add the package:
# doc-noexec — scaffolding happens in YOUR repo, not against the API.
bunx create-next-app@latest my-store --ts --app --tailwind
cd my-store && bun add @barter/storefront- The package is source-shipped TypeScript — add
transpilePackages: ["@barter/storefront"]tonext.configor nothing from it will compile. - The Tailwind preset is Tailwind 3 format (
tailwind-preset.cjs).create-next-app --tailwindscaffolds Tailwind 4 (CSS-first, no config file) — either installtailwindcss@3.4+ atailwind.config.cjswithpresets: [require("@barter/storefront/tailwind-preset")](include the package source incontent:"./node_modules/@barter/storefront/src/**/*.{ts,tsx}"), or translate the preset's tokens into Tailwind 4@themeyourself. Then define the shadcn-standard token variables (--background,--primary, … as raw oklch channels) in your CSS — the working set isexamples/storefront/src/app/globals.css. - Pin the package version — storefronts never float
latest. - Monorepo caveat: when the app lives in a workspace, set
outputFileTracingRootinnext.config— Next infers the root from the nearest stray lockfile, and a wrong root silently breaks page-segment hydration in dev (buttons render but nothing responds).
Step 2 — The client seam
Construct ONE StorefrontClient per scope and pass it to every SDK call
(all SDK functions take the client as first argument — see any domain doc's
SDK line):
- Server (RSC, server actions): construct per request;
getAuthTokenreads the customer session cookie;getLocalereads the locale cookie. - Browser: construct once; token from your session store.
Auth model (recap — full detail in auth.md):
x-client-id always; x-publishable-api-key when the store gave you one
(it scopes the catalog and carts to the key's sales channels);
authorization: Bearer <jwt> once a customer is logged in.
CORS (bites every browser client): the store API sends NO CORS
headers today, so browser-side SDK calls (cart drawer mutations, the
whole checkout orchestration) only work same-origin. Unless your
storefront is served from the Cartbase deployment origin itself, proxy the
store surface through your own origin — in Next.js one rewrite does it —
and give the BROWSER client window.location.origin as baseUrl
(server-side calls hit NEXT_PUBLIC_BARTER_URL directly and are
unaffected; see examples/storefront/next.config.ts for the working
rewrite):
// next.config.ts — proxy browser SDK traffic to the API origin
async rewrites() {
return [{ source: "/api/store/:path*",
destination: `${process.env.NEXT_PUBLIC_BARTER_URL}/api/store/:path*` }]
}Step 3 — Store configuration bootstrap
Fetch once at layout level, cache per the docs' cache headers:
- regions.md — regions (→ region_id for pricing + payment providers), currencies, supported locales.
- integrations.md —
GET /api/store/integrations: which carriers are enabled (pickup points/lockers for checkout), COD fee presence, and the tracking block (pixel/GA4/GTM/ads public ids). - consent.md — the CMP config for the consent banner.
Step 4 — Layout: consent, tracking, navigation
Order inside <body> matters (contracts in components.md
and consent.md):
<ConsentInit>FIRST child of body — static and synchronous, never awaits a fetch (first-hit consent race otherwise).- Tracking mounts (
<MetaPixel>,<GA4>, GTM) — gated on the consent state and fed by the integrations tracking block, never by env vars. - Navigation from menus.md —
main-menu/footerhandles; an unknown handle 404s and must render as "no nav", never crash.
Step 5 — Catalog
- products.md — listing + PDP. Always pass a pricing context
(
currency_codeorregion_id) or prices come back undecorated; rendervariant.calculated_price, fall back to baseprices[]. Never cache acalculated_priceresponse shared when a customer JWT was present — prices vary by customer group. - collections.md — collection pages use the membership
endpoint (
/collections/:id/products) which honors the admin's sort. - categories.md — category tree, tags, types.
- search.md — search page:
q+ facets from the response (render buckets, apply via the documented query params), typo-tolerant, synonym-aware. Related products on the PDP come from the same doc. - SEO:
seo_title/seo_descriptionfields with title/description fallbacks; path conventions are/products/<handle>,/collections/<handle>,/categories/<handle>.
Step 6 — Content
- content.md —
/pages/<handle>and/blogs/<handle>routes; body HTML is server-sanitized, safe to render raw. Every store seeds policy pages (privacy-policy,terms-of-service,refund-policy,shipping-policy) — link them in the footer. - metaobjects.md — merchant-defined content (size charts via the product→metafield→metaobject chain).
- redirects.md — call ONLY from your
not-foundhandler; 301 whento_pathis non-null. Never on regular page loads.
Step 7 — Cart
carts.md: create the cart lazily on first add-to-cart with the
region + (optionally) sales channel; persist cart.id in a cookie; all cart
mutations return the decorated cart — totals are SERVER truth, render them
verbatim, never compute client-side. Line items, quantity updates, deletes,
and the customer-attach call after login are all in that doc.
gift-cards.md: the apply/remove endpoints + the three
decoration fields (gift_cards[], gift_card_total,
gift_card_remainder) your summary UI must render.
Step 8 — Checkout
checkout.md is the authoritative sequence. In brief:
- List shipping options and payment providers with
cart_id— the server filters both through the merchant's checkout rules; your UI never hides methods on its own. - Carrier pickers (office/locker) come from the integrations config
(integrations.md); the chosen point goes into
carrier_metadata. - Buy click =
prepare-checkout(ONE call: address + shipping method + payment session at the final amount) → for card: StripeconfirmPayment(client_secret)→complete. For COD:completedirectly. Gift-card-covered carts skip the provider entirely. - Handle the documented failure codes (
checkout_method_hidden,account_required,gift_card_insufficient_balance, cart-vs-order union on complete) — each has a UI recovery path described in the doc. sync-payment-amountafter any total-changing edit on the payment step;refresh-payment-if-terminalonly from Elementsloaderror/ aged-cart mount.- Order confirmation renders from orders.md
(
/orders/display/:displayIdembeds items, fulfillments, tracking).
Step 9 — Customer accounts
auth.md: passwordless email-code login (request → verify →
Bearer session). customers.md: profile, addresses,
order history, issued documents (invoices). Respect accounts_mode
(store setting): required blocks guest checkout with 403 account_required; disabled means render no account UI at all. Gate B2B
content on customer.account_status === "approved".
Step 10 — Reviews
reviews.md: the PDP widget (/reviews/widget — aggregate +
first page + display options in one call) and the token wizard route for
email CTA links (/review/<token>): validate token → submit rating+body →
optional photo step mints the reward code. Resume rules are in the doc.
Step 11 — Tracking events
components.md tracking section: fire client events via the
package helpers; Purchase MUST use eventID = "purchase_" + order.display_id so Meta dedupes browser Pixel against the server CAPI
event; write the attribution keys (fbp/fbc/anon-id/ga session) into
cart.metadata (consent-gated) so server events inherit them.
Step 12 — Go-live checklist
- Pricing context passed on every catalog surface; no shared caching of JWT-priced responses.
- Consent banner renders for an unconfigured store (defaults are server-applied); tags mount only behind consent.
- Checkout completes: card (Stripe live), COD (fee renders from config), gift card (partial + full cover).
- Not-found handler consults redirects; policy pages linked.
- Order confirmation + emails render totals identical to the cart.
- Locale switching keeps cart + session; EUR everywhere.
- The store's publishable key (if any) is set — B2B catalogs are key-scoped and look "missing products" without it.