@spree/docs 0.1.131 → 0.1.133

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.
@@ -20,6 +20,8 @@ Every store ships with one default channel named *Online Store*. You can add mor
20
20
  | `code` | URL-safe slug, stable identifier sent via the `X-Spree-Channel` header | `pos` |
21
21
  | `active` | When `false`, the channel stops accepting orders | `true` |
22
22
  | `default` | Exactly one channel per store is the default. Used as a fallback when no channel header is present and as the auto-publish target for new products | `true` |
23
+ | `storefront_access` | Controls what an anonymous visitor may see: `public`, `prices_hidden`, or `login_required`. Unset inherits the store's setting. See [Storefront Access Gating](#storefront-access-gating) | `login_required` |
24
+ | `guest_checkout` | Whether an order may be placed without an account on this channel. Unset inherits the store's setting | `false` |
23
25
  | `preferred_order_routing_strategy` | Optional per-channel override of the store's [Order Routing](shipments.md#order-routing) strategy | `Spree::OrderRouting::Strategy::Rules` |
24
26
 
25
27
  `code` is normalized to a URL-safe slug on save — `POS` becomes `pos`, `Point of Sale!` becomes `point-of-sale`. Leaving `code` blank derives it from `name`.
@@ -89,6 +91,52 @@ Every order is attributed to one channel. The channel is set from the `X-Spree-C
89
91
 
90
92
  This attribution drives reporting (best-selling by channel, revenue per channel) and per-channel order routing — see [Order Routing](shipments.md#order-routing).
91
93
 
94
+ ### Storefront Access Gating
95
+
96
+
97
+ A channel's `storefront_access` decides what an **anonymous** visitor — a request with no authenticated customer — may see. Logged-in customers are never gated. The posture is one of three values:
98
+
99
+ | Mode | Guest sees catalog | Guest sees prices | Use case |
100
+ |---|---|---|---|
101
+ | `public` | Yes | Yes | The default. An open storefront — anyone can browse; guests can also check out when `guest_checkout` is enabled. |
102
+ | `prices_hidden` | Yes | No — prices come back `null` | A catalog you want discoverable, with pricing revealed only after sign-in (e.g. a trade catalog for lead generation). |
103
+ | `login_required` | No — reads rejected with `401` | No | A fully gated surface — a guest can't read the catalog at all (e.g. a members-only or B2B wholesale portal). |
104
+
105
+ The gate is enforced by the **Store API**, not the storefront, so a storefront app can't loosen it:
106
+
107
+ - **`login_required`** — every gated read returns `401` for an unauthenticated request. Endpoints that must stay reachable before sign-in (authentication, password reset, reference data like countries and currencies) are exempt.
108
+ - **`prices_hidden`** — reads succeed, but every money field is serialized as `null` for a guest. The storefront renders these as a sign-in prompt rather than a price.
109
+
110
+ A companion control, **`guest_checkout`**, decides whether an order can be placed without an account on the channel. It's independent of `storefront_access` — a `public` channel can still require accounts, and the two are resolved separately.
111
+
112
+ #### Store fallback
113
+
114
+ Both controls fall back to the owning [Store](stores.md) when the channel's own value is unset — the same inheritance pattern as the channel's order-routing strategy. `storefront_access` resolves to the channel value, then the store value, and finally `public` when neither is set. This lets you set a store-wide default (e.g. "all channels require login") and override per channel where needed.
115
+
116
+
117
+ ```typescript Admin SDK
118
+ // Gate a channel behind sign-in, and require accounts at checkout
119
+ await adminClient.channels.update('ch_wholesale', {
120
+ storefront_access: 'login_required',
121
+ guest_checkout: false,
122
+ })
123
+
124
+ // Clear the channel value to inherit the store's default
125
+ await adminClient.channels.update('ch_wholesale', {
126
+ storefront_access: null,
127
+ })
128
+ ```
129
+
130
+ ```bash cURL
131
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/channels/ch_wholesale' \
132
+ -H 'X-Spree-API-Key: sk_xxx' \
133
+ -H 'Content-Type: application/json' \
134
+ -d '{ "storefront_access": "login_required", "guest_checkout": false }'
135
+ ```
136
+
137
+
138
+ Switching a channel between modes takes effect immediately — the posture is resolved per request, so no cache warm-up or redeploy is needed. The Next.js storefront's [wholesale portal](../storefront/nextjs/wholesale.md) is a worked example of a `login_required` / `prices_hidden` channel driving the UI.
139
+
92
140
  ## Publishing Products on Channels
93
141
 
94
142
  ### Dashboard
@@ -172,3 +220,4 @@ The write contract is **full-set**: the array represents the complete desired st
172
220
  - [Order Routing](shipments.md#order-routing) — Channels can override the store's routing strategy
173
221
  - [Store SDK: Products](../sdk/store/products.md) — Channel-scoped product listing and filtering
174
222
  - [Admin SDK: Resources](../sdk/admin/resources.md) — How `adminClient.channels.addProducts` and other resource methods are structured
223
+ - [Wholesale Portal](../storefront/nextjs/wholesale.md) — A gated channel driving the Next.js storefront's B2B surface
@@ -5,7 +5,9 @@ description: Understand Spree Stores — the top-level tenant boundary that scop
5
5
 
6
6
  ## Overview
7
7
 
8
- The StThe Store is the top-level tenant in Spree. Every resource — products, orders, channels, markets, taxonomies — belongs to exactly one store. A store owns its [channels](channels.md) (online, POS, wholesale, …), its [markets](markets.md) (region/currency/locale), and its [product catalog](products.md).tore Attributes
8
+ The Store is the top-level tenant in Spree. Every resource — products, orders, channels, markets, taxonomies — belongs to exactly one store. A store owns its [channels](channels.md) (online, POS, wholesale, …), its [markets](markets.md) (region/currency/locale), and its [product catalog](products.md).
9
+
10
+ ## Store Attributes
9
11
 
10
12
  | Attribute | Description |
11
13
  |-----------|-------------|
@@ -19,6 +21,8 @@ The StThe Store is the top-level tenant in Spree. Every resource — products, o
19
21
  | `mail_from_address` | Sender address for transactional emails |
20
22
  | `logo_url` | URL to the store's logo |
21
23
  | `facebook`, `twitter`, `instagram` | Social media links |
24
+ | `storefront_access` | Store-wide default for anonymous [storefront access gating](channels.md#storefront-access-gating) (`public`, `prices_hidden`, `login_required`). Each channel can override it |
25
+ | `guest_checkout` | Store-wide default for whether orders can be placed without an account. Each channel can override it |
22
26
 
23
27
  ## Fetching Store Information
24
28
 
@@ -54,6 +58,10 @@ Two different ways to split a store, often confused:
54
58
 
55
59
  A single Online Store channel can serve multiple markets (one storefront → many regions). Conversely, POS and Online channels can share the same market (same currency/locale, different selling surfaces).
56
60
 
61
+ ### Storefront access defaults
62
+
63
+ The store sets the fallback for **storefront access gating** — whether anonymous visitors may browse, see prices, or must sign in first — via `storefront_access` (`public`, `prices_hidden`, `login_required`) and the companion `guest_checkout` control. Each channel inherits these unless it sets its own value, so you can gate a whole store or just one channel (e.g. a `login_required` wholesale channel on an otherwise `public` store). The full behavior of each mode and how the Store API enforces it lives in [Channels → Storefront Access Gating](channels.md#storefront-access-gating).
64
+
57
65
  ## Store Resources
58
66
 
59
67
  Each store owns its own resources. Products, orders, channels, markets, and taxonomies in one store are independent from another.
@@ -64,7 +72,7 @@ Each store owns its own resources. Products, orders, channels, markets, and taxo
64
72
  | [**Markets**](markets.md) | A store has many markets, each defining a geographic region with its own currency and locale |
65
73
  | [**Orders**](orders.md) | An order belongs to one store and one channel |
66
74
  | [**Products**](products.md) | A product belongs to one store. Its visibility across channels is controlled by [publications](channels.md#publishing-products-on-channels). |
67
- | [**Taxonomies**](products.md#categories) | A taxonomy belongs to one store |
75
+ | [**Categories**](products.md#categories) | A category belongs to one store |
68
76
  | [**Payment Methods**](payments.md) | A payment method belongs to one store |
69
77
  | [**Shipping Methods**](shipments.md) | A shipping method belongs to one store |
70
78
  | [**Promotions**](promotions.md) | A promotion belongs to one store |
@@ -82,4 +90,5 @@ If you need one Spree backend to serve **multiple distinct merchant brands** —
82
90
  - [Markets](markets.md) — Multi-region commerce within a store
83
91
  - [Products](products.md) — Product catalog
84
92
  - [Orders](orders.md) — Order management and checkout
93
+ - [Wholesale Portal](../storefront/nextjs/wholesale.md) — A gated B2B storefront surface built on channel access gating
85
94
  - [Admin SDK](../sdk/admin/quickstart.md) — TypeScript client for the Admin API used to read and update store configuration
@@ -38,20 +38,29 @@ src/
38
38
  │ │ │ └── [slug]/ # Product details
39
39
  │ │ ├── t/[...permalink]/ # Category pages
40
40
  │ │ └── categories/ # Category overview
41
- └── (checkout)/ # Checkout layout (no header/footer)
42
- ├── checkout/[id]/ # Checkout flow
43
- └── order-placed/[id]/ # Order confirmation
41
+ ├── (checkout)/ # Checkout layout (no header/footer)
42
+ ├── checkout/[id]/ # Checkout flow
43
+ └── order-placed/[id]/ # Order confirmation
44
+ │ └── (wholesale)/ # Opt-in B2B portal (gated — see Wholesale guide)
45
+ │ └── wholesale/
46
+ │ ├── page.tsx # Trade catalog
47
+ │ ├── products/[slug]/ # Wholesale PDP
48
+ │ ├── cart/ # Wholesale cart
49
+ │ ├── quick-order/ # SKU quick-order form
50
+ │ ├── apply/ # Trade-account application
51
+ │ └── _components/ # Gate, header, sign-in wall, pending, guest browse
44
52
  ├── components/
45
53
  │ ├── cart/ # CartDrawer
46
54
  │ ├── checkout/ # AddressStep, DeliveryStep, PaymentStep, etc.
47
55
  │ ├── layout/ # Header, Footer, CountrySwitcher
48
56
  │ ├── navigation/ # Breadcrumbs
49
- │ ├── products/ # ProductCard, ProductGrid, Filters, MediaGallery, VariantPicker
57
+ │ ├── products/ # ProductCard, ProductGrid, Filters, MediaGallery, HiddenPricePrompt
50
58
  │ └── search/ # SearchBar
51
59
  ├── contexts/
52
60
  │ ├── AuthContext.tsx # Auth state
53
61
  │ ├── CartContext.tsx # Client-side cart state sync
54
62
  │ ├── CheckoutContext.tsx # Checkout flow state
63
+ │ ├── HiddenPricingContext.tsx # Prices-hidden signal for wholesale guests
55
64
  │ └── StoreContext.tsx # Store/locale/currency state
56
65
  ├── hooks/
57
66
  │ ├── useCarouselProducts.ts # Product carousel data
@@ -72,7 +81,11 @@ src/
72
81
  │ ├── payment.ts # Payment processing
73
82
  │ ├── products.ts # Product queries
74
83
  │ ├── categories.ts # Categories
75
- └── utils.ts # Shared helpers (actionResult, withFallback)
84
+ ├── wholesale.ts # Wholesale channel + catalog + quick-order
85
+ │ ├── utils.ts # Shared helpers (actionResult, withFallback)
86
+ │ └── … # One file per domain — see src/lib/data/
87
+ ├── spree/ # Spree integration (client, cookies, auth refresh, middleware, surface)
88
+ ├── wholesale.ts # Approval check + volume-pricing helpers
76
89
  └── utils/ # Client utilities
77
90
  ├── address.ts # Address formatting
78
91
  ├── cookies.ts # Cookie helpers
@@ -81,6 +94,8 @@ src/
81
94
  └── product-query.ts # Product filter query builder
82
95
  ```
83
96
 
97
+ The `(wholesale)` route group is an opt-in B2B portal, off unless a wholesale channel is configured. Its gating, surfaces, and pricing modes are covered in the [Wholesale Portal](wholesale.md) guide.
98
+
84
99
  ## Authentication Flow
85
100
 
86
101
  1. User submits login form
@@ -107,17 +122,9 @@ export async function getCustomer() {
107
122
  }
108
123
  ```
109
124
 
110
- ## Multi-Region Support
111
-
112
- The storefront supports multiple countries and currencies via URL segments:
113
-
114
- ```
115
- /us/en/products # US store, English
116
- /de/de/products # German store, German
117
- /uk/en/products # UK store, English
118
- ```
125
+ ## Multi-Region
119
126
 
120
- A middleware (`src/proxy.ts`) uses `createSpreeMiddleware` from `src/lib/spree` to detect the visitor's country and locale, then redirects to the correct URL prefix. The `CountrySwitcher` component lets users change regions manually.
127
+ The storefront serves multiple countries, currencies, and languages from one deployment via `/{country}/{locale}` URL segments and an edge middleware that detects and persists the visitor's region. See the [Multi-Region](multi-region.md) guide.
121
128
 
122
129
  ## Server Actions
123
130
 
@@ -1,35 +1,19 @@
1
1
  ---
2
2
  title: Customization
3
- description: Fork, customize, and extend the Spree Next.js Storefront
3
+ description: Customize and extend the Spree Next.js Storefront
4
4
  ---
5
5
 
6
- ## Forking the Starter
6
+ The storefront is yours to modify — the code ships in your project so you can restyle it, swap components, and change the data layer directly. Scaffold a project with [`create-spree-app`](quickstart.md#installation), then edit the storefront under `apps/storefront/`.
7
7
 
8
- The recommended approach is to fork the repository so you can customize freely while pulling upstream updates.
8
+ ## Tracking Upstream Updates
9
9
 
10
- ### 1. Fork on GitHub
11
-
12
- Go to [github.com/spree/storefront](https://github.com/spree/storefront) and click **Fork**.
13
-
14
- ### 2. Clone Your Fork
15
-
16
- ```bash
17
- git clone https://github.com/YOUR_USERNAME/storefront.git
18
- cd storefront
19
- npm install
20
- ```
21
-
22
- ### 3. Add Upstream Remote
10
+ The storefront evolves upstream. To keep pulling improvements while you customize, own the code in your own Git repository (a fork of [spree/storefront](https://github.com/spree/storefront), or your own repo with the storefront as an upstream remote):
23
11
 
24
12
  ```bash
13
+ # Point an "upstream" remote at the official storefront
25
14
  git remote add upstream https://github.com/spree/storefront.git
26
- ```
27
-
28
- ### 4. Pull Upstream Updates
29
15
 
30
- When the official starter gets updates, pull them into your fork:
31
-
32
- ```bash
16
+ # Pull in the latest changes
33
17
  git fetch upstream
34
18
  git merge upstream/main
35
19
  ```
@@ -138,66 +122,8 @@ export default async function YourNewPage() {
138
122
 
139
123
  ## Transactional Emails
140
124
 
141
- Customer-facing emails are rendered in the storefront using [react-email](https://react.email) and sent via [Resend](https://resend.com). The Spree backend delivers order/shipment/password events to the storefront via [webhooks](../../core-concepts/webhooks.md).
142
-
143
- ### Templates
144
-
145
- Email templates are React components in `src/lib/emails/`:
146
-
147
- | File | Event | Description |
148
- |------|-------|-------------|
149
- | `order-confirmation.tsx` | `order.completed` | Items, totals, addresses, delivery method |
150
- | `order-canceled.tsx` | `order.canceled` | Cancellation notice with items |
151
- | `shipment-shipped.tsx` | `order.shipped` | Tracking number and link |
152
- | `password-reset.tsx` | `customer.password_reset_requested` | Reset button and fallback link |
153
- | `newsletter-confirmation.tsx` | `newsletter_subscriber.subscription_requested` | Double opt-in confirmation link |
154
-
155
- Customize templates by editing these files directly. They use `@react-email/components` for email-safe layout primitives.
156
-
157
- ### Previewing
158
-
159
- ```bash
160
- npm run email:dev
161
- ```
162
-
163
- Opens the react-email dev server with mock data for all templates at `http://localhost:3000`.
164
-
165
- ### Webhook Handler
166
-
167
- The webhook route (`src/app/api/webhooks/spree/route.ts`) uses `createWebhookHandler` from `src/lib/spree/webhooks`:
168
-
169
- ```typescript
170
- import { createWebhookHandler } from '@/lib/spree/webhooks'
171
-
172
- export const POST = createWebhookHandler({
173
- secret: process.env.SPREE_WEBHOOK_SECRET!,
174
- handlers: {
175
- 'order.completed': handleOrderCompleted,
176
- 'order.canceled': handleOrderCanceled,
177
- 'order.shipped': handleOrderShipped,
178
- 'customer.password_reset_requested': handlePasswordReset,
179
- 'newsletter_subscriber.subscription_requested': handleNewsletterConfirmation,
180
- },
181
- })
182
- ```
183
-
184
- To add a new email type:
185
-
186
- 1. Create a template in `src/lib/emails/`
187
- 2. Add a handler function in `src/lib/webhooks/handlers.ts`
188
- 3. Register the event in `route.ts`
189
- 4. Subscribe to the event in Spree Admin → Webhooks
190
-
191
- ### Local Development
192
-
193
- In dev, emails are written to `.next/emails/` as HTML files — no Resend key needed. Use [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/) to receive webhooks locally:
194
-
195
- ```bash
196
- cloudflared tunnel --url http://localhost:3001
197
- ```
198
-
199
- For full setup details, see [Sending out Emails](../../deployment/emails.md).
125
+ The storefront can render and send its own order, shipment, and account emails with react-email and Resend, driven by Spree webhooks. See the dedicated [Transactional Emails](emails.md) guide.
200
126
 
201
127
  ## Building a Custom Storefront
202
128
 
203
- If you prefer to build from scratch instead of forking the starter, you can use the `@spree/sdk` package directly in any Next.js application. The storefront's `src/lib/spree/` directory contains reusable helpers for cookie-based auth, locale resolution, middleware, and webhook verification that you can copy into your own project.
129
+ If you prefer to build from scratch instead of using the starter, you can use the `@spree/sdk` package directly in any Next.js application. The storefront's `src/lib/spree/` directory contains reusable helpers for cookie-based auth, locale resolution, middleware, and webhook verification that you can copy into your own project.
@@ -5,26 +5,7 @@ description: Deploy the Spree Next.js Storefront to Vercel, Docker, or any Node.
5
5
 
6
6
  ## Environment Variables
7
7
 
8
- Set these variables in your hosting platform's dashboard or `.env` file.
9
-
10
- ### Required
11
-
12
- | Variable | Description |
13
- |----------|-------------|
14
- | `SPREE_API_URL` | Your Spree API endpoint (e.g., `https://api.mystore.com`) |
15
- | `SPREE_PUBLISHABLE_KEY` | Publishable API key from your Spree admin |
16
-
17
- > **NOTE:** These are server-side only variables — no `NEXT_PUBLIC_` prefix needed since all API calls happen in Server Actions.
18
-
19
- ### Optional
20
-
21
- | Variable | Description | Default |
22
- |----------|-------------|---------|
23
- | `GTM_ID` | Google Tag Manager container ID | _(disabled)_ |
24
- | `SENTRY_DSN` | Sentry DSN for error tracking | _(disabled)_ |
25
- | `SENTRY_ORG` | Sentry organization slug | _(none)_ |
26
- | `SENTRY_PROJECT` | Sentry project slug | _(none)_ |
27
- | `SENTRY_AUTH_TOKEN` | Sentry auth token (for source maps in CI) | _(none)_ |
8
+ At minimum, set `SPREE_API_URL` and `SPREE_PUBLISHABLE_KEY` in your hosting platform's dashboard or `.env` file. See [Environment Variables](environment-variables.md) for the full reference — analytics, error tracking, wholesale, emails, and SEO.
28
9
 
29
10
  ## Production Build
30
11
 
@@ -43,7 +24,13 @@ Vercel is the recommended deployment platform for Next.js applications.
43
24
 
44
25
  1. Push your code to GitHub
45
26
  2. Go to [vercel.com/new](https://vercel.com/new) and import your repository
46
- 3. Add environment variables (`SPREE_API_URL`, `SPREE_PUBLISHABLE_KEY`)
27
+ 3. Add environment variables:
28
+ - `SPREE_API_URL` and `SPREE_PUBLISHABLE_KEY` (required)
29
+ - `SPREE_WEBHOOK_SECRET`, `RESEND_API_KEY`, `EMAIL_FROM` — for [transactional emails](emails.md)
30
+ - `GTM_ID` — optional, Google Tag Manager
31
+ - `SENTRY_DSN`, `SENTRY_ORG`, `SENTRY_PROJECT`, `SENTRY_AUTH_TOKEN` — optional, error tracking with readable stack traces
32
+
33
+ See the [Environment Variables reference](environment-variables.md) for the full list.
47
34
  4. Click **Deploy**
48
35
 
49
36
  Vercel automatically detects the Next.js framework and configures the build settings.
@@ -61,56 +48,50 @@ Every pull request gets a unique preview URL, making it easy to test storefront
61
48
 
62
49
  ## Docker
63
50
 
64
- Create a `Dockerfile` in your project root:
51
+ A multi-stage `Dockerfile` ships at the repo root. It uses Next.js standalone output to produce a small (~240 MB) image on `node:22-alpine`, runs as a non-root user, and exposes port `3001`.
65
52
 
66
- ```dockerfile
67
- FROM node:20-alpine AS base
53
+ > **NOTE:** `SPREE_API_URL` and `SPREE_PUBLISHABLE_KEY` are required at **build time** — the storefront prerenders pages against the Spree API. Point them at a Spree instance reachable from wherever you run `docker build` (hosted Spree, a tunnel, or `host.docker.internal` for a local backend on Docker Desktop).
68
54
 
69
- FROM base AS deps
70
- WORKDIR /app
71
- COPY package.json package-lock.json ./
72
- RUN npm ci --production=false
73
-
74
- FROM base AS builder
75
- WORKDIR /app
76
- COPY --from=deps /app/node_modules ./node_modules
77
- COPY . .
78
- RUN npm run build
55
+ Build and run:
79
56
 
80
- FROM base AS runner
81
- WORKDIR /app
82
- ENV NODE_ENV=production
57
+ ```bash
58
+ docker build \
59
+ --build-arg SPREE_API_URL=https://your-spree.example.com \
60
+ --build-arg SPREE_PUBLISHABLE_KEY=your_publishable_key \
61
+ -t spree-storefront .
83
62
 
84
- RUN addgroup --system --gid 1001 nodejs
85
- RUN adduser --system --uid 1001 nextjs
63
+ docker run -p 3001:3001 --env-file .env.local spree-storefront
64
+ ```
86
65
 
87
- COPY --from=builder /app/public ./public
88
- COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
89
- COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
66
+ ### Sentry source maps at build time
90
67
 
91
- USER nextjs
92
- EXPOSE 3000
93
- ENV PORT=3000
94
- ENV HOSTNAME="0.0.0.0"
68
+ `SENTRY_AUTH_TOKEN` is passed via a BuildKit secret so it never lands in image layers or the build cache. The other Sentry vars are regular build args:
95
69
 
96
- CMD ["node", "server.js"]
70
+ ```bash
71
+ SENTRY_AUTH_TOKEN=... docker build \
72
+ --build-arg SPREE_API_URL=... \
73
+ --build-arg SPREE_PUBLISHABLE_KEY=... \
74
+ --build-arg SENTRY_DSN=... \
75
+ --build-arg SENTRY_ORG=... \
76
+ --build-arg SENTRY_PROJECT=... \
77
+ --secret id=sentry_auth_token,env=SENTRY_AUTH_TOKEN \
78
+ -t spree-storefront .
97
79
  ```
98
80
 
99
- > **NOTE:** Enable standalone output in your `next.config.ts` for Docker deployments:
100
- >
101
- > ```typescript
102
- const nextConfig = {
103
- output: 'standalone',
104
- }
105
- ```
81
+ ### Building against a local Spree backend
106
82
 
107
- Build and run:
83
+ On Docker Desktop (macOS/Windows), reach the host's Spree via `host.docker.internal`:
108
84
 
109
85
  ```bash
110
- docker build -t spree-storefront .
111
- docker run -p 3000:3000 \
112
- -e SPREE_API_URL=https://api.mystore.com \
113
- -e SPREE_PUBLISHABLE_KEY=your_key \
86
+ docker build \
87
+ --add-host=host.docker.internal:host-gateway \
88
+ --build-arg SPREE_API_URL=http://host.docker.internal:3000 \
89
+ --build-arg SPREE_PUBLISHABLE_KEY=your_publishable_key \
90
+ -t spree-storefront .
91
+
92
+ docker run -p 3001:3001 \
93
+ --add-host=host.docker.internal:host-gateway \
94
+ --env-file .env.local \
114
95
  spree-storefront
115
96
  ```
116
97
 
@@ -131,7 +112,7 @@ npm run build
131
112
  npm start
132
113
  ```
133
114
 
134
- The server listens on port `3000` by default. Set the `PORT` environment variable to change it.
115
+ The server listens on port `3001`. Set the `PORT` environment variable to change it.
135
116
 
136
117
  ## CI/CD
137
118
 
@@ -0,0 +1,86 @@
1
+ ---
2
+ title: Transactional Emails
3
+ description: Render and send order, shipment, and account emails from the Spree Next.js Storefront with react-email, Resend, and Spree webhooks
4
+ ---
5
+
6
+ The storefront can own its customer-facing transactional emails instead of the Spree backend. Emails are rendered with [react-email](https://react.email) and sent via [Resend](https://resend.com); the Spree backend delivers order, shipment, and account events to the storefront over [webhooks](../../core-concepts/webhooks.md).
7
+
8
+ ```
9
+ Spree Backend → Webhook POST → /api/webhooks/spree → render email → send via Resend
10
+ (signed HMAC) (signature verified) (react-email) (or write to disk in dev)
11
+ ```
12
+
13
+ ## Templates
14
+
15
+ Email templates are React components in `src/lib/emails/`:
16
+
17
+ | File | Event | Description |
18
+ |------|-------|-------------|
19
+ | `order-confirmation.tsx` | `order.completed` | Items, totals, addresses, delivery method |
20
+ | `order-canceled.tsx` | `order.canceled` | Cancellation notice with items |
21
+ | `shipment-shipped.tsx` | `order.shipped` | Tracking number and link |
22
+ | `password-reset.tsx` | `customer.password_reset_requested` | Reset button and fallback link |
23
+
24
+ Customize a template by editing its file directly — they use `@react-email/components` for email-safe layout primitives.
25
+
26
+ ## Previewing
27
+
28
+ Run the storefront in development and open [http://localhost:3001/dev/emails](http://localhost:3001/dev/emails):
29
+
30
+ ```bash
31
+ npm run dev
32
+ ```
33
+
34
+ Each template renders with sample data via `@react-email/render`. The route is gated to non-production environments.
35
+
36
+ ## Configuration
37
+
38
+ Add these to `.env.local`:
39
+
40
+ ```env
41
+ SPREE_WEBHOOK_SECRET=your_webhook_endpoint_secret_key
42
+ RESEND_API_KEY=re_your_resend_api_key # production only
43
+ EMAIL_FROM=Your Store <orders@your-domain.com> # production only
44
+ ```
45
+
46
+ In development no `RESEND_API_KEY` is needed — emails are written to `.next/emails/` as HTML files with a `file://` link logged to the console.
47
+
48
+ ## Webhook Handler
49
+
50
+ The webhook route (`src/app/api/webhooks/spree/route.ts`) wires events to handlers with `createWebhookHandler` from `src/lib/spree/webhooks`. Signature verification and event routing are handled for you:
51
+
52
+ ```typescript
53
+ import { createWebhookHandler } from '@/lib/spree/webhooks'
54
+
55
+ const handler = createWebhookHandler({
56
+ secret: process.env.SPREE_WEBHOOK_SECRET!,
57
+ handlers: {
58
+ 'order.completed': handleOrderCompleted,
59
+ 'order.canceled': handleOrderCanceled,
60
+ 'order.shipped': handleOrderShipped,
61
+ 'customer.password_reset_requested': handlePasswordReset,
62
+ },
63
+ })
64
+ ```
65
+
66
+ To add a new email type:
67
+
68
+ 1. Create a template in `src/lib/emails/`.
69
+ 2. Add a handler function in `src/lib/webhooks/handlers.ts`.
70
+ 3. Register the event in `route.ts`.
71
+ 4. Subscribe to the event in **Spree Admin → Settings → Developers → Webhooks**.
72
+
73
+ ## Setup
74
+
75
+ 1. **Create a webhook endpoint** in Spree Admin → Settings → Developers → Webhooks. Subscribe to `order.completed`, `order.canceled`, `order.shipped`, and `customer.password_reset_requested`, and copy the secret key into `SPREE_WEBHOOK_SECRET`.
76
+
77
+ 2. **Receive webhooks locally.** Expose the storefront with a public URL so Spree can reach it — the simplest option is a [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/):
78
+
79
+ ```bash
80
+ brew install cloudflared
81
+ cloudflared tunnel --url http://localhost:3001
82
+ ```
83
+
84
+ Use the tunnel URL as the webhook endpoint URL in Spree Admin.
85
+
86
+ For the backend perspective on delivering these events, see [Sending out Emails](../../deployment/emails.md).
@@ -0,0 +1,88 @@
1
+ ---
2
+ title: Environment Variables
3
+ description: Every environment variable the Spree Next.js Storefront reads, with defaults and when to set it
4
+ ---
5
+
6
+ Configuration lives in `.env.local` (copy `.env.example` to start). Variables **without** a `NEXT_PUBLIC_` prefix are server-side only and never reach the browser; `NEXT_PUBLIC_` variables are inlined into the client bundle at build time, so only put non-secret values there.
7
+
8
+ ## Required
9
+
10
+ | Variable | Description |
11
+ |----------|-------------|
12
+ | `SPREE_API_URL` | Your Spree API endpoint (e.g. `http://localhost:3000` in dev, `https://api.mystore.com` in production) |
13
+ | `SPREE_PUBLISHABLE_KEY` | Publishable API key from your Spree admin |
14
+
15
+ > **NOTE:** In a Docker build these two are also required at **build time** — the storefront prerenders pages against the Spree API. See [Deployment](deployment.md).
16
+
17
+ ## Store defaults
18
+
19
+ Used by the middleware for initial redirects before API data loads, and as build-time fallbacks for sitemap/SEO generation.
20
+
21
+ | Variable | Description | Default |
22
+ |----------|-------------|---------|
23
+ | `NEXT_PUBLIC_DEFAULT_COUNTRY` | Default country ISO code — should match your store's `default_country_iso` | `us` |
24
+ | `NEXT_PUBLIC_DEFAULT_LOCALE` | Default locale code — should match your store's `default_locale` | `en` |
25
+ | `NEXT_PUBLIC_SITE_URL` | Public site URL, used for sitemap and `robots.txt` generation (e.g. `https://mystore.com`) | _(required for sitemap)_ |
26
+ | `NEXT_PUBLIC_STORE_NAME` | Store name used in metadata fallbacks | `Spree Store` |
27
+ | `NEXT_PUBLIC_STORE_DESCRIPTION` | Store description used in metadata fallbacks | _(sample text)_ |
28
+
29
+ ## SEO & social
30
+
31
+ Optional overrides for site metadata and the Organization JSON-LD. When unset, values fall back to store settings from the Spree API.
32
+
33
+ | Variable | Description |
34
+ |----------|-------------|
35
+ | `STORE_SEO_TITLE` | Default `<title>` |
36
+ | `STORE_META_DESCRIPTION` | Default meta description |
37
+ | `STORE_META_KEYWORDS` | Default meta keywords |
38
+ | `STORE_TWITTER` | Twitter/X handle or URL |
39
+ | `STORE_FACEBOOK` | Facebook page URL |
40
+ | `STORE_INSTAGRAM` | Instagram URL |
41
+ | `STORE_LOGO_URL` | Logo URL for Organization JSON-LD |
42
+ | `STORE_SUPPORT_EMAIL` | Customer support email |
43
+
44
+ ## Wholesale B2B portal
45
+
46
+ The [wholesale portal](wholesale.md) is an opt-in addon, off by default.
47
+
48
+ | Variable | Description | Default |
49
+ |----------|-------------|---------|
50
+ | `SPREE_WHOLESALE_CHANNEL` | Enable switch — the code of a gated Spree channel to bind the `/wholesale` surface to. Unset means DTC-only: every wholesale entry point is hidden and the routes 404 | _(disabled)_ |
51
+ | `SPREE_WHOLESALE_PUBLISHABLE_KEY` | Channel-scoped publishable key for the wholesale surface. Optional — the channel header selects the channel, so this falls back to `SPREE_PUBLISHABLE_KEY` | _(falls back to `SPREE_PUBLISHABLE_KEY`)_ |
52
+
53
+ ## Transactional emails
54
+
55
+ See the [Transactional Emails](emails.md) guide for the full setup.
56
+
57
+ | Variable | Description | Default |
58
+ |----------|-------------|---------|
59
+ | `SPREE_WEBHOOK_SECRET` | Webhook endpoint secret key that signs incoming Spree webhooks | _(disabled)_ |
60
+ | `RESEND_API_KEY` | [Resend](https://resend.com) API key for sending emails in production | _(dev: writes to disk)_ |
61
+ | `EMAIL_FROM` | "From" address for transactional emails (e.g. `Store <orders@mystore.com>`) | `orders@example.com` |
62
+
63
+ ## Payments
64
+
65
+ Publishable keys for client-side payment SDKs, read when the matching gateway is enabled. Set the key for whichever provider(s) you use.
66
+
67
+ | Variable | Description |
68
+ |----------|-------------|
69
+ | `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` | Stripe publishable key (`pk_…`) |
70
+
71
+ ## Analytics
72
+
73
+ | Variable | Description | Default |
74
+ |----------|-------------|---------|
75
+ | `GTM_ID` | Google Tag Manager container ID (e.g. `GTM-XXXXXXX`). Leave empty to disable | _(disabled)_ |
76
+
77
+ ## Error tracking (Sentry)
78
+
79
+ | Variable | Description | Default |
80
+ |----------|-------------|---------|
81
+ | `SENTRY_DSN` | Sentry DSN — set to enable error tracking (e.g. `https://key@o0.ingest.sentry.io/0`) | _(disabled)_ |
82
+ | `SENTRY_ORG` | Sentry organization slug (for source map uploads) | _(none)_ |
83
+ | `SENTRY_PROJECT` | Sentry project slug (for source map uploads) | _(none)_ |
84
+ | `SENTRY_AUTH_TOKEN` | Sentry auth token (for source map uploads in CI) | _(none)_ |
85
+ | `SENTRY_SEND_DEFAULT_PII` | Send PII (IP addresses, cookies, user data) to Sentry server-side | `false` |
86
+ | `NEXT_PUBLIC_SENTRY_SEND_DEFAULT_PII` | Send PII to Sentry client-side | `false` |
87
+
88
+ > **WARNING:** PII collection is disabled by default. Only set the `SENTRY_SEND_DEFAULT_PII` variables to `true` if you have appropriate user consent or a privacy policy covering this data.