@spree/docs 0.1.239 → 0.1.241

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.
Files changed (36) hide show
  1. package/dist/api-reference/store-api/metadata.md +6 -6
  2. package/dist/developer/core-concepts/architecture.md +15 -19
  3. package/dist/developer/core-concepts/carts.md +71 -1
  4. package/dist/developer/core-concepts/metafields.md +0 -2
  5. package/dist/developer/create-spree-app/quickstart.md +22 -19
  6. package/dist/developer/customization/api.md +2 -2
  7. package/dist/developer/customization/checkout.md +117 -0
  8. package/dist/developer/customization/configuration.md +76 -175
  9. package/dist/developer/customization/decorators.md +120 -130
  10. package/dist/developer/customization/metadata.md +9 -9
  11. package/dist/developer/customization/model-preferences.md +34 -68
  12. package/dist/developer/customization/permissions.md +13 -28
  13. package/dist/developer/customization/validations.md +16 -43
  14. package/dist/developer/dashboard/concepts.md +4 -2
  15. package/dist/developer/dashboard/customization/backend.md +5 -2
  16. package/dist/developer/dashboard/customization/navigation.md +1 -1
  17. package/dist/developer/dashboard/customization/quickstart.md +1 -1
  18. package/dist/developer/dashboard/overview.md +2 -2
  19. package/dist/developer/dashboard/plugins/distributing.md +1 -1
  20. package/dist/developer/dashboard/public-api.md +3 -2
  21. package/dist/developer/dashboard/recipes/attribute-end-to-end.md +3 -3
  22. package/dist/developer/dashboard/recipes/custom-form-field.md +1 -1
  23. package/dist/developer/dashboard/recipes/page-action-button.md +2 -2
  24. package/dist/developer/dashboard/recipes/sidebar-widget.md +2 -2
  25. package/dist/developer/dashboard/slots-catalog.md +60 -0
  26. package/dist/developer/getting-started/quickstart.md +25 -24
  27. package/dist/developer/how-to/custom-promotion.md +1 -1
  28. package/dist/developer/how-to/custom-search-provider.md +2 -2
  29. package/dist/developer/how-to/custom-stock-splitter.md +2 -2
  30. package/dist/developer/sdk/admin/extending.md +1 -1
  31. package/dist/developer/sdk/admin/quickstart.md +1 -1
  32. package/dist/developer/sdk/admin/resources.md +1 -1
  33. package/dist/developer/sdk/configuration.md +1 -1
  34. package/dist/developer/sdk/extending.md +1 -1
  35. package/dist/developer/tutorial/testing.md +45 -91
  36. package/package.json +1 -1
@@ -11,15 +11,17 @@ Every grantable capability is a flat key of the form `read_<resource>` or `write
11
11
 
12
12
  Keys are per resource, not per action. The money-adjacent areas (`payments`, `refunds`, `gift_cards`, `store_credits`) are separate resources from `orders`, so "view orders but don't refund" needs no special casing.
13
13
 
14
- Roles as code is plain ActiveRecord:
15
-
16
- ```ruby
17
- # db/seeds.rb
18
- Spree::Role.find_or_create_by!(name: 'support')
19
- .update!(permissions: %w[read_orders read_customers], description: 'Read-only support')
14
+ Roles are data, so create them where you manage the rest of your store's data: **Settings → Staff → Roles** in the dashboard, or the Admin API when you want it scripted.
15
+
16
+ ```bash
17
+ spree api post roles --data '{
18
+ "name": "Support",
19
+ "description": "Read-only support",
20
+ "permissions": ["read_orders", "read_customers"]
21
+ }'
20
22
  ```
21
23
 
22
- A role with `mutable: false` (set from seeds or the console) renders read-only in the dashboard — for roles the application does not want store staff to edit. The `admin` role is always protected.
24
+ A role marked immutable renders read-only in the dashboard, for roles the application does not want store staff editing. The `admin` role is always protected.
23
25
 
24
26
  ## Registering extension permissions
25
27
 
@@ -56,30 +58,13 @@ Naming another audience exposes the scope to a principal outside the store's own
56
58
 
57
59
  A key never decides *which rows* its holder reaches. That is the controller's job, through scope fetching — a seller holding `write_products` manages their own products because the seller API reads through `current_seller`, not because the key says so.
58
60
 
59
- ## Beyond the catalog
61
+ ## What the catalog does not cover
60
62
 
61
- The catalog is deliberately flat: it answers "may this role touch this kind of record", and that is the whole grant vocabulary. Two rules that do *not* belong in it:
63
+ The catalog is deliberately flat: it answers "may this role touch this kind of record", and that is the whole grant vocabulary.
62
64
 
63
- - **Record-state rules** ("a completed order cannot be deleted") live on models and workflows, so they bind every caller — including secret API keys, which never consult CanCanCan.
64
- - **Row-level restrictions** (a support agent limited to one market) are not expressible as keys. Replace the ability class outright:
65
+ **Record-state rules** — "a completed order cannot be deleted" — belong on models and workflows, not on roles. Put them there and they bind every caller, including secret API keys, which never consult the role system at all. A rule enforced only in the permission layer is a rule the API can walk straight past.
65
66
 
66
- ```ruby
67
- # config/initializers/spree.rb
68
- Spree::Dependencies.ability_class = 'MyApp::Ability'
69
- ```
70
-
71
- ```ruby
72
- # app/models/my_app/ability.rb
73
- class MyApp::Ability < Spree::Ability
74
- def initialize(user, options = {})
75
- super
76
- return unless user.respond_to?(:market_id) && user.market_id
77
-
78
- cannot :manage, Spree::Order
79
- can [:read, :admin], Spree::Order, market_id: user.market_id
80
- end
81
- end
82
- ```
67
+ **Row-level restrictions**, such as a support agent limited to one market, are not expressible as permission keys. Scope them where the records are read: a controller reaching through `current_store.orders` rather than `Spree::Order` already restricts by tenant, and the same technique narrows by market or seller. See [extending the API](api.md).
83
68
 
84
69
  ## How enforcement works
85
70
 
@@ -52,27 +52,6 @@ Both cart keys appear above because adding to a cart and setting a quantity are
52
52
  different flows. See [Services & Workflows](workflows.md)
53
53
  for the full hook list.
54
54
 
55
- ## Adding a rule to a model
56
-
57
- For rules about the shape of a record, a decorator is the supported route:
58
-
59
- ```ruby
60
- # app/models/spree/product_decorator.rb
61
- module MyStore
62
- module ProductDecorator
63
- def self.prepended(base)
64
- base.validates :external_id, presence: true, uniqueness: true
65
- end
66
- end
67
- end
68
-
69
- Spree::Product.prepend(MyStore::ProductDecorator)
70
- ```
71
-
72
- Decorators couple you to Spree internals, so prefer a hook when the rule is
73
- really about an operation. See
74
- [Decorators](decorators.md).
75
-
76
55
  ## Relaxing a rule Spree ships
77
56
 
78
57
  Never call `clear_validators!` — it wipes every validation on the model,
@@ -81,7 +60,8 @@ cause. Use one of these instead.
81
60
 
82
61
  ### Store preferences
83
62
 
84
- Some rules are already switchable, per store, from the dashboard or a console:
63
+ Some rules are switchable per store, with no code at all. Change them in the
64
+ dashboard under **Settings → Store**, or through the Admin API:
85
65
 
86
66
  | Preference | Effect |
87
67
  |---|---|
@@ -90,30 +70,23 @@ Some rules are already switchable, per store, from the dashboard or a console:
90
70
  | `address_requires_company` | Requires it — only meaningful with the field shown |
91
71
  | `disable_sku_validation` | Turns off SKU uniqueness |
92
72
 
93
- ```ruby
94
- store.update!(preferred_address_requires_company: true)
95
- ```
96
-
97
- ### Overriding the gating predicate
98
73
 
99
- Most conditional validations in `Spree::Address` are gated on a predicate you
100
- can override, which is far safer than removing the validation:
101
-
102
- ```ruby
103
- module MyStore
104
- module AddressDecorator
105
- # Business addresses only in this store.
106
- def require_company?
107
- !quick_checkout
108
- end
109
- end
110
- end
74
+ ```typescript Admin SDK
75
+ await adminClient.store.update({
76
+ preferred_address_requires_phone: false,
77
+ })
78
+ ```
111
79
 
112
- Spree::Address.prepend(MyStore::AddressDecorator)
80
+ ```bash cURL
81
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/store' \
82
+ -H 'X-Spree-API-Key: sk_xxx' \
83
+ -H 'Content-Type: application/json' \
84
+ -d '{ "preferred_address_requires_phone": false }'
113
85
  ```
114
86
 
115
- `require_name?`, `require_street?`, `require_postal_code?`, `require_phone?` and
116
- `require_company?` all work this way.
87
+
88
+ > **NOTE:** All four are editable in the dashboard except `address_requires_company`, which
89
+ > is API-only for now.
117
90
 
118
91
  ### The address validator registry
119
92
 
@@ -121,7 +94,7 @@ Address rules are regional and business-specific, so addresses carry a registry
121
94
  of extra validator classes you can add to and remove from:
122
95
 
123
96
  ```ruby
124
- # config/initializers/spree.rb — or config.to_prepare for your own classes
97
+ # server/config/initializers/spree.rb — or config.to_prepare for your own classes
125
98
  Spree.validators.addresses.register(MyStore::PostBoxValidator)
126
99
 
127
100
  # Drop a rule Spree ships
@@ -30,7 +30,9 @@ The dashboard is a **Single Page Application**: one HTML page, all routing clien
30
30
  @spree/dashboard ─ app shell: routes, resource hooks, locales, vite config
31
31
  ```
32
32
 
33
- [Overview](overview.md) covers the split in more detail. The short version: **`-ui`** has no providers or hooks (data comes via props); **`-core`** is the extension API and runtime services; **`-dashboard`** is the deployable app shell. Plugins and customizations consume `-core` and `-ui`, never the app shell.
33
+ [Overview](overview.md) covers the split in more detail. The short version: **`-ui`** has no providers or hooks (data comes via props); **`-core`** is the extension API and runtime services; **`-dashboard`** is the deployable app shell.
34
+
35
+ The split matters when you publish a plugin: it consumes `-core` and `-ui` and never the app shell, which it plugs into rather than ships. In your own dashboard app the distinction is noise, so `@spree/dashboard` re-exports both — one import, and you never have to work out which package a given export lives in.
34
36
 
35
37
  ## How customization plugs in
36
38
 
@@ -53,7 +55,7 @@ The facade `defineDashboardPlugin({ nav, routes, slots, … })` groups all five
53
55
  The signed-in admin's identity and abilities are exposed via three providers wrapping the app:
54
56
 
55
57
  - `<AuthProvider>` — the current admin user; `useAuth()` returns `{ user, signIn, signOut, … }`
56
- - `<PermissionProvider>` — CanCanCan abilities; `usePermissions()` returns `{ can, cannot }`
58
+ - `<PermissionProvider>` — CanCanCan abilities; `usePermissions()` returns `{ permissions, rules, isLoading }`, and the checks live on `permissions.can(...)`
57
59
  - `<StoreProvider>` — current store + timezone + currency; `useStore()` returns `{ storeId, store, … }`
58
60
 
59
61
  When you write a custom page or hook, pull from these. **Never reach into local state for identity** — it changes on store switch or session refresh, and the providers wire all that for you.
@@ -52,13 +52,16 @@ Resource methods are just `request<T>()` calls with the right path + types pre-b
52
52
  The dashboard's data layer is React-Query — every `useFoo` hook is `useQuery({ queryKey, queryFn })` over an SDK call. Follow the same pattern for your own data:
53
53
 
54
54
  ```ts
55
- import { adminClient } from '@spree/dashboard'
55
+ import { adminClient, useResourceKey } from '@spree/dashboard'
56
56
  import { useQuery } from '@tanstack/react-query'
57
57
  import type { AdminBrand } from './types'
58
58
 
59
59
  export function useBrands(params?: { search?: string }) {
60
60
  return useQuery({
61
- queryKey: ['brands', params],
61
+ // `useResourceKey` scopes the key to the current store. A bare
62
+ // `['brands', params]` key would serve one store's rows to another
63
+ // after a store switch.
64
+ queryKey: useResourceKey('brands', params),
62
65
  queryFn: () =>
63
66
  adminClient.request<{ data: AdminBrand[] }>('GET', '/brands', { params }),
64
67
  staleTime: 30_000,
@@ -26,7 +26,7 @@ nav.add({
26
26
  })
27
27
  ```
28
28
 
29
- `position` controls ordering — built-ins use 100–600 (Home 100, Orders 200, Products 300, Customers 400, Promotions 500, Reports 600; a conditional Getting Started entry sits at 50), leaving gaps to slot into. The default is 100. Keys must be unique, including against the built-in keys (`getting-started`, `home`, `orders`, `products`, `customers`, `promotions`, `reports`, `settings`) — duplicates throw at boot.
29
+ `position` controls ordering — built-ins run 50–600 (Getting Started 50 when shown, Home 100, Orders 200, Products 300, Inventory 350, Customers 400, Sellers 450, Loyalty 475, Promotions 500, Reports 600), leaving gaps to slot into. The default is 100. Keys must be unique, including against the built-in keys (`getting-started`, `home`, `orders`, `products`, `inventory`, `customers`, `sellers`, `loyalty`, `promotions`, `reports`, `settings`) — duplicates throw at boot.
30
30
 
31
31
  ### Add via `defineDashboardPlugin`
32
32
 
@@ -80,7 +80,7 @@ defineDashboardPlugin({
80
80
 
81
81
  Save. Click "Analytics" — your page renders inside the dashboard chrome, at `/<store id>/analytics`.
82
82
 
83
- > **WARNING:** Every `key` must be unique, including against the built-ins (`getting-started`, `home`, `orders`, `products`, `customers`, `promotions`, `reports`, `settings`). Registering a duplicate key throws at boot with a message naming the conflict.
83
+ > **WARNING:** Every `key` must be unique, including against the built-ins (`getting-started`, `home`, `orders`, `products`, `inventory`, `customers`, `sellers`, `loyalty`, `promotions`, `reports`, `settings`). Registering a duplicate key throws at boot with a message naming the conflict.
84
84
 
85
85
  ## What just happened
86
86
 
@@ -6,7 +6,7 @@ description: Learn about the Spree React Dashboard architecture and how to custo
6
6
 
7
7
  > **INFO:** The React Dashboard is currently a **Developer Preview** — APIs may still change between releases. It is becoming the default Admin interface.
8
8
 
9
- The React Dashboard is a React single-page application (SPA) that talks to the [Spree Admin API](../../api-reference/admin-api.md). It can be distributed with the Spree API or as a standalone npm package, and it can be extended with plugins. The dashboard is built with modern React libraries and patterns, including:
9
+ The React Dashboard is a React single-page application (SPA) that talks to the [Spree Admin API](../../api-reference/admin-api/introduction.md). It can be distributed with the Spree API or as a standalone npm package, and it can be extended with plugins. The dashboard is built with modern React libraries and patterns, including:
10
10
 
11
11
  | Layer | Choice |
12
12
  |---|---|
@@ -35,7 +35,7 @@ Most customization uses the registries in `@spree/dashboard-core` to add nav ent
35
35
 
36
36
  **In your own dashboard app, import from `@spree/dashboard`.** It re-exports both packages, so you never have to work out which one an export lives in. A distributed plugin is the exception: it imports `@spree/dashboard-core` and `@spree/dashboard-ui` directly, because it plugs into the shell rather than shipping it.
37
37
 
38
- You can also built your own dashboard using `@spree/dashboard-ui` primitives and `@spree/dashboard-core` registries, if you want to replace the default dashboard entirely. This is rare — most teams just add a few pages or cards to the existing dashboard.
38
+ You can also build your own dashboard using `@spree/dashboard-ui` primitives and `@spree/dashboard-core` registries, if you want to replace the default dashboard entirely. This is rare — most teams just add a few pages or cards to the existing dashboard.
39
39
 
40
40
  ## Get the dashboard
41
41
 
@@ -120,7 +120,7 @@ The registries are global singletons, so a key collision is the consumer's probl
120
120
  - **Namespace locale keys** — `admin.acme_brands.title` not `admin.brands.title`.
121
121
  - **Namespace API paths** — your Rails engine should mount under `/api/v3/admin/acme/...`, not `/api/v3/admin/brands` (which collides with core Brands if it ever ships).
122
122
 
123
- The dashboard logs a warning in dev mode when two registry entries share a key. That's the consumer's signal that two plugins are stepping on each other — and it falls back to "last-registered wins", which is rarely what either plugin wanted.
123
+ Registering a key that already exists throws at boot, with a message naming the conflict. That is deliberate: a silently shadowed entry would leave the consumer with a plugin that half works, where a failure at startup points straight at the two plugins stepping on each other.
124
124
 
125
125
  ## Distribution channels
126
126
 
@@ -4,7 +4,7 @@ sidebarTitle: Public API
4
4
  description: Every importable symbol the dashboard exposes — components, hooks, registries, providers, and the SDK client. Treat this as the surface you can safely depend on.
5
5
  ---
6
6
 
7
- The dashboard is structured as three packages, each with a documented public surface. **Anything not listed here is internal** — don't import it via `@spree/dashboard-core/src/internal/...`, because we'll move or rename it without a deprecation cycle.
7
+ The dashboard is structured as three packages. This page lists the symbols we commit to keeping — **treat anything not listed as internal**, liable to move or be renamed without a deprecation cycle, even where the barrel happens to export it today. Never reach past a package entry point into `@spree/dashboard-core/src/...`.
8
8
 
9
9
  ## `@spree/dashboard-core`
10
10
 
@@ -51,7 +51,7 @@ import {
51
51
  // Page chrome
52
52
  PageHeader,
53
53
  PageTabs,
54
- TopBar,
54
+ MobileTopBar,
55
55
  AppSidebar,
56
56
  SettingsSidebar,
57
57
  Slot,
@@ -92,6 +92,7 @@ import {
92
92
  useCustomFields,
93
93
  useDirectUpload, // ActiveStorage direct uploads
94
94
  useExport, // CSV export job lifecycle
95
+ useResourceKey, // store-scoped query key — use it for every custom query
95
96
  useResourceMutation, // useMutation wrapper with 422 handling
96
97
  useHostForm, // the built-in form a slot widget renders inside (throws when absent)
97
98
  useOptionalHostForm, // same, but null when the page has no host form
@@ -14,7 +14,7 @@ It's the full version of the [custom form field recipe](custom-form-field.md)
14
14
 
15
15
  ## First: should this be a custom field instead?
16
16
 
17
- **Default to a [custom field definition](custom-form-field.md#path-a-custom-field-definition--no-code).** It needs zero code — declare the field, and the product form renders and saves it. That covers most plugin data and everything merchants define themselves.
17
+ **Default to a [custom field definition](custom-form-field.md#path-a-custom-field-definition-no-code).** It needs zero code — declare the field, and the product form renders and saves it. That covers most plugin data and everything merchants define themselves.
18
18
 
19
19
  Graduate to a real database column only when the attribute needs **list operations or database guarantees**:
20
20
 
@@ -90,7 +90,7 @@ Add a Rails validation while you're here (`validates :lead_time_days, numericali
90
90
  ```tsx
91
91
  // src/plugins.ts (or a plugin's entry module)
92
92
  import { defineDashboardPlugin, i18n } from '@spree/dashboard'
93
- import { LeadTimeCard } from './fields/lead-time-card'
93
+ import { LeadTimeCard } from './slots/lead-time-card'
94
94
 
95
95
  i18n.addResourceBundle('en', 'translation', en, true, true)
96
96
 
@@ -123,7 +123,7 @@ defineDashboardPlugin({
123
123
  The form widget binds to the host form — no `<form>` of its own, no save button:
124
124
 
125
125
  ```tsx
126
- // src/fields/lead-time-card.tsx
126
+ // src/slots/lead-time-card.tsx
127
127
  import { Card, CardContent, CardHeader, CardTitle, Field, FieldError, FieldLabel, Input, useHostForm } from '@spree/dashboard'
128
128
  import { useTranslation } from 'react-i18next'
129
129
 
@@ -93,7 +93,7 @@ defineDashboardPlugin({
93
93
  The `product.form_sidebar` slot renders inside the product form. Bind your input to the host form with `useHostForm()` — no `<form>` of your own, no save button:
94
94
 
95
95
  ```tsx
96
- // src/fields/tech-specs-card.tsx
96
+ // src/slots/tech-specs-card.tsx
97
97
  import {
98
98
  Card,
99
99
  CardContent,
@@ -21,7 +21,7 @@ import { Button, adminClient, usePermissions, useResourceMutation } from '@spree
21
21
  import { Send } from 'lucide-react'
22
22
 
23
23
  interface Props {
24
- resource: { id: string; state: string } // Order
24
+ resource: { id: string; status: string } // Order
25
25
  }
26
26
 
27
27
  export function SendInvoiceButton({ resource }: Props) {
@@ -34,7 +34,7 @@ export function SendInvoiceButton({ resource }: Props) {
34
34
 
35
35
  // Only show to users who can act, and only on completed orders.
36
36
  if (!permissions.can('update', 'Spree::Order')) return null
37
- if (resource.state !== 'complete') return null
37
+ if (resource.status !== 'placed') return null
38
38
 
39
39
  return (
40
40
  <Button
@@ -11,7 +11,7 @@ This recipe builds a "Loyalty status" card for the customer detail page: read-on
11
11
  ## The component
12
12
 
13
13
  ```tsx
14
- // src/widgets/loyalty-status-card.tsx
14
+ // src/slots/loyalty-status-card.tsx
15
15
  import { useQuery } from '@tanstack/react-query'
16
16
  import {
17
17
  adminClient,
@@ -101,7 +101,7 @@ Worth noting:
101
101
  ```ts
102
102
  // src/plugins.ts (in your dashboard app)
103
103
  import { defineDashboardPlugin } from '@spree/dashboard'
104
- import { LoyaltyStatusCard } from './widgets/loyalty-status-card'
104
+ import { LoyaltyStatusCard } from './slots/loyalty-status-card'
105
105
 
106
106
  defineDashboardPlugin({
107
107
  slots: {
@@ -89,6 +89,66 @@ The resource detail pages each render a slot below their built-in cards. The con
89
89
  | Context | `{ customer }` — the full `Customer` record |
90
90
  | Host form | No — the page edits through sheets; widgets save via their own API calls |
91
91
 
92
+ ### `collection.form_sidebar`
93
+
94
+ | Where | Collection detail page (`products/collections/$collectionId`), end of the sidebar column |
95
+ |---|---|
96
+ | Use | Collection-scoped cards — merchandising rules, feed settings, external sync state |
97
+ | Context | `{ collection }` — the `Collection` record |
98
+ | Host form | **Yes** — form key `collection` |
99
+
100
+ ### `company.form_main`
101
+
102
+ | Where | Company detail page, end of the main column |
103
+ |---|---|
104
+ | Use | B2B account cards — credit terms, approval settings, external account references |
105
+ | Context | `{ company, kind, canEdit }` — `kind` is the company's own `kind`, `canEdit` whether the viewer may write |
106
+ | Host form | No — the widget owns its persistence |
107
+
108
+ ### `company.form_sidebar`
109
+
110
+ | Where | Company detail page, end of the sidebar column |
111
+ |---|---|
112
+ | Use | Narrow company-scoped cards alongside the main form |
113
+ | Context | `{ company, kind, canEdit }` |
114
+ | Host form | No |
115
+
116
+ ## Seller panel slots
117
+
118
+ Rendered by the marketplace seller panel (`@spree/seller-dashboard`) rather than the admin dashboard, and registered the same way.
119
+
120
+ ### `seller.form_main`
121
+
122
+ | Where | Seller detail page, end of the main column |
123
+ |---|---|
124
+ | Use | Seller-scoped cards — payout configuration, onboarding state, compliance records |
125
+ | Context | `{ seller, canEdit }` |
126
+ | Host form | No |
127
+
128
+ ### `seller.form_sidebar`
129
+
130
+ | Where | Seller detail page, end of the sidebar column |
131
+ |---|---|
132
+ | Use | Narrow seller-scoped cards |
133
+ | Context | `{ seller, canEdit }` |
134
+ | Host form | No |
135
+
136
+ ### `seller.team.actions`
137
+
138
+ | Where | Seller team screen, in the header's action area |
139
+ |---|---|
140
+ | Use | Team-level actions — bulk invite, export, an external directory sync |
141
+ | Context | `{ sellerId }` |
142
+ | Host form | No |
143
+
144
+ ### `seller.team.after`
145
+
146
+ | Where | Seller team screen, below the member list |
147
+ |---|---|
148
+ | Use | Anything that belongs under the team — audit log, role explainer, seat usage |
149
+ | Context | `{ sellerId }` |
150
+ | Host form | No |
151
+
92
152
  ## Page tabs slot
93
153
 
94
154
  ### `page.tabs` (default)
@@ -6,35 +6,39 @@ description: "Install Spree Commerce locally, run your first store, and deploy i
6
6
 
7
7
  ## Using `create-spree-app`
8
8
 
9
- The quickest way to get started. Scaffolds a full Spree project with Next.js Storefront (optional).
9
+ The quickest way to get started. Scaffolds a full Spree project with React admin dashboard and Next.js Storefront (optional).
10
10
 
11
- **Requirements:** [Node.js](https://nodejs.org/) 20+ and [Docker](https://docs.docker.com/get-docker/) running.
11
+ **Requirements:** [Node.js](https://nodejs.org/) 24+ and [Docker](https://docs.docker.com/get-docker/) running.
12
12
 
13
13
  ```bash
14
14
  npx create-spree-app@latest my-store
15
15
  ```
16
16
 
17
- The CLI walks you through an interactive setup:
17
+ The CLI walks you through an interactive setup where you choose the project name and whether to include the Next.js Storefront. It then builds a Docker stack with Postgres, and Spree API.
18
18
 
19
- 1. Choose **Full-stack** (Backend + Next.js Storefront) or **Backend only**
20
- 2. Optionally load **sample data** (products, categories, images)
19
+ Once complete it will open a browser window so you can finalize the store setup (pick name, currency, your email/password).
21
20
 
22
- Once complete, your store should be running at [http://localhost:3000/admin](http://localhost:3000/admin). The port can be different if `3000` is already used on your system.
21
+ <video autoPlay muted loop playsInline controls className="block w-auto max-w-full h-auto rounded-xl" src="/videos/store-setup.mp4"></video>
23
22
 
24
- See the full [create-spree-app documentation](../create-spree-app/quickstart.md) for all CLI flags and options.
23
+ ## Project structure and overview
25
24
 
26
- ## Accessing Admin Panel
25
+ `create-spree-app` creates a project folder like this:
27
26
 
28
- Navigate to [http://localhost:3000/admin](http://localhost:3000/admin) and log in with the default credentials:
29
-
30
- | | |
31
- |---|---|
32
- | **Email** | `spree@example.com` |
33
- | **Password** | `spree123` |
34
-
35
- Upon successful authentication, you should see the admin screen:
36
-
37
- <img src="/images/spree_admin_dashboard.png" />
27
+ ```
28
+ my-store/
29
+ ├── server/ # The Spree API
30
+ ├── apps/
31
+ │ ├── dashboard/ # Admin dashboard (Vite/React)
32
+ │ ├── seller-dashboard/ # Seller panel, for marketplaces (Vite/React)
33
+ │ └── storefront/ # Next.js storefront (if selected)
34
+ ├── .env # Ports, secrets and service URLs
35
+ ├── .spree/ # CLI credentials — gitignored, created on first run
36
+ ├── docker-compose.yml # The local stack
37
+ ├── package.json # Scripts and the pinned @spree/cli
38
+ ├── AGENTS.md # Instructions for coding agents
39
+ ├── CLAUDE.md # Project layout and conventions
40
+ └── README.md # How to run everything
41
+ ```
38
42
 
39
43
  ## Using the Spree CLI
40
44
 
@@ -58,18 +62,15 @@ See the [Spree CLI guide](../cli/quickstart.md) for all commands and the [Admin
58
62
 
59
63
  ## Next Steps
60
64
 
65
+ - [How Spree fits together](../core-concepts/architecture.md) — The catalog, the cart, the order and fulfillment — what each one is for, and the two APIs you build against. Twenty minutes here saves a lot of guessing later, and it ends by pointing you at whichever path suits you.
61
66
 
62
- - [Build with AI Agents](../agentic/overview.md) — Install the Spree agent skills and connect the docs MCP server — Claude Code, Cursor, Copilot, and 60+ other tools learn Spree's conventions and build features with you.
63
- - [Customization Tutorial](../tutorial/introduction.md) — Build a complete custom feature — model, admin UI, API, and TypeScript SDK — step by step.
64
- - [Core Concepts](../core-concepts/architecture.md) — Understand how Spree models commerce — stores, products, orders, payments.
65
- - [Next.js Storefront](../storefront/nextjs/quickstart.md) — Customize and extend the headless storefront.
66
-
67
+ In a hurry? Jump straight to the [tutorial](../tutorial/introduction.md) to build a feature end to end, the [Next.js Storefront](../storefront/nextjs/quickstart.md) to put a storefront in front of it, or the [API reference](../../api-reference/admin-api/introduction.md) to see what you can do with the Admin API.
67
68
 
68
69
  > **TIP:** Using an AI coding agent? Run `npx skills add spree/agent-skills` in your new project — it teaches your agent the customization patterns before you write a line of code. See [Agentic Development](../agentic/overview.md).
69
70
 
70
71
  ---
71
72
 
72
- Congrats! You've set up your Spree Commerce and it's looking amazing!
73
+ Congrats! You've set up your Spree application and it's looking amazing!
73
74
 
74
75
  Give Spree a [GitHub Star](https://github.com/spree/spree), why don't ya? Thank you for supporting Spree open-source!
75
76
 
@@ -150,7 +150,7 @@ module Spree
150
150
  private
151
151
 
152
152
  def matches_brand?(line_item)
153
- brand = line_item.product.get_metafield('details.brand')&.value
153
+ brand = line_item.product.get_custom_field('details.brand')&.value
154
154
  preferred_brand_names.include?(brand)
155
155
  end
156
156
  end
@@ -224,9 +224,9 @@ Background jobs (`IndexJob`, `RemoveJob`) fire on `after_commit` when `indexing_
224
224
 
225
225
  Always use prefixed IDs (`ctg_abc`, `prod_xyz`, `optval_abc`) when indexing. Never use raw database IDs — Spree supports UUID primary keys.
226
226
 
227
- ### Metafield search schema
227
+ ### Custom Field search schema
228
228
 
229
- Product metafields marked `searchable` / `sortable` on `MetafieldDefinition` are projected through `Spree::SearchProvider::MetafieldSchema` — shared by providers and `ProductPresenter`. Toggle participation with definition flags; run `rake spree:search:reindex` after changes when using Meilisearch.
229
+ Product custom fields marked `searchable` / `sortable` on `CustomFieldDefinition` are projected through `Spree::SearchProvider::CustomFieldSchema` — shared by providers and `ProductPresenter`. Toggle participation with definition flags; run `rake spree:search:reindex` after changes when using Meilisearch.
230
230
 
231
231
  Document shaping for `cf_*` fields stays on `search_product_presenter` — the only search-related dependency.
232
232
 
@@ -170,12 +170,12 @@ end
170
170
 
171
171
  ### Step 4 (optional): Add a Variant-side Predicate
172
172
 
173
- The example above relies on `variant.refrigerated?`. In a real plugin you'd back that with a [Custom Field](../core-concepts/metafields.md) on `Spree::Variant` — say a boolean metafield with key `refrigerated` — and define the predicate as a thin reader:
173
+ The example above relies on `variant.refrigerated?`. In a real plugin you'd back that with a [Custom Field](../core-concepts/metafields.md) on `Spree::Variant` — say a boolean custom field with key `refrigerated` — and define the predicate as a thin reader:
174
174
 
175
175
  ```ruby app/models/spree/variant_decorator.rb
176
176
  Spree::Variant.class_eval do
177
177
  def refrigerated?
178
- metafields.find_by(namespace: 'logistics', key: 'refrigerated')&.value == 'true'
178
+ custom_fields.find_by(namespace: 'logistics', key: 'refrigerated')&.value == 'true'
179
179
  end
180
180
  end
181
181
  ```
@@ -66,5 +66,5 @@ The SDK transforms `filter`/`sort` into the Ransack-flavored query string the Ad
66
66
  ## See also
67
67
 
68
68
  - The [Store SDK extending guide](../extending.md) covers the same pattern for storefront endpoints — the API is identical, only the base path (`/api/v3/store`) and auth header differ.
69
- - The [API](../../tutorial/admin-api.md) and [SDK](../../tutorial/storefront.md) tutorials walk through creating new Spree endpoints end-to-end (Rails controller, serializer, TypeScript types). Everything they show for the Store API applies equivalently for the Admin API.
69
+ - The [API](../../tutorial/model-and-api.md) and [SDK](../../tutorial/storefront.md) tutorials walk through creating new Spree endpoints end-to-end (Rails controller, serializer, TypeScript types). Everything they show for the Store API applies equivalently for the Admin API.
70
70
  - For building dashboard UI on top of your custom endpoint, see the [Dashboard](../../dashboard/overview.md) section — start with [Customization Quickstart](../../dashboard/customization/quickstart.md) for in-app changes or [Plugin Overview](../../dashboard/plugins/overview.md) for redistributable packages.
@@ -66,4 +66,4 @@ const page: PaginatedResponse<Order> = await client.orders.list()
66
66
  - [Admin API Reference](../../../api-reference/admin-api/introduction.md) — The underlying REST API the SDK wraps — every endpoint, parameter, and response shape.
67
67
 
68
68
 
69
- > **TIP:** Adding your own resources? The [`spree generate api_resource`](../../tutorial/admin-api.md) generator scaffolds Admin API endpoints (model, serializers, controllers, specs) in one command, and the [extending guide](../extending.md) shows how to call custom endpoints from the SDK.
69
+ > **TIP:** Adding your own resources? The [`spree generate api_resource`](../../tutorial/model-and-api.md) generator scaffolds Admin API endpoints (model, serializers, controllers, specs) in one command, and the [extending guide](../extending.md) shows how to call custom endpoints from the SDK.
@@ -127,4 +127,4 @@ await client.customFields('Spree::Product', 'prod_xxx').list()
127
127
 
128
128
  ## Adding your own resources
129
129
 
130
- The resource map grows with your store. The [`spree generate api_resource`](../../tutorial/admin-api.md) generator scaffolds new Admin API endpoints (model, serializers, controllers, routes, specs) in one command — then call them from the SDK with a [custom fetch or raw request](../extending.md), or generate a typed client for them.
130
+ The resource map grows with your store. The [`spree generate api_resource`](../../tutorial/model-and-api.md) generator scaffolds new Admin API endpoints (model, serializers, controllers, routes, specs) in one command — then call them from the SDK with a [custom fetch or raw request](../extending.md), or generate a typed client for them.
@@ -162,7 +162,7 @@ All types are exported as unprefixed names (e.g., `Product`, `Order`). Legacy `S
162
162
  - `OptionType` - Option type (e.g., Size, Color)
163
163
  - `OptionValue` - Option value (e.g., Small, Red)
164
164
  - `DigitalLink` - Digital download link
165
- - `Metafield` - Custom metafield data
165
+ - `CustomField` - Custom field data
166
166
 
167
167
  ### Wishlist Types
168
168
  - `Wishlist` - Wishlist
@@ -30,4 +30,4 @@ const nike = await client.request<Brand>('GET', '/brands/nike')
30
30
 
31
31
  `client.request` uses the same auth headers, retry logic, and locale/currency defaults as `client.products.list()` or any other built-in resource.
32
32
 
33
- For a complete walkthrough — creating the API endpoints on the backend, defining TypeScript types, filtering, and expanding associations — see the [API](../tutorial/admin-api.md) and [SDK](../tutorial/storefront.md) tutorials.
33
+ For a complete walkthrough — creating the API endpoints on the backend, defining TypeScript types, filtering, and expanding associations — see the [API](../tutorial/model-and-api.md) and [SDK](../tutorial/storefront.md) tutorials.