create-cartbase 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -68,9 +68,9 @@ as flags they are written into .env.local, otherwise placeholders are.`);
68
68
  const key = typeof flags.get("key") === "string" ? flags.get("key") : "";
69
69
  fs.writeFileSync(path.join(target, ".env.local"), [
70
70
  "# Your store's inputs — Cartbase admin, Settings.",
71
- `NEXT_PUBLIC_BARTER_URL=${url}`,
72
- `NEXT_PUBLIC_BARTER_CLIENT_ID=${clientId}`,
73
- `NEXT_PUBLIC_BARTER_PUBLISHABLE_KEY=${key}`,
71
+ `NEXT_PUBLIC_CARTBASE_URL=${url}`,
72
+ `NEXT_PUBLIC_CARTBASE_CLIENT_ID=${clientId}`,
73
+ `NEXT_PUBLIC_CARTBASE_PUBLISHABLE_KEY=${key}`,
74
74
  "",
75
75
  ].join("\n"));
76
76
  const filled = url && clientId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-cartbase",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Scaffold a Cartbase storefront: npm create cartbase my-store",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,9 +12,9 @@ What you need before starting (all three are in your Cartbase admin under
12
12
 
13
13
  | Input | Example | Where it goes |
14
14
  |---|---|---|
15
- | API origin | `https://admin.bitfar.co` | `NEXT_PUBLIC_BARTER_URL` |
16
- | Store client id | `1e7a4c02-9b31-4f7e-8d2a-5c6f90ab12cd` (uuid) | `NEXT_PUBLIC_BARTER_CLIENT_ID` |
17
- | Publishable API key | `pk_…` (optional, channel scope) | `NEXT_PUBLIC_BARTER_PUBLISHABLE_KEY` |
15
+ | API origin | `https://admin.bitfar.co` | `NEXT_PUBLIC_CARTBASE_URL` |
16
+ | Store client id | `1e7a4c02-9b31-4f7e-8d2a-5c6f90ab12cd` (uuid) | `NEXT_PUBLIC_CARTBASE_CLIENT_ID` |
17
+ | Publishable API key | `pk_…` (optional, channel scope) | `NEXT_PUBLIC_CARTBASE_PUBLISHABLE_KEY` |
18
18
 
19
19
  Sanity-check the store before writing any code:
20
20
 
@@ -73,7 +73,7 @@ whole checkout orchestration) only work same-origin. Unless your
73
73
  storefront is served from the Cartbase deployment origin itself, proxy the
74
74
  store surface through your own origin — in Next.js one rewrite does it —
75
75
  and give the BROWSER client `window.location.origin` as `baseUrl`
76
- (server-side calls hit `NEXT_PUBLIC_BARTER_URL` directly and are
76
+ (server-side calls hit `NEXT_PUBLIC_CARTBASE_URL` directly and are
77
77
  unaffected; see `examples/storefront/next.config.ts` for the working
78
78
  rewrite):
79
79
 
@@ -81,7 +81,7 @@ rewrite):
81
81
  // next.config.ts — proxy browser SDK traffic to the API origin
82
82
  async rewrites() {
83
83
  return [{ source: "/api/store/:path*",
84
- destination: `${process.env.NEXT_PUBLIC_BARTER_URL}/api/store/:path*` }]
84
+ destination: `${process.env.NEXT_PUBLIC_CARTBASE_URL}/api/store/:path*` }]
85
85
  }
86
86
  ```
87
87
 
@@ -1,76 +1,77 @@
1
- # docs/storefront/ — the bulletproof storefront docs
2
-
3
- **Audience: an AGENT building a storefront from a blank Next.js app.** These
4
- docs are the entire knowledge transfer — every call shape, every curl, every
5
- setting, every component contract. If a storefront can't be built from these
6
- files alone, the fix is a doc fix, never tribal knowledge.
7
-
8
- Start at **[BUILD-A-STOREFRONT.md](BUILD-A-STOREFRONT.md)** — the runbook.
9
- Domain files below are its reference chapters.
10
-
11
- ## Executable-docs contract (docs-truth CI)
12
-
13
- Docs that can lie aren't bulletproof, so every ```bash block in every file
14
- here is **extracted and executed** against the real test server by
15
- `tests/docs/storefront-curls.test.ts`. A drifted doc FAILS the build.
16
-
17
- Rules for doc authors (agents included):
18
-
19
- 1. All ```bash blocks in one file form ONE script, executed top-to-bottom
20
- with `bash -euo pipefail`. Later blocks may use variables exported by
21
- earlier blocks (`CART_ID=$(curl … | grep -o …)`).
22
- 2. The harness pre-exports: `BASE` (test-server origin), `CLIENT_ID` (dev
23
- tenant id), `PUBLISHABLE_KEY` (the B2B-channel dev key — channel-scoped,
24
- use only where the doc discusses key scoping).
25
- 3. Every curl uses `-sf` (silent + fail-on-HTTP-error) unless the block
26
- demonstrates an error case — then capture the status explicitly
27
- (`-o /dev/null -w '%{http_code}'`) and assert it (`test "$STATUS" = 404`).
28
- 4. Assert shape, not just liveness: pipe to `grep -q '"key"'` (or `node -e`
29
- for anything structural). A block that checks nothing proves nothing.
30
- 5. A block that must NOT run (illustrative only, external side effects)
31
- starts with `# doc-noexec` on its first line. Use sparingly — every
32
- noexec block is a hole in the truth gate.
33
- 6. Blocks must be idempotent-safe on the shared dev tenant: create what you
34
- read, suffix names with `$RUN` (pre-exported unique stamp), and clean up
35
- in a final block when you created durable rows.
36
-
37
- ## Per-domain file format
38
-
39
- One file per domain. For each endpoint, in order:
40
-
41
- - **Purpose** — one sentence, when a storefront calls it.
42
- - **Auth** — which headers (anon `x-client-id` / publishable key / Bearer).
43
- - **Request** — method, path, query/body shape (jsonc block).
44
- - **Response** — shape (jsonc block), with field notes.
45
- - **Working curl** — executable per the contract above.
46
- - **Errors** — status + `code` for every contract-listed failure.
47
- - **SDK** — the `@cartbase/storefront/api` function that wraps it.
48
- - **Components** — which `@cartbase/storefront` UI components consume it.
49
- - **Settings** — admin settings that change its behavior (checkout rules,
50
- locales, consent, accounts mode…).
51
-
52
- ## Files
53
-
54
- | File | Domain |
55
- |---|---|
56
- | [BUILD-A-STOREFRONT.md](BUILD-A-STOREFRONT.md) | The agent runbook — blank app → completed checkout |
57
- | [products.md](products.md) | Products, variants, pricing context |
58
- | [search.md](search.md) | Search, facets, related products |
59
- | [collections.md](collections.md) | Collections + membership listings |
60
- | [categories.md](categories.md) | Categories, tags, types |
61
- | [regions.md](regions.md) | Regions, currencies, locales |
62
- | [carts.md](carts.md) | Cart lifecycle + line items |
63
- | [gift-cards.md](gift-cards.md) | Gift-card tender on carts |
64
- | [checkout.md](checkout.md) | Shipping options, payment providers/collections, prepare-checkout orchestration, complete |
65
- | [orders.md](orders.md) | Order reads, display-id lookup, transfers |
66
- | [customers.md](customers.md) | Customer profile, addresses, documents |
67
- | [subscriptions.md](subscriptions.md) | Subscription portal: schedule control, contract edits, payment-method recovery |
68
- | [auth.md](auth.md) | Passwordless code login + session discipline |
69
- | [content.md](content.md) | Pages + blogs |
70
- | [menus.md](menus.md) | Navigation menus |
71
- | [metaobjects.md](metaobjects.md) | Merchant-defined content types |
72
- | [reviews.md](reviews.md) | Review widget, token wizard, photo rewards |
73
- | [integrations.md](integrations.md) | Store config: carriers, COD, tracking block, lockers |
74
- | [consent.md](consent.md) | Consent Mode v2 banner config |
75
- | [redirects.md](redirects.md) | 404-path URL redirects |
76
- | [components.md](components.md) | UI component families: contracts + required SDK calls |
1
+ # docs/storefront/ — the bulletproof storefront docs
2
+
3
+ **Audience: an AGENT building a storefront from a blank Next.js app.** These
4
+ docs are the entire knowledge transfer — every call shape, every curl, every
5
+ setting, every component contract. If a storefront can't be built from these
6
+ files alone, the fix is a doc fix, never tribal knowledge.
7
+
8
+ Start at **[BUILD-A-STOREFRONT.md](BUILD-A-STOREFRONT.md)** — the runbook.
9
+ Domain files below are its reference chapters.
10
+
11
+ ## Executable-docs contract (docs-truth CI)
12
+
13
+ Docs that can lie aren't bulletproof, so every ```bash block in every file
14
+ here is **extracted and executed** against the real test server by
15
+ `tests/docs/storefront-curls.test.ts`. A drifted doc FAILS the build.
16
+
17
+ Rules for doc authors (agents included):
18
+
19
+ 1. All ```bash blocks in one file form ONE script, executed top-to-bottom
20
+ with `bash -euo pipefail`. Later blocks may use variables exported by
21
+ earlier blocks (`CART_ID=$(curl … | grep -o …)`).
22
+ 2. The harness pre-exports: `BASE` (test-server origin), `CLIENT_ID` (dev
23
+ tenant id), `PUBLISHABLE_KEY` (the B2B-channel dev key — channel-scoped,
24
+ use only where the doc discusses key scoping).
25
+ 3. Every curl uses `-sf` (silent + fail-on-HTTP-error) unless the block
26
+ demonstrates an error case — then capture the status explicitly
27
+ (`-o /dev/null -w '%{http_code}'`) and assert it (`test "$STATUS" = 404`).
28
+ 4. Assert shape, not just liveness: pipe to `grep -q '"key"'` (or `node -e`
29
+ for anything structural). A block that checks nothing proves nothing.
30
+ 5. A block that must NOT run (illustrative only, external side effects)
31
+ starts with `# doc-noexec` on its first line. Use sparingly — every
32
+ noexec block is a hole in the truth gate.
33
+ 6. Blocks must be idempotent-safe on the shared dev tenant: create what you
34
+ read, suffix names with `$RUN` (pre-exported unique stamp), and clean up
35
+ in a final block when you created durable rows.
36
+
37
+ ## Per-domain file format
38
+
39
+ One file per domain. For each endpoint, in order:
40
+
41
+ - **Purpose** — one sentence, when a storefront calls it.
42
+ - **Auth** — which headers (anon `x-client-id` / publishable key / Bearer).
43
+ - **Request** — method, path, query/body shape (jsonc block).
44
+ - **Response** — shape (jsonc block), with field notes.
45
+ - **Working curl** — executable per the contract above.
46
+ - **Errors** — status + `code` for every contract-listed failure.
47
+ - **SDK** — the `@cartbase/storefront/api` function that wraps it.
48
+ - **Components** — which `@cartbase/storefront` UI components consume it.
49
+ - **Settings** — admin settings that change its behavior (checkout rules,
50
+ locales, consent, accounts mode…).
51
+
52
+ ## Files
53
+
54
+ | File | Domain |
55
+ |---|---|
56
+ | [BUILD-A-STOREFRONT.md](BUILD-A-STOREFRONT.md) | The agent runbook — blank app → completed checkout |
57
+ | [products.md](products.md) | Products, variants, pricing context |
58
+ | [search.md](search.md) | Search, facets, related products |
59
+ | [collections.md](collections.md) | Collections + membership listings |
60
+ | [categories.md](categories.md) | Categories, tags, types |
61
+ | [regions.md](regions.md) | Regions, currencies, locales |
62
+ | [carts.md](carts.md) | Cart lifecycle + line items |
63
+ | [gift-cards.md](gift-cards.md) | Gift-card tender on carts |
64
+ | [checkout.md](checkout.md) | Shipping options, payment providers/collections, prepare-checkout orchestration, complete |
65
+ | [orders.md](orders.md) | Order reads, display-id lookup, transfers |
66
+ | [customers.md](customers.md) | Customer profile, addresses, documents |
67
+ | [subscriptions.md](subscriptions.md) | Subscription portal: schedule control, contract edits, payment-method recovery |
68
+ | [auth.md](auth.md) | Passwordless code login + session discipline |
69
+ | [content.md](content.md) | Pages + blogs |
70
+ | [menus.md](menus.md) | Navigation menus |
71
+ | [metaobjects.md](metaobjects.md) | Merchant-defined content types |
72
+ | [reviews.md](reviews.md) | Review widget, token wizard, photo rewards |
73
+ | [integrations.md](integrations.md) | Store config: carriers, COD, tracking block, lockers |
74
+ | [consent.md](consent.md) | Consent Mode v2 banner config |
75
+ | [redirects.md](redirects.md) | 404-path URL redirects |
76
+ | [components.md](components.md) | UI component families: contracts + required SDK calls |
77
+ | [platform.md](platform.md) | Platform fingerprints: generator meta, window.Cartbase, x-cartbase-version header, cart cookie naming |
@@ -305,11 +305,11 @@ curl -sf -X DELETE "$BASE/api/store/carts/$CART_ID/line-items/$LINE_ID" \
305
305
  cart promotion and recomputes totals (automatic promotions layer in on
306
306
  their own — only coded ones travel through here).
307
307
  - **Auth** — anon `x-client-id`.
308
- - **Request** — `{promo_codes: string[]}` on BOTH verbs (Medusa reads the
309
- DELETE body, not query).
308
+ - **Request** — `{promo_codes: string[]}` on BOTH verbs (the DELETE reads
309
+ its body, not query params).
310
310
  - **Response** — `200 {cart}` (decorated; `cart.promotions` is the raw
311
311
  pivot embed `[{promotion: {...}}]`). Unknown codes on REMOVE silently
312
- no-op (Medusa parity); on ADD they error.
312
+ no-op by design; on ADD they error.
313
313
  - **Errors** — 404 `cart_not_found`; 404 `promotion_not_found` (unknown
314
314
  code on add), 400 `promotion_inactive` (draft/expired code on add), 400
315
315
  zod (empty array / non-string entries).
@@ -35,7 +35,7 @@ While checkout stays mounted: `syncPaymentAmount()` after anything that
35
35
  changes the total; `refreshPaymentIfTerminal()` from Stripe Elements
36
36
  `loaderror` (never proactively).
37
37
 
38
- **Manual (Medusa-style, for custom flows):** `updateCart` (address+email) →
38
+ **Manual (step-by-step, for custom flows):** `updateCart` (address+email) →
39
39
  `addShippingMethod` → `createPaymentCollection` → `initiatePaymentSession`
40
40
  → `completeCart`. Both paths are executed against the live server below.
41
41
 
@@ -430,7 +430,7 @@ curl -sf -X POST "$BASE/api/store/carts/$CART_ID/refresh-payment-if-terminal" \
430
430
 
431
431
  ---
432
432
 
433
- ## Manual path — collections + sessions (Medusa-style)
433
+ ## Manual path — collections + sessions (step-by-step)
434
434
 
435
435
  ### POST /api/store/payment-collections
436
436
 
@@ -195,7 +195,7 @@ format.
195
195
  - `lib/get-product-price` — `getProductPrice({product, variantId})` /
196
196
  `getPricesForVariant` → formatted `VariantPrice` (cheapest + selected)
197
197
  from server-computed `calculated_price`. Reads Cartbase's flat
198
- `price_list_type` with the Medusa nested shape as fallback. Affected by:
198
+ `price_list_type` with a legacy nested shape as fallback. Affected by:
199
199
  price lists / B2B pricing context (what `calculated_price` contains),
200
200
  currency + region query context.
201
201
  - `lib/get-percentage-diff` — sale badge math.
@@ -206,11 +206,11 @@ format.
206
206
  authority for paginated listings.
207
207
  - `lib/payment-constants` — `isStripeLike` / `isPaypal` / `isManual` +
208
208
  `paymentInfoMap`. Translated to Cartbase ids: matches `pp_stripe` exactly
209
- (code truth `src/lib/stripe/providers.ts`) plus the Medusa-era prefixes;
209
+ (code truth `src/lib/stripe/providers.ts`) plus legacy provider prefixes;
210
210
  `pp_system_default` = COD. Affected by: enabled payment providers +
211
211
  checkout rules (which ids ever reach the client).
212
212
  - `lib/store-api-error` — `storeApiError(err)`: display-boundary normalizer
213
- (capitalized message + terminal period), successor of `medusaError`.
213
+ (capitalized message + terminal period).
214
214
  Catch `StoreApiError` directly instead when branching on `status`/`code`.
215
215
  - `lib/hooks/use-intersection`, `lib/hooks/use-toggle-state` — viewport +
216
216
  toggle micro-hooks (client).
@@ -303,7 +303,7 @@ code-first through the error-copy maps, never raw API strings.
303
303
  `StripeElementsScope` mounts `<Elements mode:"payment">` where needed
304
304
  (`passthrough` renders children scope-less on COD-only stores);
305
305
  `StripeContext` boolean = "Stripe.js ready".
306
- - **SDK calls** — none (env: `NEXT_PUBLIC_STRIPE_KEY`, Medusa-era names
306
+ - **SDK calls** — none (env: `NEXT_PUBLIC_STRIPE_KEY`; legacy env names
307
307
  kept as fallbacks).
308
308
  - **Mount rules** — `PaymentWrapper` wraps the page ONCE with
309
309
  `amount={optimisticTotalCents}` (cents at this Stripe boundary only);
@@ -668,7 +668,7 @@ components read copy only through the context or explicit `labels` props).
668
668
  `optionsAsKeymap` / `optionsMatch` / `findMatchingVariant` — the
669
669
  option-choice → variant resolution extracted from product-actions, reading
670
670
  Cartbase's option-value LINK shape (`variant.options[].value.{option_id,value}`)
671
- with the Medusa flat-row fallback. Unit-tested
671
+ with a legacy flat-row fallback. Unit-tested
672
672
  (tests/unit/storefront-catalog.test.ts).
673
673
 
674
674
  ### `<ImageGallery images />` — `products/image-gallery`
@@ -861,7 +861,8 @@ search UIs (chips, drawers) that want the same URL contract.
861
861
 
862
862
  ## Family: order (`@cartbase/storefront/order/*`) — SHIPPED
863
863
 
864
- Order confirmation + account order views, production-proven. The Medusa `StoreOrder` was ONE object carrying computed line
864
+ Order confirmation + account order views, production-proven. Some
865
+ platforms ship ONE `StoreOrder` object carrying computed line
865
866
  totals, order totals, shipping methods and payments; Cartbase splits those
866
867
  across surfaces, so the family takes them as separate props — the Cartbase
867
868
  `StoreOrderDetail` (`api/orders`) carries items as version-pivot rows
@@ -25,7 +25,7 @@ headers the rest of this corpus uses. In practice you'll deploy through
25
25
  the admin UI (**Storefront** in the sidebar), the Cartbase CLI, or an
26
26
  agent connection — all three are wrappers over exactly these calls.
27
27
  Users who belong to several stores name the target store with an
28
- `x-barter-store: <client-id>` header.
28
+ `x-cartbase-store: <client-id>` header.
29
29
 
30
30
  ## What a deploy is
31
31
 
@@ -62,9 +62,9 @@ anyway):
62
62
 
63
63
  | Variable | Value |
64
64
  |---|---|
65
- | `NEXT_PUBLIC_BARTER_URL` | The store's API origin |
66
- | `NEXT_PUBLIC_BARTER_CLIENT_ID` | The store's client id |
67
- | `NEXT_PUBLIC_BARTER_PUBLISHABLE_KEY` | The store's publishable key, when one exists |
65
+ | `NEXT_PUBLIC_CARTBASE_URL` | The store's API origin |
66
+ | `NEXT_PUBLIC_CARTBASE_CLIENT_ID` | The store's client id |
67
+ | `NEXT_PUBLIC_CARTBASE_PUBLISHABLE_KEY` | The store's publishable key, when one exists |
68
68
 
69
69
  Only these public values ever reach a storefront build — secret keys are
70
70
  never injected, so code that expects one is a design error.
@@ -0,0 +1,126 @@
1
+ # Platform fingerprints — Cartbase detection
2
+
3
+ Every Cartbase-powered storefront emits a small, deliberate set of signals
4
+ so platform-detection tools (Wappalyzer, BuiltWith) classify the site as
5
+ **Ecommerce** — the same mechanism Shopify uses (`window.Shopify`,
6
+ `X-ShopId`, `_shopify_s`). Full spec: `docs/cards/platform-fingerprints.md`.
7
+ Category discipline: these are the ONLY signals emitted — nothing that
8
+ reads as a framework, CMS, or website builder, and NEVER on `/api/admin/*`.
9
+
10
+ SDK modules: `@cartbase/storefront/platform`, `@cartbase/storefront/lib/cookie-names`.
11
+
12
+ ---
13
+
14
+ ## Response header — `x-cartbase-version`
15
+
16
+ - **Purpose** — every `/api/store/*` response carries the platform version,
17
+ so a single HTTP response proves the platform without loading a page.
18
+ - **Auth** — none required to observe the header; it's stamped regardless
19
+ of whether the underlying call succeeds or errors (both `withLogging`
20
+ return paths).
21
+ - **Value** — the platform's own semver (`src/lib/platform/identity.ts` →
22
+ `PLATFORM_VERSION`).
23
+ - **Scope** — `/api/store/*` only. `/api/admin/*` never carries it (admin
24
+ is not a storefront).
25
+
26
+ ```bash
27
+ # A successful call carries the header.
28
+ HEADERS=$(curl -sfD - -o /dev/null "$BASE/api/store/consent" -H "x-client-id: $CLIENT_ID")
29
+ echo "$HEADERS" | grep -qi '^x-cartbase-version: '
30
+
31
+ # An ERROR response (missing x-client-id → 400 missing_client_id) still
32
+ # carries it — the fingerprint is not a happy-path-only afterthought.
33
+ ERR_HEADERS=$(curl -sD - -o /dev/null "$BASE/api/store/consent")
34
+ echo "$ERR_HEADERS" | grep -qi '^x-cartbase-version: '
35
+
36
+ # Admin surface never carries it (even though the same withLogging wrapper
37
+ # runs the admin route too, once auth resolves).
38
+ ADMIN_HEADERS=$(curl -sD - -o /dev/null "$BASE/api/admin/users/me")
39
+ ! echo "$ADMIN_HEADERS" | grep -qi '^x-cartbase-version: '
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Meta tag — `<meta name="generator" content="Cartbase" />`
45
+
46
+ - **Purpose** — the classic "what built this site" signal, read by every
47
+ detection tool's HTML scanner.
48
+ - **SDK** — `createStorefrontMetadata(overrides?: Metadata): Metadata`
49
+ (`@cartbase/storefront/platform`). Call it in the root layout instead of
50
+ hand-writing `export const metadata` — `generator` always resolves to
51
+ `PLATFORM_NAME` ("Cartbase") and cannot be shadowed by a stray key in
52
+ `overrides` (the helper's own assignment applies AFTER the spread).
53
+ - **Components** — every storefront's `app/layout.tsx` (see
54
+ `examples/storefront/src/app/layout.tsx` for the reference wiring).
55
+
56
+ ```ts
57
+ // app/layout.tsx
58
+ import { createStorefrontMetadata } from "@cartbase/storefront/platform"
59
+
60
+ export const metadata: Metadata = createStorefrontMetadata({
61
+ title: "My Store",
62
+ description: "...",
63
+ })
64
+ // → renders <meta name="generator" content="Cartbase" /> on every page
65
+ ```
66
+
67
+ This is a build-time Next.js Metadata API call, not a store-API endpoint —
68
+ no curl to demonstrate; verified by
69
+ `tests/unit/storefront-lib.test.ts` ("platform — generator metadata +
70
+ window.Cartbase").
71
+
72
+ ---
73
+
74
+ ## JS global — `window.Cartbase`
75
+
76
+ - **Purpose** — the strongest detector signal (how `window.Shopify` works):
77
+ a synchronous inline script sets `window.Cartbase = {version, storeId}`
78
+ before hydration.
79
+ - **Shape** — `{ version: string, storeId: string }`. No PII, no email, no
80
+ secret — store id + platform version only.
81
+ - **SDK** — `<PlatformInit storeId={...} />` (`@cartbase/storefront/platform`),
82
+ a server component shaped exactly like `<ConsentInit />`
83
+ (`@cartbase/storefront/tracking/consent-init`): one synchronous inline
84
+ `<script>`, mounted once near the top of `<body>`.
85
+ - **Components** — mount once per app, next to `<ConsentInit />`. Never
86
+ mount in an admin bundle.
87
+
88
+ ```tsx
89
+ // app/layout.tsx, inside <body>, first children:
90
+ <ConsentInit />
91
+ <PlatformInit storeId={CARTBASE_CLIENT_ID} />
92
+ ```
93
+
94
+ Also build-time/render-only — no store-API curl to demonstrate; the exact
95
+ script shape (`window.Cartbase={"version":"...","storeId":"..."};`, no
96
+ extra keys) is verified by `tests/unit/storefront-lib.test.ts`.
97
+
98
+ ---
99
+
100
+ ## Cookies — `_cartbase_cart` (was `_barter_cart_id`)
101
+
102
+ - **Purpose** — the cart-id cookie. The app still OWNS *setting* it (the
103
+ SDK never persists the cart — see [carts.md](carts.md)); the library
104
+ owns the NAME so every consumer emits the same fingerprint instead of
105
+ inventing its own prefix.
106
+ - **SDK** — `CART_COOKIE`, `LEGACY_CART_COOKIE`, `readCartCookie(get)`
107
+ (`@cartbase/storefront/lib/cookie-names`). `readCartCookie` prefers the
108
+ new name and falls back to the legacy `_barter_cart_id` name, so an
109
+ existing visitor's cart survives the rename. Every WRITE uses the new
110
+ name only.
111
+ - **`SESSION_COOKIE` (`_cartbase_session`)** — the name is reserved for a
112
+ future cookie-backed customer session. Nothing sets it today: auth is
113
+ pure Bearer-JWT, persisted by the consuming app (see
114
+ [auth.md](auth.md)). Defined now so a future session mechanism launches
115
+ with the fingerprint-correct name.
116
+
117
+ ```ts
118
+ import { CART_COOKIE, readCartCookie } from "@cartbase/storefront/lib/cookie-names"
119
+
120
+ const cartId = readCartCookie((name) => jar.get(name)?.value) // reads either name
121
+ jar.set(CART_COOKIE, cart.id, { path: "/", maxAge: THIRTY_DAYS }) // writes the new name only
122
+ ```
123
+
124
+ No store-API curl to demonstrate (this is a client-cookie contract, not a
125
+ server response); verified by `tests/unit/storefront-lib.test.ts`
126
+ ("platform — cookie names + back-compat read").
@@ -29,8 +29,8 @@ SDK module: `@cartbase/storefront/api/regions`.
29
29
  ```
30
30
 
31
31
  - **Response** — ordered by name. NOTE: Cartbase regions carry NO embedded
32
- `countries` array (divergence from Medusa's Store API — country/tax scope
33
- lives server-side in `tax_regions`).
32
+ `countries` array — country/tax scope lives server-side in
33
+ `tax_regions`.
34
34
 
35
35
  ```jsonc
36
36
  {
@@ -202,6 +202,5 @@ echo "$LOCALES" | grep -q '"en"'
202
202
  - **Errors** — 400 `missing_client_id`.
203
203
  - **SDK** — `listLocales(client)`.
204
204
  - **Components** — locale switcher; `StorefrontClient`'s `getLocale` hook.
205
- - **Settings** — Admin → Settings → Store → locales (per-store
206
- `store_locales` manager; Cartbase's intentional divergence from Medusa's
207
- global locale catalog).
205
+ - **Settings** — Admin → Settings → Store → locales (each store manages
206
+ its own locale list in `store_locales`).
@@ -15,12 +15,12 @@ const nextConfig: NextConfig = {
15
15
  // A real store should allowlist its media domain via remotePatterns.
16
16
  unoptimized: true,
17
17
  },
18
- // The barter store API ships NO CORS headers — browser-side SDK calls
18
+ // The Cartbase store API ships NO CORS headers — browser-side SDK calls
19
19
  // must be same-origin. Proxy them through the app origin (the browser
20
20
  // client's baseUrl is window.location.origin); server-side SDK calls go
21
- // straight to NEXT_PUBLIC_BARTER_URL and are unaffected.
21
+ // straight to NEXT_PUBLIC_CARTBASE_URL and are unaffected.
22
22
  async rewrites() {
23
- const barterUrl = process.env.NEXT_PUBLIC_BARTER_URL
23
+ const barterUrl = process.env.NEXT_PUBLIC_CARTBASE_URL
24
24
  if (!barterUrl) return []
25
25
  return [
26
26
  {
@@ -8,7 +8,7 @@
8
8
  "typecheck": "tsc --noEmit"
9
9
  },
10
10
  "dependencies": {
11
- "@cartbase/storefront": "^0.1.0",
11
+ "@cartbase/storefront": "^0.2.0",
12
12
  "next": "16.2.4",
13
13
  "react": "19.2.4",
14
14
  "react-dom": "19.2.4"
@@ -1,49 +1,51 @@
1
- import { cookies } from "next/headers"
2
- import { redirect } from "next/navigation"
3
- import { retrieveCart } from "@cartbase/storefront/api/carts"
4
- import {
5
- listPaymentProviders,
6
- listShippingOptions,
7
- } from "@cartbase/storefront/api/checkout"
8
- import { getIntegrationsConfig } from "@cartbase/storefront/api/integrations"
9
- import { CART_COOKIE } from "@/lib/config"
10
- import { getServerClient } from "@/lib/server-client"
11
- import { CheckoutPageClient } from "./checkout-page-client"
12
-
13
- /**
14
- * Checkout (runbook step 8 / checkout.md): list shipping options and
15
- * payment providers WITH `cart_id` (server-side rule filtering), fetch the
16
- * integrations `cod` block, and hand everything to the orchestrated
17
- * client layout. Redirect completed/missing carts server-side
18
- * (checkout-client mount rule).
19
- */
20
- export default async function CheckoutPage() {
21
- const jar = await cookies()
22
- const cartId = jar.get(CART_COOKIE)?.value
23
- if (!cartId) redirect("/")
24
-
25
- const client = await getServerClient()
26
- const cart = await retrieveCart(client, cartId)
27
- .then((res) => res.cart)
28
- .catch(() => null)
29
- if (!cart || (cart.items ?? []).length === 0) redirect("/")
30
- if (cart.completed_at) redirect("/")
31
-
32
- const [shippingOptions, paymentProviders, integrations] = await Promise.all([
33
- listShippingOptions(client, { cart_id: cart.id }),
34
- listPaymentProviders(client, {
35
- cart_id: cart.id,
36
- region_id: cart.region_id ?? undefined,
37
- }),
38
- getIntegrationsConfig(client),
39
- ])
40
-
41
- return (
42
- <CheckoutPageClient
43
- cart={cart}
44
- shippingOptions={shippingOptions.shipping_options}
45
- paymentProviders={paymentProviders.payment_providers}
46
- codConfig={integrations.cod}
47
- />
48
- )
49
- }
1
+ import { cookies } from "next/headers"
2
+ import { redirect } from "next/navigation"
3
+ import { retrieveCart } from "@cartbase/storefront/api/carts"
4
+ import {
5
+ listPaymentProviders,
6
+ listShippingOptions,
7
+ } from "@cartbase/storefront/api/checkout"
8
+ import { getIntegrationsConfig } from "@cartbase/storefront/api/integrations"
9
+ import { readCartCookie } from "@/lib/config"
10
+ import { getServerClient } from "@/lib/server-client"
11
+ import { CheckoutPageClient } from "./checkout-page-client"
12
+
13
+ /**
14
+ * Checkout (runbook step 8 / checkout.md): list shipping options and
15
+ * payment providers WITH `cart_id` (server-side rule filtering), fetch the
16
+ * integrations `cod` block, and hand everything to the orchestrated
17
+ * client layout. Redirect completed/missing carts server-side
18
+ * (checkout-client mount rule).
19
+ */
20
+ export default async function CheckoutPage() {
21
+ const jar = await cookies()
22
+ // Back-compat: prefer `_cartbase_cart`, fall back to the legacy
23
+ // `_barter_cart_id` name (platform-fingerprints card).
24
+ const cartId = readCartCookie((name) => jar.get(name)?.value)
25
+ if (!cartId) redirect("/")
26
+
27
+ const client = await getServerClient()
28
+ const cart = await retrieveCart(client, cartId)
29
+ .then((res) => res.cart)
30
+ .catch(() => null)
31
+ if (!cart || (cart.items ?? []).length === 0) redirect("/")
32
+ if (cart.completed_at) redirect("/")
33
+
34
+ const [shippingOptions, paymentProviders, integrations] = await Promise.all([
35
+ listShippingOptions(client, { cart_id: cart.id }),
36
+ listPaymentProviders(client, {
37
+ cart_id: cart.id,
38
+ region_id: cart.region_id ?? undefined,
39
+ }),
40
+ getIntegrationsConfig(client),
41
+ ])
42
+
43
+ return (
44
+ <CheckoutPageClient
45
+ cart={cart}
46
+ shippingOptions={shippingOptions.shipping_options}
47
+ paymentProviders={paymentProviders.payment_providers}
48
+ codConfig={integrations.cod}
49
+ />
50
+ )
51
+ }
@@ -1,105 +1,113 @@
1
- import type { Metadata } from "next"
2
- import Link from "next/link"
3
- import { cookies } from "next/headers"
4
- import { retrieveCart, type Cart } from "@cartbase/storefront/api/carts"
5
- import { getConsent } from "@cartbase/storefront/api/consent"
6
- import { getMenu, type Menu } from "@cartbase/storefront/api/menus"
7
- import { ConsentInit } from "@cartbase/storefront/tracking/consent-init"
8
- import { CartButtonClient } from "@cartbase/storefront/common/cart-button-client"
9
- import { CART_COOKIE } from "@/lib/config"
10
- import { getServerClient } from "@/lib/server-client"
11
- import { Providers } from "./providers"
12
- import "./globals.css"
13
-
14
- export const metadata: Metadata = {
15
- title: "Barter Example Store",
16
- description: "Reference storefront built on @cartbase/storefront",
17
- }
18
-
19
- /**
20
- * Runbook steps 3–4: bootstrap store config at layout level, then mount
21
- * order inside <body>: ConsentInit FIRST (static, synchronous), then the
22
- * consent-gated UI, then navigation. A store with no menus 404s every
23
- * handle — render no nav, never crash (menus.md).
24
- */
25
- async function fetchMenu(handle: string): Promise<Menu | null> {
26
- const client = await getServerClient()
27
- try {
28
- const { menu } = await getMenu(client, handle)
29
- return menu
30
- } catch {
31
- return null // unknown/deleted handle (404) → graceful "no nav"
32
- }
33
- }
34
-
35
- function MenuNav({ menu }: { menu: Menu | null }) {
36
- if (!menu || menu.items.length === 0) return null
37
- return (
38
- <nav className="flex items-center gap-4">
39
- {menu.items.map((item) => (
40
- <Link
41
- key={`${item.title}-${item.url}`}
42
- href={item.url}
43
- className="text-sm text-muted-foreground hover:text-foreground"
44
- >
45
- {item.title}
46
- </Link>
47
- ))}
48
- </nav>
49
- )
50
- }
51
-
52
- export default async function RootLayout({
53
- children,
54
- }: {
55
- children: React.ReactNode
56
- }) {
57
- const client = await getServerClient()
58
- const jar = await cookies()
59
- const cartId = jar.get(CART_COOKIE)?.value ?? null
60
-
61
- const [consentRes, mainMenu, footerMenu, cart] = await Promise.all([
62
- getConsent(client),
63
- fetchMenu("main-menu"),
64
- fetchMenu("footer"),
65
- cartId
66
- ? retrieveCart(client, cartId)
67
- .then((res): Cart | null => res.cart)
68
- .catch(() => null) // stale cookie empty cart state
69
- : Promise.resolve(null),
70
- ])
71
-
72
- return (
73
- <html lang="en">
74
- <body>
75
- <ConsentInit />
76
- <Providers cart={cart} consent={consentRes.consent}>
77
- <header className="border-b border-border">
78
- <div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between gap-6">
79
- <Link href="/" className="font-semibold text-lg">
80
- Barter Example Store
81
- </Link>
82
- <div className="flex items-center gap-6">
83
- <MenuNav menu={mainMenu} />
84
- <Link
85
- href="/search"
86
- className="text-sm text-muted-foreground hover:text-foreground"
87
- >
88
- Search
89
- </Link>
90
- <CartButtonClient cart={cart} />
91
- </div>
92
- </div>
93
- </header>
94
- <main>{children}</main>
95
- <footer className="border-t border-border mt-12">
96
- <div className="max-w-7xl mx-auto px-4 py-8 text-sm text-muted-foreground flex items-center justify-between">
97
- <span>Barter Example Store</span>
98
- <MenuNav menu={footerMenu} />
99
- </div>
100
- </footer>
101
- </Providers>
102
- </body>
103
- </html>
104
- )
105
- }
1
+ import type { Metadata } from "next"
2
+ import Link from "next/link"
3
+ import { cookies } from "next/headers"
4
+ import { retrieveCart, type Cart } from "@cartbase/storefront/api/carts"
5
+ import { getConsent } from "@cartbase/storefront/api/consent"
6
+ import { getMenu, type Menu } from "@cartbase/storefront/api/menus"
7
+ import { ConsentInit } from "@cartbase/storefront/tracking/consent-init"
8
+ import { CartButtonClient } from "@cartbase/storefront/common/cart-button-client"
9
+ import { createStorefrontMetadata, PlatformInit } from "@cartbase/storefront/platform"
10
+ import { BARTER_CLIENT_ID, readCartCookie } from "@/lib/config"
11
+ import { getServerClient } from "@/lib/server-client"
12
+ import { Providers } from "./providers"
13
+ import "./globals.css"
14
+
15
+ // createStorefrontMetadata (platform-fingerprints card) stamps
16
+ // `<meta name="generator" content="Cartbase" />` on every page — never
17
+ // hand-write `generator` here.
18
+ export const metadata: Metadata = createStorefrontMetadata({
19
+ title: "Barter Example Store",
20
+ description: "Reference storefront built on @cartbase/storefront",
21
+ })
22
+
23
+ /**
24
+ * Runbook steps 3–4: bootstrap store config at layout level, then mount
25
+ * order inside <body>: ConsentInit FIRST (static, synchronous), then the
26
+ * consent-gated UI, then navigation. A store with no menus 404s every
27
+ * handle — render no nav, never crash (menus.md).
28
+ */
29
+ async function fetchMenu(handle: string): Promise<Menu | null> {
30
+ const client = await getServerClient()
31
+ try {
32
+ const { menu } = await getMenu(client, handle)
33
+ return menu
34
+ } catch {
35
+ return null // unknown/deleted handle (404) graceful "no nav"
36
+ }
37
+ }
38
+
39
+ function MenuNav({ menu }: { menu: Menu | null }) {
40
+ if (!menu || menu.items.length === 0) return null
41
+ return (
42
+ <nav className="flex items-center gap-4">
43
+ {menu.items.map((item) => (
44
+ <Link
45
+ key={`${item.title}-${item.url}`}
46
+ href={item.url}
47
+ className="text-sm text-muted-foreground hover:text-foreground"
48
+ >
49
+ {item.title}
50
+ </Link>
51
+ ))}
52
+ </nav>
53
+ )
54
+ }
55
+
56
+ export default async function RootLayout({
57
+ children,
58
+ }: {
59
+ children: React.ReactNode
60
+ }) {
61
+ const client = await getServerClient()
62
+ const jar = await cookies()
63
+ // Back-compat: prefer the new `_cartbase_cart` cookie, fall back to the
64
+ // legacy `_barter_cart_id` name so an existing visitor's cart survives
65
+ // the rename (platform-fingerprints card).
66
+ const cartId = readCartCookie((name) => jar.get(name)?.value) ?? null
67
+
68
+ const [consentRes, mainMenu, footerMenu, cart] = await Promise.all([
69
+ getConsent(client),
70
+ fetchMenu("main-menu"),
71
+ fetchMenu("footer"),
72
+ cartId
73
+ ? retrieveCart(client, cartId)
74
+ .then((res): Cart | null => res.cart)
75
+ .catch(() => null) // stale cookie → empty cart state
76
+ : Promise.resolve(null),
77
+ ])
78
+
79
+ return (
80
+ <html lang="en">
81
+ <body>
82
+ <ConsentInit />
83
+ <PlatformInit storeId={BARTER_CLIENT_ID} />
84
+ <Providers cart={cart} consent={consentRes.consent}>
85
+ <header className="border-b border-border">
86
+ <div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between gap-6">
87
+ <Link href="/" className="font-semibold text-lg">
88
+ Barter Example Store
89
+ </Link>
90
+ <div className="flex items-center gap-6">
91
+ <MenuNav menu={mainMenu} />
92
+ <Link
93
+ href="/search"
94
+ className="text-sm text-muted-foreground hover:text-foreground"
95
+ >
96
+ Search
97
+ </Link>
98
+ <CartButtonClient cart={cart} />
99
+ </div>
100
+ </div>
101
+ </header>
102
+ <main>{children}</main>
103
+ <footer className="border-t border-border mt-12">
104
+ <div className="max-w-7xl mx-auto px-4 py-8 text-sm text-muted-foreground flex items-center justify-between">
105
+ <span>Barter Example Store</span>
106
+ <MenuNav menu={footerMenu} />
107
+ </div>
108
+ </footer>
109
+ </Providers>
110
+ </body>
111
+ </html>
112
+ )
113
+ }
@@ -1,43 +1,47 @@
1
- "use server"
2
-
3
- import { revalidatePath } from "next/cache"
4
- import { cookies } from "next/headers"
5
- import { addLineItem, createCart } from "@cartbase/storefront/api/carts"
6
- import { CART_COOKIE, CART_COOKIE_MAX_AGE } from "./config"
7
- import { getServerClient } from "./server-client"
8
-
9
- /**
10
- * PDP add-to-cart server action (carts.md: create the cart lazily on the
11
- * first add — region falls back to the store default; persist `cart.id`
12
- * in a cookie). Passed into `ProductTemplate`'s `addToCart` seam.
13
- */
14
- export async function addToCartAction(input: {
15
- variantId: string
16
- quantity: number
17
- }): Promise<void> {
18
- const client = await getServerClient()
19
- const jar = await cookies()
20
- const cartId = jar.get(CART_COOKIE)?.value
21
-
22
- if (cartId) {
23
- await addLineItem(client, cartId, {
24
- variant_id: input.variantId,
25
- quantity: input.quantity,
26
- })
27
- } else {
28
- const { cart } = await createCart(client, {
29
- currency_code: "eur",
30
- items: [{ variant_id: input.variantId, quantity: input.quantity }],
31
- })
32
- jar.set(CART_COOKIE, cart.id, {
33
- path: "/",
34
- sameSite: "lax",
35
- httpOnly: false, // the browser-side drawer persists the same cookie
36
- maxAge: CART_COOKIE_MAX_AGE,
37
- })
38
- }
39
-
40
- // Layout refetches the cart snapshot → CartDrawerProvider sees the new
41
- // product-line count and auto-opens the drawer.
42
- revalidatePath("/", "layout")
43
- }
1
+ "use server"
2
+
3
+ import { revalidatePath } from "next/cache"
4
+ import { cookies } from "next/headers"
5
+ import { addLineItem, createCart } from "@cartbase/storefront/api/carts"
6
+ import { CART_COOKIE, CART_COOKIE_MAX_AGE, readCartCookie } from "./config"
7
+ import { getServerClient } from "./server-client"
8
+
9
+ /**
10
+ * PDP add-to-cart server action (carts.md: create the cart lazily on the
11
+ * first add — region falls back to the store default; persist `cart.id`
12
+ * in a cookie). Passed into `ProductTemplate`'s `addToCart` seam.
13
+ *
14
+ * Reads via `readCartCookie` (new `_cartbase_cart` name, falls back to the
15
+ * legacy `_barter_cart_id` — platform-fingerprints card); every WRITE uses
16
+ * the new name only, so a visitor's cart survives the rename.
17
+ */
18
+ export async function addToCartAction(input: {
19
+ variantId: string
20
+ quantity: number
21
+ }): Promise<void> {
22
+ const client = await getServerClient()
23
+ const jar = await cookies()
24
+ const cartId = readCartCookie((name) => jar.get(name)?.value)
25
+
26
+ if (cartId) {
27
+ await addLineItem(client, cartId, {
28
+ variant_id: input.variantId,
29
+ quantity: input.quantity,
30
+ })
31
+ } else {
32
+ const { cart } = await createCart(client, {
33
+ currency_code: "eur",
34
+ items: [{ variant_id: input.variantId, quantity: input.quantity }],
35
+ })
36
+ jar.set(CART_COOKIE, cart.id, {
37
+ path: "/",
38
+ sameSite: "lax",
39
+ httpOnly: false, // the browser-side drawer persists the same cookie
40
+ maxAge: CART_COOKIE_MAX_AGE,
41
+ })
42
+ }
43
+
44
+ // Layout refetches the cart snapshot → CartDrawerProvider sees the new
45
+ // product-line count and auto-opens the drawer.
46
+ revalidatePath("/", "layout")
47
+ }
@@ -1,12 +1,17 @@
1
1
  /** Shared storefront configuration (BUILD-A-STOREFRONT.md — inputs table). */
2
2
 
3
- export const BARTER_URL = process.env.NEXT_PUBLIC_BARTER_URL ?? ""
4
- export const BARTER_CLIENT_ID = process.env.NEXT_PUBLIC_BARTER_CLIENT_ID ?? ""
3
+ export const BARTER_URL = process.env.NEXT_PUBLIC_CARTBASE_URL ?? ""
4
+ export const BARTER_CLIENT_ID = process.env.NEXT_PUBLIC_CARTBASE_CLIENT_ID ?? ""
5
5
  export const BARTER_PUBLISHABLE_KEY =
6
- process.env.NEXT_PUBLIC_BARTER_PUBLISHABLE_KEY || undefined
6
+ process.env.NEXT_PUBLIC_CARTBASE_PUBLISHABLE_KEY || undefined
7
7
 
8
- /** Cart-id cookie — owned by the app (the SDK never persists the cart). */
9
- export const CART_COOKIE = "_barter_cart_id"
8
+ /**
9
+ * Cart-id cookie — owned by the app (the SDK never persists the cart);
10
+ * the NAME is owned by the library so every consumer emits the same wire
11
+ * fingerprint (platform-fingerprints card). `readCartCookie` is the
12
+ * backward-compat reader for the legacy `_barter_cart_id` name.
13
+ */
14
+ export { CART_COOKIE, readCartCookie } from "@cartbase/storefront/lib/cookie-names"
10
15
  export const CART_COOKIE_MAX_AGE = 60 * 60 * 24 * 30 // 30 days
11
16
 
12
17
  /** Locale cookie read by the clients' `getLocale`. */