@spree/docs 0.1.246 → 0.1.248

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.
@@ -29,7 +29,7 @@ https://spreecommerce.org/docs/developer/core-concepts/orders.md
29
29
  The full developer documentation also ships as an npm package, so agents working in your repository can `Read` doc files locally — no network round-trips, works offline, and version-pinned to what you installed:
30
30
 
31
31
  ```bash
32
- npm install --save-dev @spree/docs
32
+ pnpm add -D @spree/docs
33
33
  ```
34
34
 
35
35
  ```text
@@ -242,6 +242,25 @@ Two details worth knowing:
242
242
  - **Delivery and order-level fees are shared out by item value**, so a seller whose goods made up most of the basket carries most of the delivery charge.
243
243
 
244
244
  > **WARNING:** A storefront must handle the possibility of an order group. Completing a cart may yield one order or several, and a customer's order history should show the group as one purchase rather than confronting them with three orders they don't remember placing separately.
245
+ >
246
+ > A back office must handle it too: an order raised from the admin divides the same way, so completing one may answer with a group instead of an order.
247
+
248
+
249
+ ```typescript Admin SDK
250
+ import { isOrderGroup } from '@spree/admin-sdk'
251
+
252
+ const result = await client.orders.complete('or_UkLWZg9DAJ')
253
+
254
+ // One order per seller when the order held several sellers' goods.
255
+ const orders = isOrderGroup(result) ? result.orders : [result]
256
+ ```
257
+
258
+ ```bash cURL
259
+ curl -X PATCH https://your-store.com/api/v3/admin/orders/or_UkLWZg9DAJ/complete \
260
+ -H "X-Spree-API-Key: sk_xxx" \
261
+ -H "Content-Type: application/json"
262
+ ```
263
+
245
264
 
246
265
  ## Commission
247
266
 
@@ -308,18 +327,22 @@ The scheduler is the host app's. Two jobs fan out per seller and the interval lo
308
327
  | `Spree::SellerPayouts::SweepDueJob` | daily | settles every seller who is due |
309
328
  | `Spree::SellerTransfers::ExecutePendingDueJob` | hourly | retries earnings a provider refused — usually a passing thing |
310
329
 
311
- > **WARNING:** **You have to schedule these yourself** — a new project does not ship them, so
312
- > a marketplace that skips this step never pays a seller. Add both to your
313
- > scheduler; with Solid Queue that is `server/config/recurring.yml`:
330
+ A project created with `create-spree-app` schedules both already, in
331
+ `server/config/recurring.yml`. They are a cheap no-op on a store with no
332
+ sellers, so there is nothing to turn off if you are not running a marketplace.
333
+
334
+ > **WARNING:** On an existing project, or one using a scheduler of its own, check both are
335
+ > scheduled — a marketplace that never runs the sweep never pays a seller, and
336
+ > nothing reports the omission.
314
337
  >
315
338
  > ```yaml server/config/recurring.yml
316
339
  production:
317
340
  sweep_due_seller_payouts:
318
341
  class: Spree::SellerPayouts::SweepDueJob
319
- schedule: every day at 3am
342
+ schedule: at 3am every day
320
343
  execute_pending_seller_transfers:
321
344
  class: Spree::SellerTransfers::ExecutePendingDueJob
322
- schedule: every hour
345
+ schedule: every hour at minute 27
323
346
  ```
324
347
 
325
348
  A seller on the `manual` interval is skipped by the sweep and settled by hand instead:
@@ -142,7 +142,7 @@ If you included the storefront, start it in a separate terminal:
142
142
 
143
143
  ```bash
144
144
  cd my-store/apps/storefront
145
- npm run dev
145
+ pnpm dev
146
146
  ```
147
147
 
148
148
  Open [http://localhost:3001](http://localhost:3001) to see your store.
@@ -0,0 +1,161 @@
1
+ ---
2
+ title: Theming
3
+ description: Restyle the dashboard by overriding its design tokens — colours, radius, fonts and the type scale — without forking a single component.
4
+ ---
5
+
6
+ The dashboard is styled entirely from CSS custom properties. Components read
7
+ tokens rather than hard-coded colours, so changing a token restyles everything
8
+ that uses it — you never fork a component to recolour it.
9
+
10
+ ## Where your styles go
11
+
12
+ Your app's `src/styles.css` imports the Spree stylesheet. Anything below that
13
+ import wins, because it lands later at the same specificity:
14
+
15
+ ```css src/styles.css
16
+ @import "@spree/dashboard/styles.css";
17
+
18
+ :root {
19
+ --primary: oklch(0.55 0.22 264);
20
+ --radius: 0.25rem;
21
+ }
22
+ ```
23
+
24
+ That is the whole mechanism. No build config, no plugin, no component overrides.
25
+
26
+ > **NOTE:** The seller panel works the same way — its starter imports
27
+ > `@spree/seller-dashboard/styles.css`, and the same tokens apply.
28
+
29
+ ## Dark mode
30
+
31
+ Every token has a dark counterpart under `.dark`. A theme is only complete when
32
+ you set both:
33
+
34
+ ```css src/styles.css
35
+ @import "@spree/dashboard/styles.css";
36
+
37
+ :root {
38
+ --primary: oklch(0.55 0.22 264);
39
+ }
40
+
41
+ .dark {
42
+ --primary: oklch(0.72 0.19 264);
43
+ }
44
+ ```
45
+
46
+ > **WARNING:** The theme provider decides *mode* — light, dark or system — and toggles a
47
+ > `.dark` class. It holds no colours. Override only `:root` and your theme
48
+ > breaks the moment someone switches to dark.
49
+
50
+ ## The tokens
51
+
52
+ ### Surfaces and text
53
+
54
+ | Token | What it colours |
55
+ |---|---|
56
+ | `--background` / `--foreground` | The page, and text on it |
57
+ | `--card` / `--card-foreground` | Cards, and text on them |
58
+ | `--popover` / `--popover-foreground` | Menus, dropdowns, popovers |
59
+ | `--muted` / `--muted-foreground` | Recessed surfaces and secondary text |
60
+
61
+ ### Brand and interaction
62
+
63
+ | Token | What it colours |
64
+ |---|---|
65
+ | `--primary` / `--primary-foreground` | Primary buttons and calls to action |
66
+ | `--secondary` / `--secondary-foreground` | Secondary buttons |
67
+ | `--accent` / `--accent-foreground` | Hovered rows, selected items |
68
+ | `--accent-hover`, `--accent-strong`, `--accent-strong-hover` | Deeper accent steps |
69
+ | `--link` / `--link-hover` | Inline text links |
70
+ | `--ring` | Focus rings |
71
+
72
+ ### Status
73
+
74
+ `--destructive` and `--destructive-foreground` for dangerous actions, plus four
75
+ status families used by badges and callouts — each with `-bg`, `-border` and
76
+ `-fg`:
77
+
78
+ `--status-green-*`, `--status-amber-*`, `--status-red-*`, `--status-blue-*`
79
+
80
+ ### Borders and structure
81
+
82
+ | Token | What it colours |
83
+ |---|---|
84
+ | `--border` | The default border, aliasing `--border-base` |
85
+ | `--border-card`, `--border-subtle`, `--border-control` | Cards, dividers, inputs |
86
+ | `--input` | Input borders |
87
+ | `--radius` | Corner radius everywhere |
88
+
89
+ ### Sidebar
90
+
91
+ The sidebar has its own family so you can give the chrome a different tone from
92
+ the content: `--sidebar`, `--sidebar-foreground`, `--sidebar-primary`,
93
+ `--sidebar-accent` and its hover steps, `--sidebar-border`, `--sidebar-ring`.
94
+
95
+ ### Charts
96
+
97
+ `--chart-1` through `--chart-5`, in series order.
98
+
99
+ ## Tokens defined from other tokens
100
+
101
+ Several tokens alias another rather than naming their own colour:
102
+
103
+ ```css
104
+ --primary: var(--foreground);
105
+ --popover: var(--card);
106
+ --accent: var(--secondary);
107
+ --muted: var(--background);
108
+ ```
109
+
110
+ So overriding one base token cascades. Setting `--foreground` alone also moves
111
+ primary buttons, because that is where `--primary` points. Override the alias
112
+ directly when you want them to diverge.
113
+
114
+ ## Fonts and the type scale
115
+
116
+ Typography is set in a `@theme inline` block and overridden the same way:
117
+
118
+ ```css src/styles.css
119
+ @import "@spree/dashboard/styles.css";
120
+
121
+ @theme inline {
122
+ --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
123
+ --text-base: 0.9375rem;
124
+ }
125
+ ```
126
+
127
+ The dashboard's scale is deliberately smaller than Tailwind's default, because
128
+ admin screens are dense. Raising `--text-base` raises everything built on it.
129
+
130
+ ## Using tokens in your own pages
131
+
132
+ Tailwind maps every token through `@theme inline`, so utilities follow your
133
+ values with no extra work:
134
+
135
+ ```tsx
136
+ <div className="bg-card text-card-foreground border-border rounded-lg border">
137
+ <p className="text-muted-foreground">Follows your theme.</p>
138
+ </div>
139
+ ```
140
+
141
+ Use the utilities rather than raw `var(--token)` — a page built from
142
+ `bg-card` and `text-muted-foreground` inherits every future change, including
143
+ dark mode.
144
+
145
+ For inline text links there is a `.link` class carrying the canonical treatment:
146
+
147
+ ```tsx
148
+ <Link to="/$storeId/settings/store" params={{ storeId }} className="link">
149
+ Store settings
150
+ </Link>
151
+ ```
152
+
153
+ It is for links in prose and help text. A link that is really a table row, a
154
+ breadcrumb, a nav item or a button rendered as an anchor carries its own
155
+ styling, and `.link` would fight it.
156
+
157
+ ## Related
158
+
159
+ - [Dashboard overview](../overview.md) — the three packages and what each is for
160
+ - [Translations](translations.md) — the other half of white-labelling
161
+ - [Slots](slots.md) — adding your own UI to existing screens
@@ -44,7 +44,7 @@ Your copy of the dashboard is a small Vite app (the "host app") that imports `<D
44
44
  - **New project:** `npx create-spree-app my-store` — answer **Yes** to "Include React Dashboard?" and it lands in `apps/dashboard/`, pointed at your API.
45
45
  - **Existing project:** run `npx spree add dashboard` from your project root — same result.
46
46
 
47
- Either way: `cd apps/dashboard && npm run dev`, open http://localhost:5173, and sign in with your admin email and password. No API keys to configure — admins authenticate interactively.
47
+ Either way: `cd apps/dashboard && pnpm dev`, open http://localhost:5173, and sign in with your admin email and password. No API keys to configure — admins authenticate interactively.
48
48
 
49
49
  ## Choose your path
50
50
 
@@ -34,11 +34,12 @@ production:
34
34
 
35
35
  ### Marketplace payouts
36
36
 
37
- A marketplace has to schedule one more job, or its sellers are never paid:
37
+ A marketplace settles its sellers on a recurring job, which a new project
38
+ already schedules:
38
39
 
39
40
  ```yaml config/recurring.yml
40
41
  production:
41
- sweep_seller_payouts:
42
+ sweep_due_seller_payouts:
42
43
  class: Spree::SellerPayouts::SweepDueJob
43
44
  schedule: at 3am every day
44
45
  ```
@@ -141,7 +141,7 @@ When `SMTP_HOST` is not set, emails are printed to the Rails log instead of bein
141
141
  In development, no email provider is needed. Emails are rendered to HTML files in `.next/emails/` with a clickable `file://` link in the console. To preview and design templates:
142
142
 
143
143
  ```bash
144
- npm run email:dev
144
+ pnpm email:dev
145
145
  ```
146
146
 
147
147
  To test the full webhook flow locally, use [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/):
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  title: Build a Multi-Vendor Marketplace
3
- description: Marketplace capabilities are part of Spree core — sellers, order splitting, commissions and payouts, without a separate extension.
3
+ description: Learn how to extremely fast launch a marketplace on Spree
4
4
  ---
5
5
 
6
- This guide covers the open-source core.
6
+ Spree fully supports multi-vendor marketplaces out of the box: sellers are first-class records, orders split per seller at completion, robust commission engine, and a payout ledger with pluggable payout providers.
7
7
 
8
- Spree brings multi-vendor marketplace support into the open-source core: sellers as first-class records, orders split per seller at completion, a commission engine, and a payout ledger with pluggable payout providers.
8
+ So building a marketplace doesn't require weeks or months of development, it's just configuring your Spree application to admit sellers, review their submissions, and pay them. In most cases you don't need to customize Spree or write any code at all. This guide is a walkthrough of all elements that make a marketplace, and how to configure them.
9
9
 
10
- ## What you will build
10
+ ## What you get
11
11
 
12
12
  - **Sellers** — onboard sellers with an operator-configured requirements checklist, review submissions, and approve them for selling.
13
13
  - **Order splitting** — a customer checks out once; completion produces per-seller orders grouped under one purchase.
@@ -15,11 +15,750 @@ Spree brings multi-vendor marketplace support into the open-source core: sellers
15
15
  - **Payouts** — a transfer and payout ledger, with Stripe Connect payouts (Express onboarding, on-fulfillment transfers) shipping in the open-source monorepo.
16
16
  - **Seller operations** — sellers work through their own dedicated API surface, scoped so a seller only ever sees their own trade.
17
17
 
18
- ## Outline
18
+ ## Before you start
19
19
 
20
- 1. Enable marketplace capabilities and configure the store
21
- 2. Define seller onboarding requirements
22
- 3. Invite and approve sellers
23
- 4. Assign catalog to sellers and take a multi-seller order through checkout
24
- 5. Configure commissions
25
- 6. Connect Stripe for payouts
20
+ You need a running Spree 6 project and admin credentials. The marketplace surfaces are:
21
+
22
+ | Surface | Who uses it | Package |
23
+ |---|---|---|
24
+ | Admin dashboard | The marketplace operator | [`@spree/dashboard`](../dashboard/overview.md) |
25
+ | Seller panel | Each seller's own team | [`@spree/seller-dashboard`](#step-7-stand-up-the-seller-panel) |
26
+ | Admin API | Operator-side integrations | [`@spree/admin-sdk`](../../api-reference/admin-api/introduction.md) |
27
+ | Seller API | A signed-in seller only | [`@spree/seller-sdk`](../../api-reference/seller-api/introduction.md) |
28
+
29
+ To try the flow quickly, you can seed a sample seller with an owner account, a pending invitation and a fund ledger:
30
+
31
+
32
+ ```bash Spree CLI
33
+ spree run rake spree:sellers:sample_data
34
+ ```
35
+
36
+ ```bash Without CLI
37
+ bin/rails spree:sellers:sample_data
38
+ ```
39
+
40
+
41
+ The task refuses to run outside development and test, because it writes a known password. It signs in at `seller@example.com` / `spree123` — override with `SELLER_EMAIL`, `SELLER_PASSWORD` and `SELLER_NAME`.
42
+
43
+ ## Configure the store for marketplace trading
44
+
45
+ There is no single "marketplace mode" switch. A store becomes a marketplace when it has sellers; what you configure up front is how strictly it admits them and how it pays them.
46
+
47
+ ### Payout settings
48
+
49
+ Three store preferences decide how sellers get paid. All three are writable through the Admin API, and they appear in the dashboard under **Settings → Marketplace**.
50
+
51
+ | Preference | Default | What it decides |
52
+ |---|---|---|
53
+ | `preferred_payout_provider` | blank | Who moves the money. Blank means core's record-only provider: the ledger is kept correctly and the operator settles by hand. |
54
+ | `preferred_default_payouts_schedule_interval` | `monthly` | How often sellers are settled. One of `daily`, `weekly`, `biweekly`, `monthly`, `manual`. A seller can carry its own interval, which wins. |
55
+ | `preferred_default_minimum_payout_amount` | `0` | What a seller's balance must reach before a settlement is worth sending. Below it the balance carries to the next period. |
56
+
57
+
58
+ ```typescript Admin SDK
59
+ await adminClient.store.update({
60
+ preferred_default_payouts_schedule_interval: 'weekly',
61
+ preferred_default_minimum_payout_amount: 25,
62
+ })
63
+ ```
64
+
65
+ ```bash cURL
66
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/store' \
67
+ -H 'X-Spree-API-Key: sk_xxx' \
68
+ -H 'Content-Type: application/json' \
69
+ -d '{
70
+ "preferred_default_payouts_schedule_interval": "weekly",
71
+ "preferred_default_minimum_payout_amount": 25
72
+ }'
73
+ ```
74
+
75
+
76
+ A mistyped provider key is refused at write time rather than silently ignored, so you cannot end up with a bookkeeping-only ledger and no indication that your choice was dropped. Ask the API which providers are registered before you set one:
77
+
78
+ ```bash
79
+ curl 'https://api.mystore.com/api/v3/admin/payout_providers' \
80
+ -H 'X-Spree-API-Key: sk_xxx'
81
+ ```
82
+
83
+ ### Admission and review settings
84
+
85
+ Four more preferences decide who gets to trade and what they hear from you. They
86
+ sit on the same **Settings → Marketplace** screen, and are writable through the
87
+ Admin API like the payout ones.
88
+
89
+ | Preference | Default | What it decides |
90
+ |---|---|---|
91
+ | `preferred_auto_approve_sellers` | `false` | Admit a seller the moment they finish the checklist, with nobody looking at them. |
92
+ | `preferred_auto_approve_seller_products` | `false` | Put a seller's product on sale the moment they submit it. |
93
+ | `preferred_send_seller_transactional_emails` | `true` | Whether Spree emails sellers. Turn it off if you front seller communications yourself. |
94
+ | `preferred_default_commission_tax_rate` | `0` | Tax on your commission when neither the rate nor the tax provider names one. |
95
+
96
+
97
+ ```typescript Admin SDK
98
+ await adminClient.store.update({
99
+ preferred_auto_approve_sellers: false,
100
+ preferred_auto_approve_seller_products: false,
101
+ preferred_send_seller_transactional_emails: true,
102
+ preferred_default_commission_tax_rate: 0.23,
103
+ })
104
+ ```
105
+
106
+ ```bash cURL
107
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/store' \
108
+ -H 'X-Spree-API-Key: sk_xxx' \
109
+ -H 'Content-Type: application/json' \
110
+ -d '{
111
+ "preferred_auto_approve_sellers": false,
112
+ "preferred_auto_approve_seller_products": false,
113
+ "preferred_send_seller_transactional_emails": true,
114
+ "preferred_default_commission_tax_rate": 0.23
115
+ }'
116
+ ```
117
+
118
+
119
+ > **WARNING:** Turning either auto-approve preference on removes a human decision from your marketplace. `auto_approve_sellers` lets any applicant who ticks every box start trading; `auto_approve_seller_products` puts their listings on sale without review. Both are appropriate for an invite-only marketplace where you already trust the sellers, and dangerous for an open one.
120
+
121
+ Commission tax is stored as a fraction — `0.23` means 23%. The dashboard asks for
122
+ a percentage and converts, so you only meet the fraction through the API. See
123
+ [Tax on commission](../core-concepts/commissions.md#tax-on-commission) for
124
+ when it applies and what overrides it.
125
+
126
+ ## Define what a seller must do before trading
127
+
128
+ A seller requirement is one thing the marketplace asks of a seller before it will let them trade. The operator composes the checklist from registered **kinds** — rows are configuration, and only a genuinely new kind of check needs code.
129
+
130
+ The checklist is enforced in exactly two places: when the seller asks to be reviewed, and when the operator approves them. Nothing else consults it.
131
+
132
+ ### The kinds that ship
133
+
134
+ Thirteen kinds are registered by core. Seven of them are provisioned into a new store's default checklist, in the order a seller meets them.
135
+
136
+ | `type` | What it asks | Default? | Seller submits? | Operator reviews? |
137
+ |---|---|:---:|:---:|:---:|
138
+ | `accept_terms` | Read and accept the marketplace terms | ✅ | — | — |
139
+ | `complete_profile` | Fill in the public profile shoppers see | ✅ | — | — |
140
+ | `billing_address` | The address commission invoices are addressed to | ✅ | — | — |
141
+ | `returns_address` | Where customer returns are sent | ✅ | — | — |
142
+ | `delivery_method` | At least one way to ship | ✅ | — | — |
143
+ | `package_type` | The box they ship orders in, with measurements | ✅ | — | — |
144
+ | `minimum_products` | List at least this many products | ✅ | — | — |
145
+ | `payout_account` | An account the payout provider will pay | — | — | — |
146
+ | `required_custom_fields` | Named custom fields filled in (VAT number, registration number) | — | — | — |
147
+ | `policy` | Publish a policy with this name | — | — | — |
148
+ | `attestation` | Confirm something in their own right | — | ✅ | — |
149
+ | `operator_review` | Something a person at the marketplace checks by hand | — | ✅ | ✅ |
150
+ | `document` | Upload a file for the marketplace to review | — | ✅ | ✅ |
151
+
152
+ The first ten are **computed**: Spree reads the seller's own data and answers for itself, so there is nothing for the seller to submit. The last three take a **submission** — a row recording what the seller said and when — and the bottom two wait for someone to accept it.
153
+
154
+ Four kinds may be configured more than once per store, because their meaning comes from the operator's own wording: `attestation`, `operator_review`, `document` and `policy`. Those four require a `name`. The rest are one per store.
155
+
156
+ > **NOTE:** The generic kinds are deliberately absent from the default checklist. An attestation or a document means nothing until the operator has written what they are asking for, so Spree does not guess.
157
+
158
+ ### Build the checklist from the registry, never a hardcoded list
159
+
160
+ Always read the kinds from `types()` rather than shipping your own list — a marketplace's own registered kinds then appear for free. The endpoint returns each kind's `preference_schema`, which is what a configuration form renders from.
161
+
162
+
163
+ ```typescript Admin SDK
164
+ // What an operator can add, and the shape of each one's config form
165
+ const { data: kinds } = await adminClient.sellerRequirements.types()
166
+
167
+ // Ask every seller for a business registration
168
+ await adminClient.sellerRequirements.create({
169
+ type: 'document',
170
+ name: 'Business registration',
171
+ description: 'A certificate of incorporation or equivalent.',
172
+ required: true,
173
+ })
174
+
175
+ // Collect a VAT number through a custom field definition on Spree::Seller
176
+ await adminClient.sellerRequirements.create({
177
+ type: 'required_custom_fields',
178
+ custom_field_definition_ids: ['cfd_xxx'],
179
+ required: true,
180
+ })
181
+ ```
182
+
183
+ ```bash cURL
184
+ curl 'https://api.mystore.com/api/v3/admin/seller_requirements/types' \
185
+ -H 'X-Spree-API-Key: sk_xxx'
186
+
187
+ curl -X POST 'https://api.mystore.com/api/v3/admin/seller_requirements' \
188
+ -H 'X-Spree-API-Key: sk_xxx' \
189
+ -H 'Content-Type: application/json' \
190
+ -d '{
191
+ "type": "document",
192
+ "name": "Business registration",
193
+ "description": "A certificate of incorporation or equivalent.",
194
+ "required": true
195
+ }'
196
+ ```
197
+
198
+
199
+ Each kind takes its own configuration through `preferences`. `accept_terms` carries `terms_body`, `terms_url` and `terms_effective_from` — setting that date is how you ask everyone again after rewriting the terms, since anyone who accepted before it falls back to unmet. `minimum_products` carries `minimum_count`. `complete_profile` carries `require_about`, `require_logo`, `require_cover_photo` and `require_contact_email`.
200
+
201
+ Two flags govern every row regardless of kind. `required: false` makes a line advisory — it appears on the seller's checklist but does not gate approval. `active: false` retires it without deleting the submissions that answered it. Position is the order sellers work through, and the list is drag-ordered in the dashboard.
202
+
203
+ > **NOTE:** `type` is write-once. The API strips it on update, because a saved row's submissions answered the old kind. To change a kind, delete the row and create a new one.
204
+
205
+ **Read more:** [Onboarding requirements](../core-concepts/sellers.md#onboarding-requirements) for the model, and [Seller Requirements](/user/settings/seller-requirements) for the operator's screen.
206
+
207
+ ## Invite a seller and approve them
208
+
209
+ A seller moves through its life on the marketplace by **workflow**, never by assigning a status. Each transition is its own Admin API action because each carries its own arguments, sends its own mail and runs its own extension hooks — mass assignment would skip all three.
210
+
211
+ ```mermaid
212
+ stateDiagram-v2
213
+ [*] --> pending : sellers.create
214
+ pending --> invited : sellers.invite
215
+ invited --> onboarding : invitation accepted
216
+ onboarding --> ready_for_review : seller submits
217
+ ready_for_review --> approved : sellers.approve
218
+ ready_for_review --> onboarding : sellers.reopenOnboarding
219
+ onboarding --> rejected : sellers.reject
220
+ ready_for_review --> rejected : sellers.reject
221
+ rejected --> approved : sellers.approve
222
+ approved --> suspended : sellers.suspend
223
+ suspended --> approved : sellers.approve
224
+ ```
225
+
226
+ | Status | Meaning |
227
+ |---|---|
228
+ | `pending` | Created; nothing sent yet |
229
+ | `invited` | Someone has been asked to run it, and has not accepted |
230
+ | `onboarding` | Somebody accepted and is working through the checklist |
231
+ | `ready_for_review` | The seller says they are done; waiting on the operator |
232
+ | `approved` | Trading |
233
+ | `rejected` | An applicant turned away |
234
+ | `suspended` | A trading seller halted; reversible |
235
+ | `canceled` | Closed |
236
+
237
+ ### Create and invite
238
+
239
+ Creating a seller through the workflow provisions a stock location for them, which is where their inventory lives and where customer returns land. A seller without one cannot finish onboarding, so this is not a step an operator can forget.
240
+
241
+
242
+ ```typescript Admin SDK
243
+ const seller = await adminClient.sellers.create({
244
+ name: 'Bright Sparks',
245
+ contact_email: 'hello@brightsparks.example',
246
+ })
247
+
248
+ // Opens the seller's own team to someone; they join when they accept
249
+ await adminClient.sellers.invite(seller.id, { email: 'owner@brightsparks.example' })
250
+ ```
251
+
252
+ ```bash cURL
253
+ curl -X POST 'https://api.mystore.com/api/v3/admin/sellers' \
254
+ -H 'X-Spree-API-Key: sk_xxx' \
255
+ -H 'Content-Type: application/json' \
256
+ -d '{ "name": "Bright Sparks", "contact_email": "hello@brightsparks.example" }'
257
+
258
+ curl -X POST 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/invite' \
259
+ -H 'X-Spree-API-Key: sk_xxx' \
260
+ -H 'Content-Type: application/json' \
261
+ -d '{ "email": "owner@brightsparks.example" }'
262
+ ```
263
+
264
+
265
+ The invited person accepts through the seller panel — the emailed link carries a prefixed ID and a token, which together are the credential. Accepting creates the membership and starts onboarding, so the seller reaches `onboarding` without the operator doing anything more.
266
+
267
+ Re-inviting is deliberate: invitations expire, and the first one goes to the wrong address often enough. What cannot be re-opened is a seller already trading or already turned away.
268
+
269
+ `role_id` names a role the seller owns; leave it out and they accept into the seller's own admin role. A seller's roles are the same permission machinery the store's back office uses, pointed at the seller — see [Staff & Roles](../core-concepts/staff-roles.md).
270
+
271
+ ### Review submissions
272
+
273
+ The two reviewed kinds (`operator_review`, `document`) leave a submission for someone to decide. Accepting one satisfies that line of the checklist; rejecting sends it back with a note the seller reads.
274
+
275
+
276
+ ```typescript Admin SDK
277
+ const { data: submissions } = await adminClient.sellers.requirementSubmissions.list('sel_xxx')
278
+
279
+ await adminClient.sellers.requirementSubmissions.accept('sel_xxx', 'selsub_xxx')
280
+
281
+ await adminClient.sellers.requirementSubmissions.reject('sel_xxx', 'selsub_xxx', {
282
+ review_note: 'The certificate has expired — please upload a current one.',
283
+ })
284
+
285
+ // Excuse this seller from something the store asks of everyone
286
+ await adminClient.sellers.requirementSubmissions.waive('sel_xxx', {
287
+ requirement_id: 'selreq_xxx',
288
+ review_note: 'Verified against Companies House by hand.',
289
+ })
290
+ ```
291
+
292
+ ```bash cURL
293
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/requirement_submissions/selsub_xxx/reject' \
294
+ -H 'X-Spree-API-Key: sk_xxx' \
295
+ -H 'Content-Type: application/json' \
296
+ -d '{ "review_note": "The certificate has expired — please upload a current one." }'
297
+ ```
298
+
299
+
300
+ A submission is `pending`, `accepted`, `rejected` or `waived`. A waiver is the operator recording that they dealt with this outside the marketplace — it reads as met without pretending the seller did it. Submissions accumulate rather than overwrite: the latest row for a (seller, requirement) pair decides the standing, and the ones before it are the record of how you got there.
301
+
302
+ Uploaded documents are stored privately and served only through the admin and seller branches. They are business registrations and identity documents, and Spree identifies them from their bytes rather than trusting the uploaded filename — a script named `certificate.pdf` is refused.
303
+
304
+ ### Approve, suspend, reject
305
+
306
+
307
+ ```typescript Admin SDK
308
+ // Where this seller stands against the checklist
309
+ const onboarding = await adminClient.sellers.onboarding('sel_xxx')
310
+ // → { status, progress: { done, total }, requirements: [...] }
311
+
312
+ await adminClient.sellers.approve('sel_xxx')
313
+
314
+ // Admit them anyway, despite an unfinished checklist
315
+ await adminClient.sellers.approve('sel_xxx', { override_requirements: true })
316
+
317
+ // Send them back to fix something
318
+ await adminClient.sellers.reopenOnboarding('sel_xxx', {
319
+ note: 'Your returns address is a PO box — we need a street address.',
320
+ })
321
+
322
+ await adminClient.sellers.suspend('sel_xxx', { reason: 'Unresolved delivery complaints' })
323
+ await adminClient.sellers.reject('sel_xxx', { reason: 'Outside our categories' })
324
+ ```
325
+
326
+ ```bash cURL
327
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/approve' \
328
+ -H 'X-Spree-API-Key: sk_xxx' \
329
+ -H 'Content-Type: application/json' \
330
+ -d '{ "override_requirements": true }'
331
+ ```
332
+
333
+
334
+ Approval is refused while a required requirement is unmet, unless `override_requirements` says the operator means it. The override is recorded on the `seller.approved` event along with what was outstanding — the operator can step over the checklist, but not without saying so.
335
+
336
+ **Suspend and reject are not the same thing.** Reject turns away an applicant who never traded; it refuses an approved seller outright. Suspend halts a trading seller: their catalog stops selling, their record and history stay, and `approve` is the way back. A suspended seller being reinstated is not re-measured against the checklist, because they were admitted once already.
337
+
338
+ A seller can also take themselves off sale without any operator involvement, by setting `holiday_mode_until`. The catalog stays visible; what stops is selling.
339
+
340
+ **Read more:** [The seller lifecycle](../core-concepts/sellers.md#the-seller-lifecycle), [Onboarding](/user/sellers/onboarding), [Managing sellers](/user/sellers/managing-sellers).
341
+
342
+ ## Review what sellers list
343
+
344
+ Products belong to sellers through `seller_id` on `Spree::Product`. A product with no seller is the marketplace's own.
345
+
346
+ A marketplace adds two statuses to the product lifecycle that a single-merchant store never sees: `proposed` and `rejected`. A seller never assigns a status — they ask, and the marketplace decides.
347
+
348
+ ```mermaid
349
+ stateDiagram-v2
350
+ draft --> proposed : seller submits
351
+ proposed --> active : operator approves
352
+ proposed --> rejected : operator rejects
353
+ rejected --> proposed : seller resubmits
354
+ active --> draft : seller takes it down
355
+ active --> archived : seller withdraws it
356
+ ```
357
+
358
+ | Status | Meaning | Storefront |
359
+ |---|---|:---:|
360
+ | `draft` | The seller is still working on it | Hidden |
361
+ | `proposed` | Submitted, waiting on the marketplace | Hidden |
362
+ | `rejected` | Sent back with a reason, awaiting changes | Hidden |
363
+ | `active` | Approved and on sale | **Visible** |
364
+ | `archived` | Withdrawn | Hidden |
365
+
366
+ A seller can always take their own listing down — that is not a review decision. Putting one up is.
367
+
368
+ ### How it relates to the submission record
369
+
370
+ The product's `status` is the operational truth; a `Spree::ProductSubmission` row is the record of how it got there — who asked, who decided, when, and what they told the seller. Its statuses are `pending`, `approved`, `rejected` and `withdrawn`.
371
+
372
+ Rows accumulate: a seller sent back three times leaves three rows, and the latest one is live. Taking a listing back to `draft` before anyone ruled on it closes the open row as `withdrawn`, so a `pending` row always means the marketplace still owes an answer rather than "abandoned".
373
+
374
+ > **WARNING:** A rejection reason belongs on the submission, never on the product. A seller can write their own product's `metadata`, so a note kept there is erased the next time they save. Send it as `reason` to the reject endpoint and Spree puts it where the seller cannot overwrite it.
375
+
376
+ ### Both sides of the review
377
+
378
+
379
+ ```typescript Seller SDK
380
+ // The seller asks for their listing to go on sale
381
+ await sellerClient.products.submit(productId)
382
+
383
+ // Or a selection of them
384
+ await sellerClient.products.bulkSubmit({ ids: [id1, id2] })
385
+ ```
386
+
387
+ ```typescript Admin SDK
388
+ // The operator decides
389
+ await adminClient.products.approve('prod_xxx')
390
+ await adminClient.products.reject('prod_xxx', {
391
+ reason: 'The lead photo has a competitor watermark.',
392
+ })
393
+ ```
394
+
395
+ ```bash cURL
396
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/products/prod_xxx/reject' \
397
+ -H 'X-Spree-API-Key: sk_xxx' \
398
+ -H 'Content-Type: application/json' \
399
+ -d '{ "reason": "The lead photo has a competitor watermark." }'
400
+ ```
401
+
402
+
403
+ Filter the review queue with a Ransack query on status: `adminClient.products.list({ status_eq: 'proposed' })`.
404
+
405
+ With `preferred_auto_approve_seller_products` on, submitting chains straight into approval. The submission row still gets written and carries an `auto_approved` marker, so a blank reviewer reads as "this store does not review listings" rather than as a lost name.
406
+
407
+ There is no bulk route onto `active` for sellers, and that is deliberate: reaching it is the operator's decision on one listing at a time.
408
+
409
+ **Read more:** [Seller submissions](../core-concepts/products.md#seller-submissions), [Seller products](/user/sellers/products).
410
+
411
+ ## Set what the marketplace charges
412
+
413
+ Commission is what the marketplace takes from a sale. You configure **rates**; Spree records what was actually charged as immutable **commission lines**, frozen when the order was placed.
414
+
415
+ ### Rates and precedence
416
+
417
+ **The list is the precedence.** Rates are walked top-down and the first whose targeting matches the sale wins. A rate with no rules matches every sale, so anything below it is unreachable — the marketplace default belongs at the bottom of the list. A new rate is placed at the top by default, ahead of anything more general already there.
418
+
419
+
420
+ ```typescript Admin SDK
421
+ // Discover the rule kinds this marketplace has registered
422
+ const { data: ruleTypes } = await adminClient.commissionRates.ruleTypes()
423
+
424
+ // 8% on one seller's electronics, with a floor and a cap per currency
425
+ await adminClient.commissionRates.create({
426
+ name: 'Electronics — Bright Sparks',
427
+ kind: 'percentage',
428
+ value: 8,
429
+ rules: [
430
+ { type: 'seller_rule', preferences: { seller_ids: ['sel_xxx'] } },
431
+ ],
432
+ })
433
+
434
+ // The marketplace default, at the bottom, matching everything
435
+ await adminClient.commissionRates.create({
436
+ name: 'Standard commission',
437
+ kind: 'percentage',
438
+ value: 15,
439
+ position: 99,
440
+ })
441
+ ```
442
+
443
+ ```bash cURL
444
+ curl -X POST 'https://api.mystore.com/api/v3/admin/commission_rates' \
445
+ -H 'X-Spree-API-Key: sk_xxx' \
446
+ -H 'Content-Type: application/json' \
447
+ -d '{ "name": "Standard commission", "kind": "percentage", "value": 15, "position": 99 }'
448
+ ```
449
+
450
+
451
+ A rate is either `percentage` (a share of the sale) or `fixed` (a flat fee). A flat fee states its `amounts` per currency — a rate is skipped for a currency it names no amount in, so that sale falls through to the next matching rate rather than being charged a converted figure nobody set.
452
+
453
+ Rules ride the regular payload as `rules: [...]`, and the server replaces the rate's rules with exactly what it is sent. Every rule must hold for the rate to apply, and a rule naming several records means any of them — so "(Cameras OR Audio) AND that seller" is a category rule holding two ids beside a seller rule holding one.
454
+
455
+ ### Lines are read-only
456
+
457
+
458
+ ```typescript Admin SDK
459
+ const { data: lines } = await adminClient.commissionLines.list({ seller_id_eq: 'sel_xxx' })
460
+ ```
461
+
462
+ ```bash cURL
463
+ curl 'https://api.mystore.com/api/v3/admin/commission_lines?q[seller_id_eq]=sel_xxx' \
464
+ -H 'X-Spree-API-Key: sk_xxx'
465
+ ```
466
+
467
+
468
+ There is no write path. Correcting a charge is a reversal, not an edit.
469
+
470
+ In the EU the fee is a separate supply from the sale, so it is taxed separately — that is what `preferred_default_commission_tax_rate` from step 1 is for, and a rate or a tax provider can name its own.
471
+
472
+ **Read more:** [Commissions](../core-concepts/commissions.md) covers the four rule types, gross-versus-net, how delivery is treated, currency floors and caps, and commission tax in full. [Commission rates](/user/sellers/commission-rates) is the operator's screen.
473
+
474
+ ## Understand what a split checkout produces
475
+
476
+ This is the part that makes a marketplace different from a shop, and a storefront has to handle it.
477
+
478
+ A customer fills one basket, enters one address, and pays once. But each seller needs their own order — they fulfil separately, get paid separately, and must never see each other's business. So at completion, a checkout spanning several sellers becomes an **order group**: one container holding one order per seller.
479
+
480
+ ```mermaid
481
+ erDiagram
482
+ OrderGroup ||--o{ Order : "one per seller"
483
+ OrderGroup ||--o{ Payment : "one payment"
484
+ Payment ||--o{ PaymentSplit : "apportioned"
485
+ Order }o--|| Seller : "belongs to"
486
+ Order ||--o{ Fulfillment : "shipped by that seller"
487
+ ```
488
+
489
+ | Level | Owns |
490
+ |---|---|
491
+ | **Order group** | The customer, the addresses, the payment, the combined totals |
492
+ | **Order** | One seller's items, fulfillments and money lines |
493
+
494
+ The single payment is apportioned across the child orders as **payment splits**, so each seller's share of one charge is recorded exactly — which is what makes per-seller refunds and settlement possible later.
495
+
496
+ Two details worth knowing:
497
+
498
+ - **Group totals are added up, not divided.** The group's total is the sum of its children, so it always agrees with them.
499
+ - **Delivery and order-level fees are shared out by item value**, so a seller whose goods made up most of the basket carries most of the delivery charge.
500
+
501
+ > **WARNING:** **A storefront must handle the possibility of an order group.** Completing a cart yields either one order or a group, depending on how many sellers the basket reached. A customer's order history should show the group as one purchase rather than confronting them with three orders they don't remember placing separately.
502
+
503
+ A single-seller checkout produces no group at all — just the order, exactly as a normal store does. Nothing about a marketplace changes the shape of a single-seller sale.
504
+
505
+ Operators can read groups back; there is no write path, because everything an operator acts on lives on the orders inside them.
506
+
507
+
508
+ ```typescript Admin SDK
509
+ const { data: groups } = await adminClient.orderGroups.list()
510
+ const group = await adminClient.orderGroups.get('ogrp_xxx', { expand: ['orders'] })
511
+ ```
512
+
513
+ ```bash cURL
514
+ curl 'https://api.mystore.com/api/v3/admin/order_groups/ogrp_xxx?expand=orders' \
515
+ -H 'X-Spree-API-Key: sk_xxx'
516
+ ```
517
+
518
+
519
+ **Read more:** [One checkout, several sellers](../core-concepts/sellers.md#one-checkout-several-sellers), [Orders](../core-concepts/orders.md).
520
+
521
+ ## Pay your sellers
522
+
523
+ The fund ledger has two levels, and both are written by fulfilment and by the scheduled sweep rather than by a caller.
524
+
525
+ | Level | Model | Written when |
526
+ |---|---|---|
527
+ | **Transfer** | `Spree::SellerTransfer` | One order earned one seller something — credited as the goods went out |
528
+ | **Payout** | `Spree::SellerPayout` | A settlement was swept up and sent |
529
+
530
+ A seller's balance is derived from the two rather than stored, per currency, because nothing is ever converted: a seller trading in two currencies accrues two balances and is paid twice. Only settlements known to have completed count against the balance, so one whose outcome was never established still reads as owed — the honest answer while nobody knows, and it cannot be paid twice by mistake.
531
+
532
+ ### The sweep jobs
533
+
534
+ Two recurring jobs keep the ledger moving. A project created with
535
+ `create-spree-app` schedules both already:
536
+
537
+ ```yaml server/config/recurring.yml
538
+ production:
539
+ sweep_due_seller_payouts:
540
+ class: Spree::SellerPayouts::SweepDueJob
541
+ schedule: at 3am every day
542
+ execute_pending_seller_transfers:
543
+ class: Spree::SellerTransfers::ExecutePendingDueJob
544
+ schedule: every hour at minute 27
545
+ ```
546
+
547
+ > **WARNING:** On an existing project, or one running a scheduler of its own, confirm both are
548
+ > scheduled. A marketplace that never runs the sweep never pays a seller, and
549
+ > nothing reports the omission.
550
+
551
+ | Job | Run it | Does |
552
+ |---|---|---|
553
+ | `Spree::SellerPayouts::SweepDueJob` | daily | settles every seller who is due |
554
+ | `Spree::SellerTransfers::ExecutePendingDueJob` | hourly | retries earnings a provider refused — usually a passing thing |
555
+
556
+ ### Settling by hand
557
+
558
+ A seller on the `manual` interval is skipped by the sweep. That is what the interval means: the operator decides when. The same endpoint also pays any seller early.
559
+
560
+
561
+ ```typescript Admin SDK
562
+ // Where the seller stands, one row per currency
563
+ const { data: balances } = await adminClient.sellers.balances('sel_xxx')
564
+
565
+ // Settle them now, whatever their schedule says — 422 if there is nothing owed
566
+ await adminClient.sellers.settle('sel_xxx')
567
+
568
+ // The built-in provider: record that the bank transfer went out
569
+ await adminClient.sellerPayouts.complete('vpo_xxx', { reference: 'TRN-2026-0912-0042' })
570
+
571
+ // Read the ledger
572
+ const { data: transfers } = await adminClient.sellerTransfers.list({ seller_id_eq: 'sel_xxx' })
573
+ const { data: payouts } = await adminClient.sellerPayouts.list({ seller_id_eq: 'sel_xxx' })
574
+ ```
575
+
576
+ ```bash cURL
577
+ curl 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/balances' \
578
+ -H 'X-Spree-API-Key: sk_xxx'
579
+
580
+ curl -X POST 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/payouts' \
581
+ -H 'X-Spree-API-Key: sk_xxx'
582
+ ```
583
+
584
+
585
+ ### Payout providers
586
+
587
+ A provider is a stateless class registered in `Spree.payout_providers` and chosen per store with `preferred_payout_provider`.
588
+
589
+ | Provider | Moves money? | Completion |
590
+ |---|---|---|
591
+ | `Spree::PayoutProvider::System` (default) | No — keeps the books | The operator marks each payout paid, with the bank reference |
592
+ | Stripe Connect (`spree_stripe`) | Yes — transfers each earning to the seller's connected account as goods ship | A Stripe webhook, when the money lands |
593
+
594
+ Providers that need a seller to hold an account with them answer `requires_payout_account?` and implement `onboarding_url` — which is what the `payout_account` requirement from step 2 drives. Those links are short-lived and single-use, so the seller panel asks for a fresh one at the moment of clicking rather than minting one while drawing the checklist.
595
+
596
+ A connected provider confirms settlements through `POST /api/v3/webhooks/payouts/:payment_method_id` — separate from the payment webhook, because providers scope seller-account events to their own subscription and signing secret.
597
+
598
+ **Read more:** [Payouts](../core-concepts/sellers.md#payouts) for the ledger, [Stripe Connect for marketplaces](../../integrations/payments/stripe-connect.md) for the shipped provider, [Seller payouts](../providers/payouts.md) to write your own, and [Payouts settings](/user/settings/marketplace) for the operator's screen.
599
+
600
+ > **NOTE:** Refund clawbacks and netting across settlements, reconciliation, KYC operations and seller tax reporting (DAC7) are Spree Enterprise.
601
+
602
+ ## Stand up the seller panel
603
+
604
+ Sellers get their own application — never access to the marketplace's dashboard. It is a separate React SPA against a separate API, shipped as `@spree/seller-dashboard`.
605
+
606
+ It shares its foundations with the admin dashboard: components from `@spree/dashboard-ui`, framework pieces (registries, providers, hooks) from `@spree/dashboard-core`. The two panels therefore stay visually and behaviourally consistent, and anything you learn customizing one applies to the other.
607
+
608
+ ### Scaffold and run it
609
+
610
+ The panel ships as a shell you render from a thin host app that you own. Two ways to get one:
611
+
612
+
613
+ ```bash New project
614
+ npx create-spree-app my-marketplace
615
+ # Both panels are scaffolded automatically — no prompt to answer
616
+ ```
617
+
618
+ ```bash Existing project
619
+ npx spree add seller-dashboard
620
+ ```
621
+
622
+
623
+ A new project always gets both admin SPAs, so the starter's Docker image bakes both in; a store that never invites a seller simply leaves `/sellers` unvisited. Either way the host app lands at `apps/seller-dashboard/` with its own `package.json`, Vite config and a `src/plugins.ts` for your customizations. Then:
624
+
625
+ ```bash
626
+ cd apps/seller-dashboard && pnpm dev
627
+ ```
628
+
629
+ It serves on port **5174** (the admin dashboard is on 5173, so both run side by side).
630
+
631
+ `.env.local` holds one setting — the Rails server the dev proxy forwards `/api` to:
632
+
633
+ ```bash apps/seller-dashboard/.env.local
634
+ VITE_API_PROXY_TARGET=http://localhost:3000
635
+ ```
636
+
637
+ > **WARNING:** Do **not** set `VITE_SPREE_API_URL` in development. It switches the SDK to absolute cross-origin URLs and bypasses the proxy, which breaks the seller refresh-token cookie: the cookie rides under `SameSite=Lax`, and that only works while the panel is same-origin with the API.
638
+
639
+ ### What sellers get out of the box
640
+
641
+ | Area | Pages |
642
+ |---|---|
643
+ | Onboarding | The checklist, submissions, payout-account link |
644
+ | Products | List, create, edit, submit for review |
645
+ | Orders | List, detail, fulfillments, deliveries, labels, returns, exchanges, claims |
646
+ | Money | Earnings, payouts |
647
+ | Profile | Public profile, tax identifiers |
648
+ | Settings | Team, delivery methods, package types, stock locations, policies |
649
+ | Auth | Login, forgot/reset password, invitation acceptance |
650
+
651
+ Sellers cannot see other sellers, the marketplace's own catalog, or anything belonging to the store at large.
652
+
653
+ ### Deploying it
654
+
655
+ By default the seller panel, same as dashboard is build with Spree docker image and is server via `/sellers` path. You can also deploy it as a static bundle on a CDN or static host. The entire process is baked into the default `Dockerfile` which you get when you scaffold a new project with `create-spree-app`. The `spree build --production` command builds both panels and bakes them into the image, so you can run it as-is. This also automatically resolves CORS issues, since the panel is served from the same origin as the API.
656
+
657
+ ### Customizing it
658
+
659
+ `src/plugins.ts` in your host app is where customizations are registered — the same `defineDashboardPlugin` API the admin dashboard uses, because both panels sit on `@spree/dashboard-core`.
660
+
661
+ ```tsx apps/seller-dashboard/src/plugins.ts
662
+ import { defineDashboardPlugin } from '@spree/seller-dashboard'
663
+ import { PayoutsPage } from './pages/payouts'
664
+
665
+ defineDashboardPlugin({
666
+ nav: {
667
+ add: [{ key: 'payouts', label: 'Payouts', path: '/payouts', position: 300 }],
668
+ remove: ['team'],
669
+ },
670
+ slots: {
671
+ 'seller.team.after': [{ id: 'audit', component: TeamAuditCard }],
672
+ },
673
+ })
674
+ ```
675
+
676
+ Import everything from `@spree/seller-dashboard` — it re-exports both the framework and the design system, so you never have to work out which package an export lives in.
677
+
678
+ | What you want | How |
679
+ |---|---|
680
+ | Add or remove a nav entry | `nav.add` / `nav.remove` in `defineDashboardPlugin` |
681
+ | Inject a widget into a built-in page | A [slot](../dashboard/customization/slots.md) |
682
+ | Add a page | A [route](../dashboard/customization/routes.md) plus a nav entry |
683
+ | Add a table column | The [table registry](../dashboard/customization/tables.md) |
684
+ | Change wording | [Translations](../dashboard/customization/translations.md) merged with `i18n.addResourceBundle` |
685
+ | Install a third-party plugin | `npm add <plugin>` and restart — the Vite plugin picks it up from your dependencies |
686
+
687
+ The seller panel currently exposes just two slots, both on its team page: `seller.team.actions` and `seller.team.after`. (`seller.form_main` and `seller.form_sidebar` sound related but belong to the *admin* dashboard's seller detail page — they are for the operator's view of a seller, not the seller's own.) See the [slots catalog](../dashboard/slots-catalog.md) for their contexts. To put a widget anywhere else in the panel, add a route rather than looking for a slot that is not there yet.
688
+
689
+ > **NOTE:** **White-labelling is theming and copy, not a branding config object.** There is no logo or brand-name setting to fill in. You restyle by owning the Tailwind layer in your host app, and you re-word by overriding translation keys. Both are real customization paths — there is simply no shortcut that skips them.
690
+
691
+ > **WARNING:** **`@spree/seller-dashboard` ships English only.** The admin dashboard ships several locales; the seller panel ships `en.json` and nothing else. A marketplace serving sellers in another language supplies that locale itself, through `i18n.addResourceBundle` in the host app.
692
+
693
+ **Read more:** [Dashboard overview](../dashboard/overview.md) for the package split and both customization paths, [Dashboard customization](../dashboard/customization/quickstart.md) for the registries in detail, and [Plugins](../dashboard/plugins/overview.md) for shipping a redistributable one.
694
+
695
+ ## Build against the Seller API
696
+
697
+ If you are not using the shipped panel — a native seller app, an ERP integration, a bespoke portal — the Seller API is the surface, and `@spree/seller-sdk` is the client.
698
+
699
+ **Every request is scoped to the seller making it. No endpoint takes a seller ID.** That is the security property worth relying on: a seller cannot ask for another seller's data, because there is nowhere in the request to name one.
700
+
701
+ ```bash
702
+ npm install @spree/seller-sdk
703
+ ```
704
+
705
+ ```typescript
706
+ import { createSellerClient } from '@spree/seller-sdk'
707
+
708
+ const sellerClient = createSellerClient({ baseUrl: 'https://marketplace.example.com' })
709
+
710
+ const tokens = await sellerClient.auth.login({
711
+ email: 'owner@brightsparks.example',
712
+ password: '…',
713
+ })
714
+ sellerClient.setToken(tokens.access_token)
715
+
716
+ // A user who runs several sellers picks which one they are acting as
717
+ sellerClient.setSeller('sel_xxx')
718
+ ```
719
+
720
+ Authentication is JWT only. **There is deliberately no secret-key equivalent**: a key that acts as a seller without a seller signing in is exactly what the Seller API's design exists to prevent. Sign-in fails for a store staff member who runs no seller, even though staff share the same user class.
721
+
722
+ The chosen seller travels as `X-Spree-Seller-Id`. The store is derived from the seller server-side and never sent alongside, so no header a client can set widens what it reaches.
723
+
724
+ ### What the client covers
725
+
726
+ | Namespace | Notable methods |
727
+ |---|---|
728
+ | `auth` | `login`, `refresh`, `logout`, `providers`, `lookupInvitation`, `acceptInvitation`, `requestPasswordReset` |
729
+ | `onboarding` | `get`, `submitForReview`, `payoutAccount` |
730
+ | `requirementSubmissions` | `create` — create only; the seller submits again rather than editing |
731
+ | `profile`, `taxIdentifiers`, `policies` | The seller's own identity and legal documents |
732
+ | `team`, `invitations` | The seller hires and manages their own staff |
733
+ | `products` | Full CRUD plus `submit`, `draft`, `archive` and their bulk equivalents |
734
+ | `orders` | List and detail, plus `cancel`, `address`, nested `fulfillments`, `deliveries`, `labels`, `returns`, `exchanges`, `claims`, `notes` |
735
+ | `balances`, `transfers`, `payouts` | Read-only — written by fulfilment and the sweep, never by the seller |
736
+ | `deliveryMethods`, `packageTypes`, `stockLocations` | How and from where they ship |
737
+ | `imports`, `exports`, `directUploads` | Bulk listing by CSV, and CSV of what they sold |
738
+ | `productTypes`, `deliveryProfiles`, `deliveryZones`, `returnReasons`, `claimReasons`, `trackingCarriers`, `countries` | Read-only marketplace vocabulary a seller picks from |
739
+
740
+ Two asymmetries are worth noticing, because both are deliberate:
741
+
742
+ - **A seller cannot set a product's status.** They call `submit`, and the marketplace decides. Bulk moves exist only for the transitions a seller may make alone.
743
+ - **A seller cannot mark an order delivered.** That a parcel arrived is the buyer's word, not the sender's, so confirming receipt stays with the operator and the carrier feed.
744
+
745
+ What a seller's staff may do within all this is governed by [roles the seller owns](../core-concepts/staff-roles.md) — the same permission system as the back office, with a narrower set of keys, so a seller role can never reach store settings.
746
+
747
+ **Read more:** [Seller API introduction](../../api-reference/seller-api/introduction.md), [Seller API authentication](../../api-reference/seller-api/authentication.md), [Seller API errors](../../api-reference/seller-api/errors.md).
748
+
749
+ ## Related
750
+
751
+ - [Sellers](../core-concepts/sellers.md) — the full model: lifecycle, ownership, order splitting, ledger
752
+ - [Commissions](../core-concepts/commissions.md) — rates, rules, precedence, currency handling, commission tax
753
+ - [Products](../core-concepts/products.md#seller-submissions) — the listing review flow in detail
754
+ - [Orders](../core-concepts/orders.md) — orders and their statuses
755
+ - [Customers](../core-concepts/customers.md) — who buys, across every seller
756
+ - [Delivery setup](../core-concepts/delivery-setup.md#on-a-marketplace) — how a seller's goods ship and what they ship in
757
+ - [Staff & Roles](../core-concepts/staff-roles.md) — how seller teams are governed
758
+ - [Seller payouts](../providers/payouts.md) — writing your own payout provider
759
+ - [Stripe Connect](../../integrations/payments/stripe-connect.md) — the shipped payout provider
760
+ - [Seller API](../../api-reference/seller-api/introduction.md) — the full endpoint reference
761
+ - [Dashboard overview](../dashboard/overview.md) — customizing either panel
762
+ - [Sellers (operator guide)](/user/sellers/sellers) — running a marketplace from the dashboard
763
+ - [Seller requirements (operator guide)](/user/settings/seller-requirements) — building the checklist
764
+ - [Payouts settings (operator guide)](/user/settings/marketplace) — who pays sellers and how often
@@ -10,7 +10,7 @@ At minimum, set `SPREE_API_URL` and `SPREE_PUBLISHABLE_KEY` in your hosting plat
10
10
  ## Production Build
11
11
 
12
12
  ```bash
13
- npm run build
13
+ pnpm build
14
14
  npm start
15
15
  ```
16
16
 
@@ -102,8 +102,8 @@ For any platform that supports Node.js (Render, Railway, Fly.io, AWS, etc.):
102
102
  1. Install dependencies and build:
103
103
 
104
104
  ```bash
105
- npm ci
106
- npm run build
105
+ pnpm install --frozen-lockfile
106
+ pnpm build
107
107
  ```
108
108
 
109
109
  2. Start the production server:
@@ -130,13 +130,15 @@ jobs:
130
130
  steps:
131
131
  - uses: actions/checkout@v4
132
132
 
133
+ - uses: pnpm/action-setup@v4
134
+
133
135
  - uses: actions/setup-node@v4
134
136
  with:
135
137
  node-version: 20
136
- cache: npm
138
+ cache: pnpm
137
139
 
138
- - run: npm ci
139
- - run: npm run build
140
+ - run: pnpm install --frozen-lockfile
141
+ - run: pnpm build
140
142
 
141
143
  # Add your deployment step here
142
144
  # Example for Vercel:
@@ -28,7 +28,7 @@ Customize a template by editing its file directly — they use `@react-email/com
28
28
  Run the storefront in development and open [http://localhost:3001/dev/emails](http://localhost:3001/dev/emails):
29
29
 
30
30
  ```bash
31
- npm run dev
31
+ pnpm dev
32
32
  ```
33
33
 
34
34
  Each template renders with sample data via `@react-email/render`. The route is gated to non-production environments.
@@ -51,7 +51,7 @@ To run the storefront on its own — against an existing Spree backend, or to wo
51
51
  ```bash
52
52
  git clone https://github.com/spree/storefront.git
53
53
  cd storefront
54
- npm install
54
+ pnpm install
55
55
  ```
56
56
 
57
57
  Then follow [Configuration](#configuration) to point it at your Spree API. If you plan to customize and track upstream changes, [fork first](customization.md#tracking-upstream-updates).
@@ -78,7 +78,7 @@ See [Deployment](deployment.md) for all optional environment variables (Sentry,
78
78
  ## Development
79
79
 
80
80
  ```bash
81
- npm run dev
81
+ pnpm dev
82
82
  ```
83
83
 
84
84
  Open [http://localhost:3001](http://localhost:3001) in your browser.
@@ -88,7 +88,7 @@ Open [http://localhost:3001](http://localhost:3001) in your browser.
88
88
  ## Production Build
89
89
 
90
90
  ```bash
91
- npm run build
91
+ pnpm build
92
92
  npm start
93
93
  ```
94
94
 
@@ -9,12 +9,12 @@ The storefront ships two test layers: fast unit/integration tests with Vitest, a
9
9
 
10
10
  ```bash
11
11
  npm test # one-shot
12
- npm run test:watch # watch mode
12
+ pnpm test:watch # watch mode
13
13
  ```
14
14
 
15
15
  ## End-to-end (Playwright)
16
16
 
17
- The E2E suite drives a browser against a real Spree backend. `npm run e2e:up` boots the backend — Postgres, Redis, and the official `ghcr.io/spree/spree:latest` image from `e2e-backend/docker-compose.yml` — then seeds it and mints an API key through the official [`@spree/cli`](../../cli/quickstart.md) (`spree seed`, `spree sample-data`, `spree api-key create`), installed as a dev dependency. No `create-spree-app` setup is required.
17
+ The E2E suite drives a browser against a real Spree backend. `pnpm e2e:up` boots the backend — Postgres, Redis, and the official `ghcr.io/spree/spree:latest` image from `e2e-backend/docker-compose.yml` — then seeds it and mints an API key through the official [`@spree/cli`](../../cli/quickstart.md) (`spree seed`, `spree sample-data`, `spree api-key create`), installed as a dev dependency. No `create-spree-app` setup is required.
18
18
 
19
19
  ```bash
20
20
  # 1. Export a Stripe test-mode key pair from your own Stripe sandbox.
@@ -26,16 +26,16 @@ export STRIPE_SECRET_KEY=sk_test_…
26
26
 
27
27
  # 2. Boot Spree + Postgres + Redis, seed sample data, register a Stripe
28
28
  # payment gateway, mint a publishable key, and write .env.e2e.
29
- npm run e2e:up
29
+ pnpm e2e:up
30
30
 
31
31
  # 3. Run the suite. Playwright boots `next dev` against .env.e2e.
32
- npm run test:e2e
32
+ pnpm test:e2e
33
33
 
34
34
  # Optional: interactive UI mode.
35
- npm run test:e2e:ui
35
+ pnpm test:e2e:ui
36
36
 
37
37
  # Tear everything down.
38
- npm run e2e:down
38
+ pnpm e2e:down
39
39
  ```
40
40
 
41
41
  The checkout test pays with card `4242 4242 4242 4242` through Stripe's [test mode](https://docs.stripe.com/keys). PaymentIntents land in whichever Stripe test account owns the keys you exported.
@@ -55,8 +55,8 @@ The E2E stack reads a `SPREE_IMAGE` env var and falls back to the stock image wh
55
55
  If you already publish a customized Spree image:
56
56
 
57
57
  ```bash
58
- SPREE_IMAGE=ghcr.io/your-org/your-spree:latest npm run e2e:up
59
- npm run test:e2e
58
+ SPREE_IMAGE=ghcr.io/your-org/your-spree:latest pnpm e2e:up
59
+ pnpm test:e2e
60
60
  ```
61
61
 
62
62
  ### A `create-spree-app` project's backend
@@ -69,13 +69,13 @@ docker build -t project-spree:e2e ./backend
69
69
 
70
70
  # From apps/storefront — run E2E against it
71
71
  cd apps/storefront
72
- SPREE_IMAGE=project-spree:e2e npm run e2e:up
73
- npm run test:e2e
74
- npm run e2e:down
72
+ SPREE_IMAGE=project-spree:e2e pnpm e2e:up
73
+ pnpm test:e2e
74
+ pnpm e2e:down
75
75
  ```
76
76
 
77
77
  > **NOTE:** New `create-spree-app` projects wire this into the generated storefront CI workflow automatically — the workflow builds `server/Dockerfile` and runs the storefront E2E against that image rather than stock Spree.
78
78
 
79
79
  ### Interactive / manual
80
80
 
81
- To click through the storefront against a running backend (no E2E harness), point it there directly: set `SPREE_API_URL` and `SPREE_PUBLISHABLE_KEY` in `.env.local` (see [Environment Variables](environment-variables.md)) and run `npm run dev`.
81
+ To click through the storefront against a running backend (no E2E harness), point it there directly: set `SPREE_API_URL` and `SPREE_PUBLISHABLE_KEY` in `.env.local` (see [Environment Variables](environment-variables.md)) and run `pnpm dev`.
@@ -16,7 +16,7 @@ Apple Pay and Google Pay require HTTPS **and a publicly-reachable URL** — Stri
16
16
  2. Start the dev server normally (HTTP on port 3001):
17
17
 
18
18
  ```bash
19
- npm run dev
19
+ pnpm dev
20
20
  ```
21
21
 
22
22
  3. In a second terminal, expose it through a quick tunnel:
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  title: Upgrading to Spree 6.0
3
3
  description: Guide to upgrading a Spree 5.6 application to Spree 6.0
4
- hidden: true
5
4
  ---
6
5
 
7
6
  > **INFO:** Before proceeding to upgrade, please ensure you're at [Spree 5.6](/v5/developer/upgrades/5.5-to-5.6). Spree 6.0 requires **Rails 8.1** and is the designated breaking-change window for the platform — read the behavioral changes section even if your upgrade runs clean.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spree/docs",
3
- "version": "0.1.246",
3
+ "version": "0.1.248",
4
4
  "description": "Spree Commerce developer documentation for AI agents and local reference",
5
5
  "type": "module",
6
6
  "license": "CC-BY-4.0",