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"] to next.config or nothing from it will compile.
  • The Tailwind preset is Tailwind 3 format (tailwind-preset.cjs). create-next-app --tailwind scaffolds Tailwind 4 (CSS-first, no config file) — either install tailwindcss@3.4 + a tailwind.config.cjs with presets: [require("@barter/storefront/tailwind-preset")] (include the package source in content: "./node_modules/@barter/storefront/src/**/*.{ts,tsx}"), or translate the preset's tokens into Tailwind 4 @theme yourself. Then define the shadcn-standard token variables (--background, --primary, … as raw oklch channels) in your CSS — the working set is examples/storefront/src/app/globals.css.
  • Pin the package version — storefronts never float latest.
  • Monorepo caveat: when the app lives in a workspace, set outputFileTracingRoot in next.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; getAuthToken reads the customer session cookie; getLocale reads 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:

  1. regions.md — regions (→ region_id for pricing + payment providers), currencies, supported locales.
  2. integrations.mdGET /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).
  3. consent.md — the CMP config for the consent banner.

Order inside <body> matters (contracts in components.md and consent.md):

  1. <ConsentInit> FIRST child of body — static and synchronous, never awaits a fetch (first-hit consent race otherwise).
  2. Tracking mounts (<MetaPixel>, <GA4>, GTM) — gated on the consent state and fed by the integrations tracking block, never by env vars.
  3. Navigation from menus.mdmain-menu / footer handles; 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_code or region_id) or prices come back undecorated; render variant.calculated_price, fall back to base prices[]. Never cache a calculated_price response 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_description fields 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-found handler; 301 when to_path is 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:

  1. 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.
  2. Carrier pickers (office/locker) come from the integrations config (integrations.md); the chosen point goes into carrier_metadata.
  3. Buy click = prepare-checkout (ONE call: address + shipping method + payment session at the final amount) → for card: Stripe confirmPayment(client_secret)complete. For COD: complete directly. Gift-card-covered carts skip the provider entirely.
  4. 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.
  5. sync-payment-amount after any total-changing edit on the payment step; refresh-payment-if-terminal only from Elements loaderror / aged-cart mount.
  6. Order confirmation renders from orders.md (/orders/display/:displayId embeds 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.