# 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:

```bash
# 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:

```bash
# 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 @cartbase/storefront
```

- The package is **source-shipped TypeScript** — add
  `transpilePackages: ["@cartbase/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("@cartbase/storefront/tailwind-preset")]` (include the
  package source in `content`:
  `"./node_modules/@cartbase/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](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):

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

## Step 4 — Layout: consent, tracking, navigation

Order inside `<body>` matters (contracts in [components.md](components.md)
and [consent.md](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.md](menus.md) — `main-menu` / `footer` handles;
   an unknown handle 404s and must render as "no nav", never crash.

## Step 5 — Catalog

- [products.md](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](collections.md) — collection pages use the membership
  endpoint (`/collections/:id/products`) which honors the admin's sort.
- [categories.md](categories.md) — category tree, tags, types.
- [search.md](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](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](metaobjects.md) — merchant-defined content (size charts
  via the product→metafield→metaobject chain).
- [redirects.md](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](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](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](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](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.md)
   (`/orders/display/:displayId` embeds items, fulfillments, tracking).

## Step 9 — Customer accounts

[auth.md](auth.md): passwordless email-code login (request → verify →
Bearer session). [customers.md](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](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](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.

## Step 13 — Ship it

[deploy.md](deploy.md): send the app to Cartbase hosting — permanent
preview URL on every deploy, **Publish** to go live, deploy history as
rollback. Hosted storefronts get the three inputs above injected
automatically, so there is nothing to configure.
