create-cartbase 0.0.1 → 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/LICENSE +21 -0
- package/README.md +9 -3
- package/dist/index.js +94 -0
- package/package.json +18 -4
- package/template/app/CLAUDE.md +18 -0
- package/template/app/docs/BUILD-A-STOREFRONT.md +216 -0
- package/template/app/docs/README.md +77 -0
- package/template/app/docs/auth.md +105 -0
- package/template/app/docs/carts.md +376 -0
- package/template/app/docs/categories.md +194 -0
- package/template/app/docs/checkout.md +611 -0
- package/template/app/docs/collections.md +167 -0
- package/template/app/docs/components.md +1090 -0
- package/template/app/docs/consent.md +81 -0
- package/template/app/docs/content.md +126 -0
- package/template/app/docs/customers.md +269 -0
- package/template/app/docs/deploy.md +192 -0
- package/template/app/docs/gift-cards.md +153 -0
- package/template/app/docs/integrations.md +137 -0
- package/template/app/docs/menus.md +73 -0
- package/template/app/docs/metaobjects.md +126 -0
- package/template/app/docs/orders.md +221 -0
- package/template/app/docs/platform.md +126 -0
- package/template/app/docs/products.md +300 -0
- package/template/app/docs/redirects.md +50 -0
- package/template/app/docs/regions.md +206 -0
- package/template/app/docs/reviews.md +223 -0
- package/template/app/docs/search.md +218 -0
- package/template/app/docs/subscriptions.md +148 -0
- package/template/app/next.config.ts +34 -0
- package/template/app/package.json +25 -0
- package/template/app/postcss.config.cjs +6 -0
- package/template/app/smoke.mjs +158 -0
- package/template/app/src/app/checkout/checkout-page-client.tsx +66 -0
- package/template/app/src/app/checkout/page.tsx +51 -0
- package/template/app/src/app/globals.css +42 -0
- package/template/app/src/app/layout.tsx +113 -0
- package/template/app/src/app/order/[id]/confirmed/page.tsx +77 -0
- package/template/app/src/app/page.tsx +25 -0
- package/template/app/src/app/products/[handle]/page.tsx +58 -0
- package/template/app/src/app/providers.tsx +54 -0
- package/template/app/src/app/search/page.tsx +20 -0
- package/template/app/src/lib/browser-client.ts +35 -0
- package/template/app/src/lib/cart-actions.ts +47 -0
- package/template/app/src/lib/config.ts +21 -0
- package/template/app/src/lib/server-client.ts +25 -0
- package/template/app/tailwind.config.cjs +9 -0
- package/template/app/tsconfig.json +41 -0
- package/template/app/tsconfig.tsbuildinfo +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cartbase
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# create-cartbase
|
|
2
2
|
|
|
3
|
-
Scaffold a
|
|
3
|
+
Scaffold a storefront on the [Cartbase](https://cartbase.net) commerce
|
|
4
|
+
platform:
|
|
4
5
|
|
|
5
6
|
```bash
|
|
6
7
|
npm create cartbase my-store
|
|
8
|
+
# or, prefilled from your admin's Settings values:
|
|
9
|
+
npm create cartbase my-store -- --url https://… --client-id … --key pk_…
|
|
7
10
|
```
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
You get a working Next.js storefront wired to your store through
|
|
13
|
+
`@cartbase/storefront`, with the **complete platform docs included in
|
|
14
|
+
`docs/`** and a `CLAUDE.md` brief — hand the folder to a coding agent
|
|
15
|
+
and it has everything it needs offline. Deploy with the
|
|
16
|
+
[`cartbase`](https://www.npmjs.com/package/cartbase) CLI.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
function parseArgs(argv) {
|
|
6
|
+
const flags = new Map();
|
|
7
|
+
let dir;
|
|
8
|
+
for (let i = 0; i < argv.length; i++) {
|
|
9
|
+
const arg = argv[i];
|
|
10
|
+
if (arg.startsWith("--")) {
|
|
11
|
+
const name = arg.slice(2);
|
|
12
|
+
const next = argv[i + 1];
|
|
13
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
14
|
+
flags.set(name, next);
|
|
15
|
+
i++;
|
|
16
|
+
}
|
|
17
|
+
else
|
|
18
|
+
flags.set(name, true);
|
|
19
|
+
}
|
|
20
|
+
else if (!dir)
|
|
21
|
+
dir = arg;
|
|
22
|
+
}
|
|
23
|
+
return { dir, flags };
|
|
24
|
+
}
|
|
25
|
+
function copyDir(from, to) {
|
|
26
|
+
fs.mkdirSync(to, { recursive: true });
|
|
27
|
+
for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
|
|
28
|
+
const src = path.join(from, entry.name);
|
|
29
|
+
const dest = path.join(to, entry.name);
|
|
30
|
+
if (entry.isDirectory())
|
|
31
|
+
copyDir(src, dest);
|
|
32
|
+
else if (entry.isFile())
|
|
33
|
+
fs.copyFileSync(src, dest);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function main() {
|
|
37
|
+
const { dir, flags } = parseArgs(process.argv.slice(2));
|
|
38
|
+
if (!dir || flags.get("help") === true) {
|
|
39
|
+
console.log(`create-cartbase — scaffold a Cartbase storefront
|
|
40
|
+
|
|
41
|
+
Usage:
|
|
42
|
+
npm create cartbase <directory> [-- --url <api-origin> --client-id <uuid> --key <pk_…>]
|
|
43
|
+
|
|
44
|
+
The three store inputs live in your Cartbase admin under Settings; given
|
|
45
|
+
as flags they are written into .env.local, otherwise placeholders are.`);
|
|
46
|
+
process.exitCode = dir ? 0 : 1;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const target = path.resolve(dir);
|
|
50
|
+
if (fs.existsSync(target) && fs.readdirSync(target).length > 0) {
|
|
51
|
+
console.error(`${target} already exists and is not empty.`);
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const templateApp = path.join(path.dirname(path.dirname(fileURLToPath(import.meta.url))), "template", "app");
|
|
56
|
+
copyDir(templateApp, target);
|
|
57
|
+
// Name the app after its directory.
|
|
58
|
+
const pkgPath = path.join(target, "package.json");
|
|
59
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
60
|
+
pkg.name = path
|
|
61
|
+
.basename(target)
|
|
62
|
+
.toLowerCase()
|
|
63
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
64
|
+
.replace(/^-+|-+$/g, "") || "cartbase-storefront";
|
|
65
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
66
|
+
const url = typeof flags.get("url") === "string" ? flags.get("url") : "";
|
|
67
|
+
const clientId = typeof flags.get("client-id") === "string" ? flags.get("client-id") : "";
|
|
68
|
+
const key = typeof flags.get("key") === "string" ? flags.get("key") : "";
|
|
69
|
+
fs.writeFileSync(path.join(target, ".env.local"), [
|
|
70
|
+
"# Your store's inputs — Cartbase admin, Settings.",
|
|
71
|
+
`NEXT_PUBLIC_CARTBASE_URL=${url}`,
|
|
72
|
+
`NEXT_PUBLIC_CARTBASE_CLIENT_ID=${clientId}`,
|
|
73
|
+
`NEXT_PUBLIC_CARTBASE_PUBLISHABLE_KEY=${key}`,
|
|
74
|
+
"",
|
|
75
|
+
].join("\n"));
|
|
76
|
+
const filled = url && clientId;
|
|
77
|
+
console.log(`
|
|
78
|
+
Created ${path.basename(target)}/
|
|
79
|
+
|
|
80
|
+
Next steps:
|
|
81
|
+
cd ${dir}
|
|
82
|
+
bun install (or npm install)${filled
|
|
83
|
+
? ""
|
|
84
|
+
: `
|
|
85
|
+
→ open .env.local and fill in your store's URL, client id and
|
|
86
|
+
publishable key (Cartbase admin, Settings)`}
|
|
87
|
+
bun run dev
|
|
88
|
+
|
|
89
|
+
The complete storefront reference is in docs/ — hand CLAUDE.md to your
|
|
90
|
+
coding agent and it has everything. Deploy with the Cartbase CLI:
|
|
91
|
+
cartbase login, then cartbase deploy.
|
|
92
|
+
`);
|
|
93
|
+
}
|
|
94
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-cartbase",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Scaffold a Cartbase storefront
|
|
5
|
-
"license": "
|
|
6
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Scaffold a Cartbase storefront: npm create cartbase my-store",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-cartbase": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": ["dist", "template"],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "node scripts/build-template.mjs && tsc -p tsconfig.json",
|
|
13
|
+
"prepack": "node scripts/build-template.mjs && tsc -p tsconfig.json"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"typescript": "^5"
|
|
20
|
+
}
|
|
7
21
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# This is a Cartbase storefront
|
|
2
|
+
|
|
3
|
+
A Next.js app wired to a Cartbase store through `@cartbase/storefront`
|
|
4
|
+
(typed API client + UI components). Everything you need is local:
|
|
5
|
+
|
|
6
|
+
- **docs/** — the complete storefront reference (also at the store's docs
|
|
7
|
+
site): endpoints, shapes, auth, checkout orchestration, components.
|
|
8
|
+
Start with docs/BUILD-A-STOREFRONT.md.
|
|
9
|
+
- **.env.local** — the three store inputs (API origin, client id,
|
|
10
|
+
publishable key). Missing values: ask the merchant to copy them from
|
|
11
|
+
their Cartbase admin under Settings.
|
|
12
|
+
- Deploy with the Cartbase CLI: `cartbase login` then `cartbase deploy`
|
|
13
|
+
— you get a permanent preview URL; the merchant publishes from their
|
|
14
|
+
admin. Never commit .env.local.
|
|
15
|
+
|
|
16
|
+
House rules: totals and prices are SERVER truth (render them verbatim);
|
|
17
|
+
always pass a pricing context; consent before tracking; the store API is
|
|
18
|
+
proxied same-origin via next.config rewrites.
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Build a storefront — the runbook
|
|
2
|
+
|
|
3
|
+
**This runbook takes you from a blank Next.js app to a completed checkout
|
|
4
|
+
against your Cartbase store.** It is written to be followed by a developer
|
|
5
|
+
or handed to a coding agent as-is. Each step names the domain doc that
|
|
6
|
+
carries the full contracts (shapes, curls, error codes, settings). Follow
|
|
7
|
+
the steps in order — later steps assume earlier wiring exists. If anything
|
|
8
|
+
here is unclear or wrong, it's a documentation bug — report it.
|
|
9
|
+
|
|
10
|
+
What you need before starting (all three are in your Cartbase admin under
|
|
11
|
+
**Settings**):
|
|
12
|
+
|
|
13
|
+
| Input | Example | Where it goes |
|
|
14
|
+
|---|---|---|
|
|
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
|
+
|
|
19
|
+
Sanity-check the store before writing any code:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# The regions listing is the cheapest liveness + auth probe.
|
|
23
|
+
curl -sf "$BASE/api/store/regions" -H "x-client-id: $CLIENT_ID" | grep -q '"regions"'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Step 1 — Scaffold + package
|
|
27
|
+
|
|
28
|
+
Create the Next.js app (App Router) and add the package:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# doc-noexec — scaffolding happens in YOUR repo, not against the API.
|
|
32
|
+
bunx create-next-app@latest my-store --ts --app --tailwind
|
|
33
|
+
cd my-store && bun add @cartbase/storefront
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- The package is **source-shipped TypeScript** — add
|
|
37
|
+
`transpilePackages: ["@cartbase/storefront"]` to `next.config` or nothing
|
|
38
|
+
from it will compile.
|
|
39
|
+
- The Tailwind preset is **Tailwind 3 format** (`tailwind-preset.cjs`).
|
|
40
|
+
`create-next-app --tailwind` scaffolds Tailwind 4 (CSS-first, no config
|
|
41
|
+
file) — either install `tailwindcss@3.4` + a `tailwind.config.cjs` with
|
|
42
|
+
`presets: [require("@cartbase/storefront/tailwind-preset")]` (include the
|
|
43
|
+
package source in `content`:
|
|
44
|
+
`"./node_modules/@cartbase/storefront/src/**/*.{ts,tsx}"`), or translate
|
|
45
|
+
the preset's tokens into Tailwind 4 `@theme` yourself. Then define the
|
|
46
|
+
shadcn-standard token variables (`--background`, `--primary`, … as raw
|
|
47
|
+
oklch channels) in your CSS — the working set is
|
|
48
|
+
`examples/storefront/src/app/globals.css`.
|
|
49
|
+
- Pin the package version — storefronts never float `latest`.
|
|
50
|
+
- Monorepo caveat: when the app lives in a workspace, set
|
|
51
|
+
`outputFileTracingRoot` in `next.config` — Next infers the root from
|
|
52
|
+
the nearest stray lockfile, and a wrong root silently breaks
|
|
53
|
+
page-segment hydration in dev (buttons render but nothing responds).
|
|
54
|
+
|
|
55
|
+
## Step 2 — The client seam
|
|
56
|
+
|
|
57
|
+
Construct ONE `StorefrontClient` per scope and pass it to every SDK call
|
|
58
|
+
(all SDK functions take the client as first argument — see any domain doc's
|
|
59
|
+
SDK line):
|
|
60
|
+
|
|
61
|
+
- **Server** (RSC, server actions): construct per request; `getAuthToken`
|
|
62
|
+
reads the customer session cookie; `getLocale` reads the locale cookie.
|
|
63
|
+
- **Browser**: construct once; token from your session store.
|
|
64
|
+
|
|
65
|
+
Auth model (recap — full detail in [auth.md](auth.md)):
|
|
66
|
+
`x-client-id` always; `x-publishable-api-key` when the store gave you one
|
|
67
|
+
(it scopes the catalog and carts to the key's sales channels);
|
|
68
|
+
`authorization: Bearer <jwt>` once a customer is logged in.
|
|
69
|
+
|
|
70
|
+
**CORS (bites every browser client):** the store API sends NO CORS
|
|
71
|
+
headers today, so browser-side SDK calls (cart drawer mutations, the
|
|
72
|
+
whole checkout orchestration) only work same-origin. Unless your
|
|
73
|
+
storefront is served from the Cartbase deployment origin itself, proxy the
|
|
74
|
+
store surface through your own origin — in Next.js one rewrite does it —
|
|
75
|
+
and give the BROWSER client `window.location.origin` as `baseUrl`
|
|
76
|
+
(server-side calls hit `NEXT_PUBLIC_CARTBASE_URL` directly and are
|
|
77
|
+
unaffected; see `examples/storefront/next.config.ts` for the working
|
|
78
|
+
rewrite):
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
// next.config.ts — proxy browser SDK traffic to the API origin
|
|
82
|
+
async rewrites() {
|
|
83
|
+
return [{ source: "/api/store/:path*",
|
|
84
|
+
destination: `${process.env.NEXT_PUBLIC_CARTBASE_URL}/api/store/:path*` }]
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Step 3 — Store configuration bootstrap
|
|
89
|
+
|
|
90
|
+
Fetch once at layout level, cache per the docs' cache headers:
|
|
91
|
+
|
|
92
|
+
1. [regions.md](regions.md) — regions (→ region_id for pricing + payment
|
|
93
|
+
providers), currencies, supported locales.
|
|
94
|
+
2. [integrations.md](integrations.md) — `GET /api/store/integrations`: which
|
|
95
|
+
carriers are enabled (pickup points/lockers for checkout), COD fee
|
|
96
|
+
presence, and the **tracking block** (pixel/GA4/GTM/ads public ids).
|
|
97
|
+
3. [consent.md](consent.md) — the CMP config for the consent banner.
|
|
98
|
+
|
|
99
|
+
## Step 4 — Layout: consent, tracking, navigation
|
|
100
|
+
|
|
101
|
+
Order inside `<body>` matters (contracts in [components.md](components.md)
|
|
102
|
+
and [consent.md](consent.md)):
|
|
103
|
+
|
|
104
|
+
1. `<ConsentInit>` FIRST child of body — static and synchronous, never
|
|
105
|
+
awaits a fetch (first-hit consent race otherwise).
|
|
106
|
+
2. Tracking mounts (`<MetaPixel>`, `<GA4>`, GTM) — gated on the consent
|
|
107
|
+
state and fed by the integrations tracking block, never by env vars.
|
|
108
|
+
3. Navigation from [menus.md](menus.md) — `main-menu` / `footer` handles;
|
|
109
|
+
an unknown handle 404s and must render as "no nav", never crash.
|
|
110
|
+
|
|
111
|
+
## Step 5 — Catalog
|
|
112
|
+
|
|
113
|
+
- [products.md](products.md) — listing + PDP. Always pass a pricing context
|
|
114
|
+
(`currency_code` or `region_id`) or prices come back undecorated; render
|
|
115
|
+
`variant.calculated_price`, fall back to base `prices[]`. **Never cache a
|
|
116
|
+
`calculated_price` response shared when a customer JWT was present** —
|
|
117
|
+
prices vary by customer group.
|
|
118
|
+
- [collections.md](collections.md) — collection pages use the membership
|
|
119
|
+
endpoint (`/collections/:id/products`) which honors the admin's sort.
|
|
120
|
+
- [categories.md](categories.md) — category tree, tags, types.
|
|
121
|
+
- [search.md](search.md) — search page: `q` + facets from the response
|
|
122
|
+
(render buckets, apply via the documented query params), typo-tolerant,
|
|
123
|
+
synonym-aware. Related products on the PDP come from the same doc.
|
|
124
|
+
- SEO: `seo_title`/`seo_description` fields with title/description
|
|
125
|
+
fallbacks; path conventions are `/products/<handle>`,
|
|
126
|
+
`/collections/<handle>`, `/categories/<handle>`.
|
|
127
|
+
|
|
128
|
+
## Step 6 — Content
|
|
129
|
+
|
|
130
|
+
- [content.md](content.md) — `/pages/<handle>` and `/blogs/<handle>` routes;
|
|
131
|
+
body HTML is server-sanitized, safe to render raw. Every store seeds
|
|
132
|
+
policy pages (`privacy-policy`, `terms-of-service`, `refund-policy`,
|
|
133
|
+
`shipping-policy`) — link them in the footer.
|
|
134
|
+
- [metaobjects.md](metaobjects.md) — merchant-defined content (size charts
|
|
135
|
+
via the product→metafield→metaobject chain).
|
|
136
|
+
- [redirects.md](redirects.md) — call ONLY from your `not-found` handler;
|
|
137
|
+
301 when `to_path` is non-null. Never on regular page loads.
|
|
138
|
+
|
|
139
|
+
## Step 7 — Cart
|
|
140
|
+
|
|
141
|
+
[carts.md](carts.md): create the cart lazily on first add-to-cart with the
|
|
142
|
+
region + (optionally) sales channel; persist `cart.id` in a cookie; all cart
|
|
143
|
+
mutations return the decorated cart — totals are SERVER truth, render them
|
|
144
|
+
verbatim, never compute client-side. Line items, quantity updates, deletes,
|
|
145
|
+
and the customer-attach call after login are all in that doc.
|
|
146
|
+
[gift-cards.md](gift-cards.md): the apply/remove endpoints + the three
|
|
147
|
+
decoration fields (`gift_cards[]`, `gift_card_total`,
|
|
148
|
+
`gift_card_remainder`) your summary UI must render.
|
|
149
|
+
|
|
150
|
+
## Step 8 — Checkout
|
|
151
|
+
|
|
152
|
+
[checkout.md](checkout.md) is the authoritative sequence. In brief:
|
|
153
|
+
|
|
154
|
+
1. List shipping options and payment providers **with `cart_id`** — the
|
|
155
|
+
server filters both through the merchant's checkout rules; your UI never
|
|
156
|
+
hides methods on its own.
|
|
157
|
+
2. Carrier pickers (office/locker) come from the integrations config
|
|
158
|
+
([integrations.md](integrations.md)); the chosen point goes into
|
|
159
|
+
`carrier_metadata`.
|
|
160
|
+
3. Buy click = `prepare-checkout` (ONE call: address + shipping method +
|
|
161
|
+
payment session at the final amount) → for card: Stripe
|
|
162
|
+
`confirmPayment(client_secret)` → `complete`. For COD: `complete`
|
|
163
|
+
directly. Gift-card-covered carts skip the provider entirely.
|
|
164
|
+
4. Handle the documented failure codes (`checkout_method_hidden`,
|
|
165
|
+
`account_required`, `gift_card_insufficient_balance`, cart-vs-order
|
|
166
|
+
union on complete) — each has a UI recovery path described in the doc.
|
|
167
|
+
5. `sync-payment-amount` after any total-changing edit on the payment step;
|
|
168
|
+
`refresh-payment-if-terminal` only from Elements `loaderror` / aged-cart
|
|
169
|
+
mount.
|
|
170
|
+
6. Order confirmation renders from [orders.md](orders.md)
|
|
171
|
+
(`/orders/display/:displayId` embeds items, fulfillments, tracking).
|
|
172
|
+
|
|
173
|
+
## Step 9 — Customer accounts
|
|
174
|
+
|
|
175
|
+
[auth.md](auth.md): passwordless email-code login (request → verify →
|
|
176
|
+
Bearer session). [customers.md](customers.md): profile, addresses,
|
|
177
|
+
order history, issued documents (invoices). Respect `accounts_mode`
|
|
178
|
+
(store setting): `required` blocks guest checkout with `403
|
|
179
|
+
account_required`; `disabled` means render no account UI at all. Gate B2B
|
|
180
|
+
content on `customer.account_status === "approved"`.
|
|
181
|
+
|
|
182
|
+
## Step 10 — Reviews
|
|
183
|
+
|
|
184
|
+
[reviews.md](reviews.md): the PDP widget (`/reviews/widget` — aggregate +
|
|
185
|
+
first page + display options in one call) and the token wizard route for
|
|
186
|
+
email CTA links (`/review/<token>`): validate token → submit rating+body →
|
|
187
|
+
optional photo step mints the reward code. Resume rules are in the doc.
|
|
188
|
+
|
|
189
|
+
## Step 11 — Tracking events
|
|
190
|
+
|
|
191
|
+
[components.md](components.md) tracking section: fire client events via the
|
|
192
|
+
package helpers; **Purchase MUST use `eventID = "purchase_" +
|
|
193
|
+
order.display_id`** so Meta dedupes browser Pixel against the server CAPI
|
|
194
|
+
event; write the attribution keys (fbp/fbc/anon-id/ga session) into
|
|
195
|
+
`cart.metadata` (consent-gated) so server events inherit them.
|
|
196
|
+
|
|
197
|
+
## Step 12 — Go-live checklist
|
|
198
|
+
|
|
199
|
+
- [ ] Pricing context passed on every catalog surface; no shared caching of
|
|
200
|
+
JWT-priced responses.
|
|
201
|
+
- [ ] Consent banner renders for an unconfigured store (defaults are
|
|
202
|
+
server-applied); tags mount only behind consent.
|
|
203
|
+
- [ ] Checkout completes: card (Stripe live), COD (fee renders from config),
|
|
204
|
+
gift card (partial + full cover).
|
|
205
|
+
- [ ] Not-found handler consults redirects; policy pages linked.
|
|
206
|
+
- [ ] Order confirmation + emails render totals identical to the cart.
|
|
207
|
+
- [ ] Locale switching keeps cart + session; EUR everywhere.
|
|
208
|
+
- [ ] The store's publishable key (if any) is set — B2B catalogs are
|
|
209
|
+
key-scoped and look "missing products" without it.
|
|
210
|
+
|
|
211
|
+
## Step 13 — Ship it
|
|
212
|
+
|
|
213
|
+
[deploy.md](deploy.md): send the app to Cartbase hosting — permanent
|
|
214
|
+
preview URL on every deploy, **Publish** to go live, deploy history as
|
|
215
|
+
rollback. Hosted storefronts get the three inputs above injected
|
|
216
|
+
automatically, so there is nothing to configure.
|
|
@@ -0,0 +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 |
|
|
77
|
+
| [platform.md](platform.md) | Platform fingerprints: generator meta, window.Cartbase, x-cartbase-version header, cart cookie naming |
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Auth — passwordless code login + session discipline
|
|
2
|
+
|
|
3
|
+
Passwordless email-code login (customer-accounts card, Shopify Customer
|
|
4
|
+
Account API direction). Two calls: **request** emails a 6-digit code,
|
|
5
|
+
**verify** exchanges it for a session. Password login
|
|
6
|
+
(`supabase.auth.signInWithPassword` client-side) stays available and yields
|
|
7
|
+
an equivalent JWT — nothing here is needed for it.
|
|
8
|
+
|
|
9
|
+
## The session discipline (one rule)
|
|
10
|
+
|
|
11
|
+
`verify` mints a **real Supabase session**. Its `access_token` is the SAME
|
|
12
|
+
`authorization: Bearer <jwt>` every `/api/store/*` route accepts — wire it
|
|
13
|
+
into `StorefrontClient` once and every customer surface (customers/me,
|
|
14
|
+
orders, documents, cart attach) is authenticated:
|
|
15
|
+
|
|
16
|
+
```jsonc
|
|
17
|
+
// new StorefrontClient({ baseUrl, clientId, getAuthToken: () => storedAccessToken })
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Persist `access_token` + `refresh_token`; refresh client-side with the
|
|
21
|
+
Supabase SDK (`setSession` → `refreshSession`) — the Cartbase API does not
|
|
22
|
+
proxy token refresh. `expires_in` is seconds; `expires_at` epoch-seconds.
|
|
23
|
+
|
|
24
|
+
## Server-enforced security posture (the SDK adds nothing)
|
|
25
|
+
|
|
26
|
+
- Request NEVER reveals whether the email is registered — `{ok: true}`
|
|
27
|
+
always (no enumeration oracle). The code exists ONLY in the sent email.
|
|
28
|
+
- Codes: hashed at rest, TTL **10 min**, single-use, one live code per
|
|
29
|
+
email (a new request supersedes prior codes), **5-wrong-attempt lockout**.
|
|
30
|
+
- Rate limits per 15-min window: **5 requests per email**, **20 per IP** →
|
|
31
|
+
`429 rate_limited` — the only distinguishable request failure.
|
|
32
|
+
- Every verify failure — wrong code, expired, consumed, locked out, unknown
|
|
33
|
+
email — is the SAME `401 invalid_code`.
|
|
34
|
+
|
|
35
|
+
## POST /api/store/auth/code/request — step 1
|
|
36
|
+
|
|
37
|
+
- **Purpose**: email a 6-digit one-time login code. First-time emails
|
|
38
|
+
register lazily at VERIFY, not here.
|
|
39
|
+
- **Auth**: anon (`x-client-id` only).
|
|
40
|
+
- **Request**: `{email: string}`.
|
|
41
|
+
- **Response**: `{ok: true}` — always, by design.
|
|
42
|
+
- **Errors**: `429 rate_limited` · `400 validation_failed` (malformed
|
|
43
|
+
email) · `400 missing_client_id`.
|
|
44
|
+
- **SDK**: `auth.requestLoginCode(client, {email})`
|
|
45
|
+
- **Components**: login form (account pages family).
|
|
46
|
+
- **Settings**: the `auth-code` notification template carries the code.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# The request contract: {ok:true}, nothing else — no code echo, no
|
|
50
|
+
# registered-or-not oracle. RUN-stamped email → no rate-limit collisions.
|
|
51
|
+
curl -sf -X POST "$BASE/api/store/auth/code/request" \
|
|
52
|
+
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
|
|
53
|
+
-d '{"email": "doc-auth-'$RUN'@doc.test"}' \
|
|
54
|
+
| grep -q '^{"ok":true}$'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## POST /api/store/auth/code/verify — step 2
|
|
58
|
+
|
|
59
|
+
- **Purpose**: exchange (email, code) for a session. This is ALSO
|
|
60
|
+
passwordless **registration** — a first-time email lazy-creates the
|
|
61
|
+
customer (`account_status` per store policy) and triggers the welcome
|
|
62
|
+
email.
|
|
63
|
+
- **Auth**: anon (`x-client-id` only).
|
|
64
|
+
- **Request**: `{email: string, code: string}` — code is exactly 6 digits
|
|
65
|
+
(leading zeros count; any other shape → `400 validation_failed`).
|
|
66
|
+
- **Response**:
|
|
67
|
+
|
|
68
|
+
```jsonc
|
|
69
|
+
{
|
|
70
|
+
"access_token": "eyJ…", // the Bearer JWT for every store route
|
|
71
|
+
"refresh_token": "…",
|
|
72
|
+
"expires_in": 3600, // seconds
|
|
73
|
+
"expires_at": 1789300000, // epoch seconds (may be absent)
|
|
74
|
+
"token_type": "bearer",
|
|
75
|
+
"customer": { /* full customer + addresses — see customers.md */ }
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- **Errors**: `401 invalid_code` (every auth failure, generic) ·
|
|
80
|
+
`400 validation_failed` · `400 missing_client_id`.
|
|
81
|
+
- **SDK**: `auth.verifyLoginCode(client, {email, code})`
|
|
82
|
+
- **Components**: code-entry form (account pages family).
|
|
83
|
+
- **Settings**: store approval policy (new-customer `account_status`);
|
|
84
|
+
`account-welcome` template.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# The 401 invalid_code contract — a well-formed but wrong code against the
|
|
88
|
+
# RUN-stamped email (its real code lives only in the email we never read).
|
|
89
|
+
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
90
|
+
-X POST "$BASE/api/store/auth/code/verify" \
|
|
91
|
+
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
|
|
92
|
+
-d '{"email": "doc-auth-'$RUN'@doc.test", "code": "000000"}')
|
|
93
|
+
test "$STATUS" = 401
|
|
94
|
+
curl -s -X POST "$BASE/api/store/auth/code/verify" \
|
|
95
|
+
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
|
|
96
|
+
-d '{"email": "doc-auth-'$RUN'@doc.test", "code": "000000"}' \
|
|
97
|
+
| grep -q '"code":"invalid_code"'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
> Cleanup note: the code rows this doc creates are single-per-run
|
|
101
|
+
> (RUN-stamped email), superseded on any later request and dead after the
|
|
102
|
+
> 10-minute TTL — there is no store-surface delete for them by design (they
|
|
103
|
+
> are the auth trail). The happy-path session mint is exercised with a real
|
|
104
|
+
> inbox-free code by `tests/store/customer-accounts-auth.test.ts` and the
|
|
105
|
+
> SDK contract test (server-side code issue via the lib).
|