@spree/docs 0.1.248 → 0.1.249

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 (76) hide show
  1. package/dist/api-reference/admin-api/authentication.md +34 -14
  2. package/dist/api-reference/admin-api/endpoints.md +366 -14
  3. package/dist/api-reference/admin-api/errors.md +2 -2
  4. package/dist/api-reference/admin-api/introduction.md +3 -3
  5. package/dist/api-reference/admin-api/querying.md +6 -6
  6. package/dist/api-reference/store-api/monetary-amounts.md +5 -5
  7. package/dist/api-reference/webhooks-events.md +330 -335
  8. package/dist/developer/agentic/agent-skills.md +5 -2
  9. package/dist/developer/agentic/llm-docs.md +2 -1
  10. package/dist/developer/cli/admin-api.md +1 -1
  11. package/dist/developer/cli/quickstart.md +2 -2
  12. package/dist/developer/contributing/creating-an-extension.md +292 -146
  13. package/dist/developer/contributing/developing-spree.md +13 -17
  14. package/dist/developer/core-concepts/catalogs.md +2 -2
  15. package/dist/developer/core-concepts/channels.md +3 -3
  16. package/dist/developer/core-concepts/companies.md +2 -1
  17. package/dist/developer/core-concepts/delivery-setup.md +2 -2
  18. package/dist/developer/core-concepts/discounts.md +3 -3
  19. package/dist/developer/core-concepts/events.md +6 -5
  20. package/dist/developer/core-concepts/freight.md +3 -2
  21. package/dist/developer/core-concepts/fulfillments.md +10 -8
  22. package/dist/developer/core-concepts/imports-exports.md +11 -8
  23. package/dist/developer/core-concepts/inventory.md +2 -2
  24. package/dist/developer/core-concepts/media.md +14 -14
  25. package/dist/developer/core-concepts/orders.md +2 -2
  26. package/dist/developer/core-concepts/payments.md +1 -2
  27. package/dist/developer/core-concepts/products.md +6 -6
  28. package/dist/developer/core-concepts/reporting.md +4 -3
  29. package/dist/developer/core-concepts/returns-exchanges-claims.md +7 -7
  30. package/dist/developer/core-concepts/search-filtering.md +3 -3
  31. package/dist/developer/core-concepts/sellers.md +3 -3
  32. package/dist/developer/core-concepts/staff-roles.md +4 -2
  33. package/dist/developer/core-concepts/store-credits-gift-cards.md +1 -1
  34. package/dist/developer/core-concepts/stores.md +2 -2
  35. package/dist/developer/core-concepts/translations.md +12 -8
  36. package/dist/developer/core-concepts/webhooks.md +19 -18
  37. package/dist/developer/create-spree-app/quickstart.md +2 -7
  38. package/dist/developer/customization/api.md +1 -1
  39. package/dist/developer/customization/checkout.md +2 -2
  40. package/dist/developer/customization/dependencies.md +53 -37
  41. package/dist/developer/customization/permissions.md +2 -2
  42. package/dist/developer/dashboard/concepts.md +1 -1
  43. package/dist/developer/dashboard/customization/navigation.md +3 -2
  44. package/dist/developer/dashboard/customization/permissions.md +6 -6
  45. package/dist/developer/dashboard/plugins/publishing.md +4 -4
  46. package/dist/developer/dashboard/plugins/scaffolding.md +1 -1
  47. package/dist/developer/dashboard/public-api.md +1 -1
  48. package/dist/developer/dashboard/recipes/attribute-end-to-end.md +3 -18
  49. package/dist/developer/deployment/aws.md +1 -1
  50. package/dist/developer/deployment/aws_ecs.md +3 -3
  51. package/dist/developer/deployment/background_jobs.md +9 -3
  52. package/dist/developer/deployment/docker.md +1 -2
  53. package/dist/developer/deployment/emails.md +3 -1
  54. package/dist/developer/deployment/environment_variables.md +2 -2
  55. package/dist/developer/deployment/render.md +2 -2
  56. package/dist/developer/how-to/build-a-marketplace.md +2 -2
  57. package/dist/developer/how-to/custom-api-authentication.md +1 -1
  58. package/dist/developer/how-to/custom-delivery-rate-provider.md +11 -3
  59. package/dist/developer/how-to/custom-document-numbers.md +1 -1
  60. package/dist/developer/how-to/custom-order-routing.md +15 -14
  61. package/dist/developer/how-to/custom-payment-method.md +17 -19
  62. package/dist/developer/how-to/custom-promotion.md +4 -4
  63. package/dist/developer/how-to/custom-search-provider.md +12 -5
  64. package/dist/developer/how-to/custom-stock-splitter.md +25 -24
  65. package/dist/developer/how-to/sell-digital-products.md +1 -1
  66. package/dist/developer/multi-tenant/quickstart.md +2 -2
  67. package/dist/developer/providers/payouts.md +6 -2
  68. package/dist/developer/sdk/admin/querying-and-errors.md +1 -1
  69. package/dist/developer/sdk/admin/quickstart.md +4 -4
  70. package/dist/developer/sdk/authentication.md +5 -2
  71. package/dist/developer/sdk/store/cart-checkout.md +4 -4
  72. package/dist/developer/storefront/nextjs/emails.md +4 -2
  73. package/dist/developer/storefront/nextjs/testing.md +1 -1
  74. package/dist/developer/upgrades/5.6-to-6.0.md +51 -20
  75. package/dist/integrations/search/meilisearch.md +4 -4
  76. package/package.json +1 -1
@@ -98,10 +98,10 @@ await adminClient.sellers.suspend('sel_xxx', { reason: 'Unresolved delivery comp
98
98
  curl -X POST 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/invite' \
99
99
  -H 'X-Spree-API-Key: sk_xxx'
100
100
 
101
- curl -X POST 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/approve' \
101
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/approve' \
102
102
  -H 'X-Spree-API-Key: sk_xxx'
103
103
 
104
- curl -X POST 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/suspend' \
104
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/suspend' \
105
105
  -H 'X-Spree-API-Key: sk_xxx' \
106
106
  -H 'Content-Type: application/json' \
107
107
  -d '{ "reason": "Unresolved delivery complaints" }'
@@ -204,7 +204,7 @@ curl -X POST 'https://marketplace.example.com/api/v3/seller/products' \
204
204
  -H 'Content-Type: application/json' \
205
205
  -d '{ "name": "Handmade Vase" }'
206
206
 
207
- curl -X POST 'https://marketplace.example.com/api/v3/seller/products/prod_xxx/submit' \
207
+ curl -X PATCH 'https://marketplace.example.com/api/v3/seller/products/prod_xxx/submit' \
208
208
  -H 'Authorization: Bearer $SELLER_JWT' \
209
209
  -H 'X-Spree-Seller-Id: sel_xxx'
210
210
  ```
@@ -211,9 +211,11 @@ The invitation system publishes [events](events.md) you can subscribe to:
211
211
 
212
212
  ## Permissions
213
213
 
214
- Spree uses [CanCanCan](https://github.com/CanCanCommunity/cancancan) for authorization. Permissions apply to both customers (Store API access) and admins (Admin Panel access).
214
+ A role holds flat permission keys from one catalog (`read_orders`, `write_products`, …) — the same vocabulary as secret API key scopes. Every Admin API request checks the staff member's keys on the current store against the endpoint's `read_<resource>` / `write_<resource>` key and returns `403` with `details.required_permission` when it is missing. Staff endpoints (`/admin_users`, `/invitations`, `/roles`) need `read_staff` / `write_staff`.
215
215
 
216
- See the [Customize Permissions guide](../customization/permissions.md) for details on creating custom roles and permission sets.
216
+ Permissions apply to staff only. Customers never hold roles: the Store API authorizes them by ownership-scoped queries plus `Spree::Storefront::AccessPolicy`.
217
+
218
+ See the [Customize Permissions guide](../customization/permissions.md) for details on creating custom roles and registering your own permission keys.
217
219
 
218
220
  ## Related Documentation
219
221
 
@@ -18,7 +18,7 @@ Spree provides two stored value mechanisms that customers can use at checkout:
18
18
  | Has code | No | Yes |
19
19
  | Created by | Admin | Admin |
20
20
  | Assignment | Directly to customer | Applied to order via code |
21
- | Expiration | Configurable by type | Configurable per card |
21
+ | Expiration | Never expire | Optional, per card |
22
22
 
23
23
  ## Store Credits
24
24
 
@@ -5,7 +5,7 @@ description: Understand Spree Stores — the top-level tenant boundary that scop
5
5
 
6
6
  ## Overview
7
7
 
8
- The Store is the top-level tenant in Spree. Every resource — products, orders, channels, markets, taxonomies — belongs to exactly one store. A store owns its [channels](channels.md) (online, POS, wholesale, …), its [markets](markets.md) (region/currency/locale), and its [product catalog](products.md).
8
+ The Store is the top-level tenant in Spree. Every resource — products, orders, channels, markets, categories — belongs to exactly one store. A store owns its [channels](channels.md) (online, POS, wholesale, …), its [markets](markets.md) (region/currency/locale), and its [product catalog](products.md).
9
9
 
10
10
  ```mermaid
11
11
  erDiagram
@@ -85,7 +85,7 @@ The store sets the fallback for **storefront access gating** — whether anonymo
85
85
 
86
86
  ## Store Resources
87
87
 
88
- Each store owns its own resources. Products, orders, channels, markets, and taxonomies in one store are independent from another.
88
+ Each store owns its own resources. Products, orders, channels, markets, and categories in one store are independent from another.
89
89
 
90
90
  | Resource | Relationship |
91
91
  |----------|-------------|
@@ -85,26 +85,30 @@ If a field has no translation for the requested locale, you get the default-lang
85
85
 
86
86
  Merchants translate in the dashboard, switching locale on the record they're editing. For bulk work — handing a catalogue to a translation agency — export to CSV, translate, and import it back.
87
87
 
88
- Translations can also be written through the Admin API, which is what you'd use to sync from an external translation service:
88
+ Translations can also be written through the Admin API, which is what you'd use to sync from an external translation service. Writes go through one batch endpoint — every entry succeeds or none do — so a product and its option values can be translated in a single request:
89
89
 
90
90
 
91
91
  ```typescript Admin SDK
92
- await adminClient.products.update('prod_xxx', {
93
- translations: {
94
- fr: { name: 'Sac Spree', description: 'Un sac fourre-tout élégant…' },
95
- de: { name: 'Spree Tasche' },
92
+ await adminClient.translations.batch([
93
+ {
94
+ resource_type: 'product',
95
+ resource_id: 'prod_xxx',
96
+ values: {
97
+ fr: { name: 'Sac Spree', description: 'Un sac fourre-tout élégant…' },
98
+ de: { name: 'Spree Tasche' },
99
+ },
96
100
  },
97
- })
101
+ ])
98
102
 
99
103
  // Which resources and fields accept translations
100
104
  const { data: resources } = await adminClient.translatableResources.list()
101
105
  ```
102
106
 
103
107
  ```bash cURL
104
- curl -X PATCH 'https://api.mystore.com/api/v3/admin/products/prod_xxx' \
108
+ curl -X POST 'https://api.mystore.com/api/v3/admin/translations/batch' \
105
109
  -H 'X-Spree-API-Key: sk_xxx' \
106
110
  -H 'Content-Type: application/json' \
107
- -d '{ "translations": { "de": { "name": "Baumwolltasche" } } }'
111
+ -d '{ "translations": [ { "resource_type": "product", "resource_id": "prod_xxx", "values": { "de": { "name": "Baumwolltasche" } } } ] }'
108
112
  ```
109
113
 
110
114
 
@@ -12,7 +12,7 @@ Webhooks are built on top of Spree's [event system](events.md), providing:
12
12
  - **Multi-store support** - Each store has its own webhook endpoints
13
13
  - **Event filtering** - Subscribe to specific events or patterns with wildcards
14
14
  - **Secure delivery** - HMAC-SHA256 signatures for payload verification
15
- - **Automatic retries** - Failed deliveries retry with exponential backoff
15
+ - **Failure handling** - Failed deliveries are recorded and can be re-sent, and an endpoint that keeps failing is disabled
16
16
  - **Full audit trail** - Track every delivery attempt with response codes and timing
17
17
 
18
18
  ## How Webhooks Work
@@ -34,10 +34,9 @@ flowchart LR
34
34
 
35
35
  subgraph External Service
36
36
  H --> I{Response}
37
- I -->|2xx| J[Success]
38
- I -->|Error| K[Retry with Backoff]
39
- K -->|Max Retries| L[Mark Failed]
40
- K -->|Retry| H
37
+ I -->|2xx| J[Mark Successful]
38
+ I -->|Error| K[Mark Failed]
39
+ K -->|15 failures in a row| L[Disable Endpoint]
41
40
  end
42
41
  ```
43
42
 
@@ -51,7 +50,7 @@ flowchart LR
51
50
 
52
51
  ### Via Admin Panel
53
52
 
54
- Navigate to **Settings → Developers → Webhooks** in the admin panel to create and manage webhook endpoints.
53
+ Navigate to **Settings → Developer → Webhooks** in the admin panel to create and manage webhook endpoints.
55
54
 
56
55
  ### Via the Admin API
57
56
 
@@ -139,7 +138,7 @@ The `subscriptions` array accepts exact event names and wildcard patterns:
139
138
  | `['order.placed', 'order.canceled']` | Only those two events |
140
139
  | `['order.*']` | All order events |
141
140
  | `['*.created']` | All creation events |
142
- | `['order.*', 'payment.*', 'shipment.shipped']` | Multiple patterns |
141
+ | `['order.*', 'payment.*', 'fulfillment.fulfilled']` | Multiple patterns |
143
142
  | `[]` or `['*']` | All events |
144
143
 
145
144
  ## Webhook Payload
@@ -154,7 +153,7 @@ Each webhook delivery sends a JSON payload with the following structure. The `da
154
153
  "data": {
155
154
  "id": "or_m3Rp9wXz",
156
155
  "number": "R123456789",
157
- "fulfillment_status": "shipped",
156
+ "fulfillment_status": "unfulfilled",
158
157
  "payment_status": "paid",
159
158
  "total": "99.99",
160
159
  "display_total": "$99.99",
@@ -165,7 +164,7 @@ Each webhook delivery sends a JSON payload with the following structure. The `da
165
164
  "payments": [ ... ]
166
165
  },
167
166
  "metadata": {
168
- "spree_version": "5.1.0"
167
+ "spree_version": "6.0.0"
169
168
  }
170
169
  }
171
170
  ```
@@ -243,7 +242,7 @@ class WebhooksController < ApplicationController
243
242
 
244
243
  case event['name']
245
244
  when 'order.placed'
246
- handle_order_completed(event['data'])
245
+ handle_order_placed(event['data'])
247
246
  when 'product.updated'
248
247
  handle_product_updated(event['data'])
249
248
  end
@@ -266,11 +265,13 @@ class WebhooksController < ApplicationController
266
265
  end
267
266
  ```
268
267
 
269
- ## Delivery Status & Retries
268
+ ## Delivery Status
270
269
 
271
- ### Automatic Retries
270
+ ### Failed Deliveries
272
271
 
273
- Failed webhook deliveries automatically retry up to 5 times with exponential backoff. This handles temporary network issues and endpoint downtime.
272
+ Spree sends each delivery once. Any response other than 2xx — including a timeout or a connection error — marks the delivery as failed, and Spree does not send it again on its own. To send a failed delivery again, re-send it from the delivery log (see below).
273
+
274
+ If an endpoint fails 15 deliveries in a row, Spree disables it and emails the store staff. Fix the endpoint, then turn it back on by marking it active again.
274
275
 
275
276
  ### Checking Delivery Status
276
277
 
@@ -321,7 +322,7 @@ SSL certificates are verified in production, and not in development — so you c
321
322
 
322
323
  ## Available Events
323
324
 
324
- Webhooks can subscribe to any event in Spree's event system. See [Events](events.md#available-events) for a complete list.
325
+ Webhooks can subscribe to any event in Spree's event system. See [Webhook Events & Payloads](../../api-reference/webhooks-events.md) for the complete list.
325
326
 
326
327
  Common webhook events include:
327
328
 
@@ -330,12 +331,12 @@ Common webhook events include:
330
331
  | `order.placed` | A customer completed checkout |
331
332
  | `order.paid` | The order is fully paid |
332
333
  | `order.canceled` | The order was canceled |
333
- | `order.shipped` | Everything on the order has shipped |
334
- | `fulfillment.shipped` | One parcel went out |
334
+ | `order.fulfilled` | Everything on the order has shipped |
335
+ | `fulfillment.fulfilled` | One parcel went out |
335
336
  | `payment.completed` | A payment succeeded |
336
337
  | `product.created` / `product.updated` | Catalog changes |
337
338
  | `product.out_of_stock` / `product.back_in_stock` | Availability flipped |
338
- | `customer.created` | A new customer registered |
339
+ | `user.created` | A new customer registered |
339
340
  | `return.received` | A return arrived |
340
341
 
341
342
  ## Testing Webhooks
@@ -386,7 +387,7 @@ curl -X POST 'https://api.mystore.com/api/v3/admin/webhook_endpoints' \
386
387
 
387
388
  - **Verify signatures** — Always verify the `X-Spree-Webhook-Signature` header to ensure the webhook is authentic.
388
389
 
389
- - **Handle duplicates** — Use the event `id` to detect and handle duplicate deliveries. Webhooks may be retried.
390
+ - **Handle duplicates** — Use the event `id` to detect and handle duplicate deliveries. A delivery that is re-sent carries the same event `id`.
390
391
 
391
392
  - **Subscribe selectively** — Only subscribe to events you need. Use specific patterns rather than `*` when possible.
392
393
 
@@ -123,14 +123,9 @@ The project includes [@spree/cli](../cli/quickstart.md) for managing your Spree
123
123
 
124
124
  ### Admin Dashboard
125
125
 
126
- Open [http://localhost:3000/admin](http://localhost:3000/admin) and log in with:
126
+ The dashboard runs as its own dev server: `spree dev` starts it alongside the API at [http://localhost:5173](http://localhost:5173), live-reloading from `apps/dashboard/`. See the [dashboard docs](../dashboard/overview.md).
127
127
 
128
- | | |
129
- |---|---|
130
- | **Email** | `spree@example.com` |
131
- | **Password** | `spree123` |
132
-
133
- The dashboard runs as its own dev server: `spree dev` starts it alongside the API at [http://localhost:5173](http://localhost:5173), with the same credentials, live-reloading from `apps/dashboard/`. See the [dashboard docs](../dashboard/overview.md).
128
+ No default admin account is created. On the first run, `spree dev` opens a one-time setup link where you create the admin account. If you missed it, print it again with `spree rails spree:setup:token`.
134
129
 
135
130
  ### Store API
136
131
 
@@ -141,7 +141,7 @@ The base `ResourceController` provides:
141
141
  | Pagination | [Pagy](https://github.com/ddnexus/pagy) — `?page=2&limit=25` |
142
142
  | Filtering | [Ransack](https://github.com/activerecord-hackery/ransack) — `?q[name_cont]=nike` |
143
143
  | Sorting | JSON:API style — `?sort=-name` |
144
- | Authorization | [CanCanCan](https://github.com/CanCanCommunity/cancancan) |
144
+ | Authorization | Ownership scoping — `scope` decides what the caller can see (Admin API controllers add the `scoped_resource` permission gate) |
145
145
  | Prefixed IDs | Stripe-style — `brand_k5nR8xLq` |
146
146
 
147
147
  Override these methods to customize:
@@ -34,7 +34,7 @@ Spree::Checkout::Registry.add_requirement(
34
34
  step: :address,
35
35
  field: :vat_number,
36
36
  message: 'VAT number is required for business orders',
37
- satisfied: ->(cart) { cart.custom_fields['vat_number'].present? }
37
+ satisfied: ->(cart) { cart.metadata['vat_number'].present? }
38
38
  )
39
39
  ```
40
40
 
@@ -58,7 +58,7 @@ A step bundles its own requirements and says when it is satisfied:
58
58
  Spree::Checkout::Registry.register_step(
59
59
  name: :loyalty,
60
60
  before: :payment,
61
- satisfied: ->(cart) { cart.custom_fields['loyalty_number'].present? },
61
+ satisfied: ->(cart) { cart.metadata['loyalty_number'].present? },
62
62
  requirements: ->(cart) {
63
63
  [{ step: 'loyalty', field: 'loyalty_number',
64
64
  message: 'Loyalty number is required' }]
@@ -6,8 +6,8 @@ section: customization
6
6
  ## Overview
7
7
 
8
8
  With Dependencies, you can replace parts of Spree core with your custom code:
9
- [Services and Workflows](workflows.md), CanCanCan
10
- Abilities (used for [Permissions](permissions.md)), and API Serializers (used for
9
+ [Services and Workflows](workflows.md), the staff ability class
10
+ and the storefront access policy (used for [Permissions](permissions.md)), and API Serializers (used for
11
11
  generating JSON API responses).
12
12
 
13
13
  > **TIP:** Replacing a whole class means keeping your copy in sync with every Spree
@@ -18,19 +18,19 @@ generating JSON API responses).
18
18
 
19
19
  ## Application (global) customization
20
20
 
21
- This will change every aspect of the application (both APIs, Admin Panel, and Storefront).
21
+ This will change every aspect of the application (the Store API, the Admin API, and everything built on them).
22
22
 
23
23
  In your `config/initializers/spree.rb` file, you can set the following:
24
24
 
25
25
  ```ruby
26
- Spree.cart_update_service = MyStore::CartUpdate
26
+ Spree.carts_update_service = MyStore::CartUpdate
27
27
  ```
28
28
 
29
29
  or using the block syntax:
30
30
 
31
31
  ```ruby
32
32
  Spree.dependencies do |dependencies|
33
- dependencies.cart_update_service = MyStore::CartUpdate
33
+ dependencies.carts_update_service = MyStore::CartUpdate
34
34
  end
35
35
  ```
36
36
 
@@ -86,6 +86,18 @@ Spree.cart_add_item_workflow = MyStore::AddItem
86
86
  > reasons people replace this class, and they don't need maintaining across
87
87
  > upgrades.
88
88
 
89
+ > **WARNING:** A subclass runs its hooks under its **own** key, derived from its class name —
90
+ > `MyStore::AddItem` fires `my_store.add_item.*`, not `carts.add_item.*`. Hooks
91
+ > registered on the original key (by you or by an installed extension) stop
92
+ > firing once you swap the class in. To keep them, declare the original key in
93
+ > the subclass:
94
+ >
95
+ > ```ruby
96
+ class AddItem < Spree::Carts::AddItem
97
+ workflow_key 'carts.add_item'
98
+ end
99
+ ```
100
+
89
101
  ## Using dependencies in your code
90
102
 
91
103
  When you need to use a dependency in your code, you can access it directly via the `Spree` module:
@@ -95,60 +107,53 @@ When you need to use a dependency in your code, you can access it directly via t
95
107
  Spree.cart_add_item_workflow.call(cart: cart, variant: variant, quantity: 1)
96
108
 
97
109
  # For API dependencies, use the Spree.api accessor
98
- Spree.api.storefront_cart_serializer.new(order).serializable_hash
110
+ Spree.api.cart_serializer.new(cart).serializable_hash
99
111
  ```
100
112
 
101
113
  ## Controller level customization
102
114
 
103
- If you need to replace [serializers](https://github.com/jsonapi-serializer/jsonapi-serializer) or Services in a specific API endpoint you can create a [code decorator](decorators.md):
115
+ If you need to replace a serializer in a specific API endpoint only, you can create a [code decorator](decorators.md):
104
116
 
105
117
  ```bash
106
- mkdir -p app/controllers/spree && touch app/controllers/spree/cart_controller_decorator.rb
118
+ mkdir -p app/controllers/spree/api/v3/store && touch app/controllers/spree/api/v3/store/carts_controller_decorator.rb
107
119
  ```
108
120
 
109
121
  and add the following code to it:
110
122
 
111
123
  ```ruby
112
124
  module Spree
113
- module CartControllerDecorator
114
- def resource_serializer
115
- MyNewAwesomeCartSerializer
116
- end
117
-
118
- def add_item_service
119
- MyNewAwesomeAddItemToCart
125
+ module Api
126
+ module V3
127
+ module Store
128
+ module CartsControllerDecorator
129
+ def serializer_class
130
+ MyNewAwesomeCartSerializer
131
+ end
132
+ end
133
+
134
+ CartsController.prepend(CartsControllerDecorator)
135
+ end
120
136
  end
121
137
  end
122
-
123
- CartController.prepend(CartControllerDecorator)
124
138
  end
125
139
  ```
126
140
 
127
- This will change the serializer in this API endpoint to `MyNewAwesomeCartSerializer` and also it will swap the default `add_item_service` to `MyNewAwesomeAddItemToCart`.
141
+ This will change the serializer in this API endpoint to `MyNewAwesomeCartSerializer`. Services and workflows are resolved through the global dependencies (e.g. `Spree.cart_add_item_workflow`), so swap those at the application level.
128
142
 
129
- Different API endpoints can have different dependency injection points. You can review their [source code](https://github.com/spree/spree/tree/main/api/app/controllers/spree/api/v3) to see what you can replace.
143
+ Different API endpoints can have different dependency injection points. You can review their [source code](https://github.com/spree/spree/tree/main/spree/api/app/controllers/spree/api/v3) to see what you can replace.
130
144
 
131
145
  ## API level customization
132
146
 
133
- Storefront API and Platform API have separate Dependencies injection points so you can easily customize one without touching the other.
147
+ API serializers have their own injection points under `Spree.api` — Store API serializers (`cart_serializer`, `product_serializer`, …) and Admin API serializers (`admin_order_serializer`, …) — so you can customize one surface without touching the other.
134
148
 
135
149
  In your Spree initializer (`config/initializers/spree.rb`) please add:
136
150
 
137
151
  ```ruby
138
- Spree.api.storefront_cart_serializer = MyNewAwesomeCartSerializer
139
- Spree.api.storefront_cart_add_item_service = MyNewAwesomeAddItemToCart
152
+ Spree.api.cart_serializer = MyNewAwesomeCartSerializer
153
+ Spree.api.admin_order_serializer = MyNewAwesomeAdminOrderSerializer
140
154
  ```
141
155
 
142
- This will swap the default Cart serializer and Add Item to Cart service for your custom ones within all Storefront API endpoints that use those classes.
143
-
144
- You can mix and match both global and API-level customizations:
145
-
146
- ```ruby
147
- Spree.cart_add_item_workflow = MyNewAwesomeAddItemToCart
148
- Spree.api.storefront_cart_add_item_service = AnotherAddItemToCart
149
- ```
150
-
151
- The second line will have precedence over the first one, and the Storefront API will use `AnotherAddItemToCart` and the rest of the application will use `MyNewAwesomeAddItemToCart`.
156
+ This will swap the default serializers for your custom ones within all API endpoints that use them.
152
157
 
153
158
  ## Debugging dependencies
154
159
 
@@ -170,8 +175,8 @@ cart_recalculate_workflow Spree::Carts::Recalculate [OVERRIDDEN]
170
175
  ...
171
176
 
172
177
  [API]
173
- storefront_cart_serializer Spree::V2::Storefront::CartSerializer
174
- storefront_cart_add_item_service MyApp::CartAddItem [OVERRIDDEN]
178
+ cart_serializer Spree::Api::V3::CartSerializer
179
+ product_serializer MyApp::ProductSerializer [OVERRIDDEN]
175
180
  ...
176
181
  ```
177
182
 
@@ -194,7 +199,7 @@ This shows only the dependencies that have been customized, along with their ori
194
199
  cart_recalculate_workflow Spree::Carts::Recalculate -> MyApp::CartRecalculate (config/initializers/spree.rb:15)
195
200
 
196
201
  [API OVERRIDES]
197
- storefront_cart_add_item_service Spree::Carts::AddItem -> MyApp::CartAddItem (config/initializers/spree.rb:20)
202
+ product_serializer Spree::Api::V3::ProductSerializer -> MyApp::ProductSerializer (config/initializers/spree.rb:20)
198
203
  ```
199
204
 
200
205
  ### Validate all dependencies
@@ -243,11 +248,16 @@ Seams backed by a [workflow](workflows.md) use a
243
248
  | `cart_add_item_service` | `cart_add_item_workflow` |
244
249
  | `cart_recalculate_service` | `cart_recalculate_workflow` |
245
250
  | `carts_complete_service` | `carts_complete_workflow` |
251
+ | `carts_upsert_items_service` | `cart_upsert_items_workflow` |
246
252
  | `cart_merge_strategy` | `cart_merge_workflow` |
247
253
  | `order_cancel_service` | `order_cancel_workflow` |
248
254
  | `order_complete_service` | `order_complete_workflow` |
249
255
  | `fulfillment_create_service` | `fulfillment_create_workflow` |
250
256
  | `payments_handle_webhook_service` | `payments_handle_webhook_workflow` |
257
+ | `shipment_update_service`, `fulfillment_update_service` | `fulfillment_update_workflow` |
258
+ | `gift_card_apply_service` | `gift_card_apply_workflow` |
259
+ | `gift_card_remove_service` | `gift_card_remove_workflow` |
260
+ | `gift_card_redeem_service` | `gift_card_redeem_workflow` |
251
261
 
252
262
  > **WARNING:** The legacy names stay readable, but **assigning to one no longer has any
253
263
  > effect** — the override is recorded and a deprecation warning names the seam to
@@ -258,6 +268,12 @@ Seams backed by a [workflow](workflows.md) use a
258
268
  > If you override any of these, move to the `*_workflow` name and make sure your
259
269
  > class subclasses the workflow.
260
270
 
271
+ Two seams are plain renames with an unchanged contract, so an override set
272
+ under the old name is still applied (with a deprecation warning):
273
+ `checkout_add_store_credit_service` → `store_credit_apply_service` and
274
+ `checkout_remove_store_credit_service` → `store_credit_remove_service`. All
275
+ legacy names are removed in Spree 6.1.
276
+
261
277
  ## Backwards compatibility
262
278
 
263
279
  The legacy string-based syntax is still supported for backwards compatibility:
@@ -280,5 +296,5 @@ Default values can be easily checked by:
280
296
 
281
297
  1. Using the rake task: `bin/rake spree:dependencies:list`
282
298
  2. Looking at the source code:
283
- * [Application (global) dependencies](https://github.com/spree/spree/blob/main/core/lib/spree/core/dependencies.rb)
284
- * [API level dependencies](https://github.com/spree/spree/blob/main/api/lib/spree/api/dependencies.rb)
299
+ * [Application (global) dependencies](https://github.com/spree/spree/blob/main/spree/core/lib/spree/core/dependencies.rb)
300
+ * [API level dependencies](https://github.com/spree/spree/blob/main/spree/api/lib/spree/api/dependencies.rb)
@@ -11,7 +11,7 @@ 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 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.
14
+ Roles are data, so create them where you manage the rest of your store's data: **Settings → Roles** in the dashboard, or the Admin API when you want it scripted.
15
15
 
16
16
  ```bash
17
17
  spree api post roles --data '{
@@ -68,6 +68,6 @@ The catalog is deliberately flat: it answers "may this role touch this kind of r
68
68
 
69
69
  ## How enforcement works
70
70
 
71
- Every Admin API controller declares its resource (`scoped_resource :orders`). Each request checks the principal's keys — a staff member's role permissions on the current store, or a secret key's scopes — against `read_<resource>` or `write_<resource>` for the action. A missing key produces a 403 naming it in `details.required_permission`. Behind that gate, keys compile to CanCanCan rules for record-level concerns, and `GET /api/v3/admin/me` returns both the rule dump the dashboard mirrors and `permission_keys`, the flat key list.
71
+ Every Admin API controller declares its resource (`scoped_resource :orders`). Each request checks the principal's keys — a staff member's role permissions on the current store, or a secret key's scopes — against `read_<resource>` or `write_<resource>` for the action. A missing key produces a 403 naming it — `details.required_permission` for staff, `details.required_scope` for a secret key. Behind that gate, keys compile to CanCanCan rules for record-level concerns, and `GET /api/v3/admin/me` returns both the rule dump the dashboard mirrors and `permission_keys`, the flat key list.
72
72
 
73
73
  Storefront customers are not part of this system — customer authorization is ownership, enforced by the Store API's scoped lookups, with nothing to configure. The one swappable piece is `Spree::Storefront::AccessPolicy` (via `Spree::Dependencies.storefront_access_policy_class`): a generic `readable?` / `writable?` / `scope` protocol that defaults to "the caller owns the record", with carts and orders adding guest-token access. Replace it only when access must widen beyond the owner, such as company accounts sharing purchases or wishlists.
@@ -55,7 +55,7 @@ The facade `defineDashboardPlugin({ nav, routes, slots, … })` groups all five
55
55
  The signed-in admin's identity and abilities are exposed via three providers wrapping the app:
56
56
 
57
57
  - `<AuthProvider>` — the current admin user; `useAuth()` returns `{ user, signIn, signOut, … }`
58
- - `<PermissionProvider>` — CanCanCan abilities; `usePermissions()` returns `{ permissions, rules, isLoading }`, and the checks live on `permissions.can(...)`
58
+ - `<PermissionProvider>` — the admin's permissions on the current store; `usePermissions()` returns `{ permissions, rules, permissionKeys, isLoading, refresh }`, and the checks live on `permissions.can(...)`
59
59
  - `<StoreProvider>` — current store + timezone + currency; `useStore()` returns `{ storeId, store, … }`
60
60
 
61
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.
@@ -22,7 +22,7 @@ nav.add({
22
22
  path: '/analytics', // prefixed with /$storeId at render time
23
23
  icon: BarChartIcon,
24
24
  position: 650,
25
- subject: 'Spree::Order', // optional CanCanCan subject — hides item without read permission
25
+ subject: 'Spree::Order', // optional permission subject — hides item without read permission
26
26
  })
27
27
  ```
28
28
 
@@ -49,6 +49,7 @@ defineDashboardPlugin({
49
49
  nav.add({
50
50
  key: 'analytics',
51
51
  label: 'Analytics',
52
+ path: '/analytics', // required, even on a parent
52
53
  icon: BarChartIcon,
53
54
  position: 650,
54
55
  children: [
@@ -196,7 +197,7 @@ settingsNav.add({
196
197
 
197
198
  ## Permission gating
198
199
 
199
- `subject` on a nav entry checks `permissions.can('read', subject)` — when it fails, the sidebar item is hidden. **This is UX, not authorization.** The backend still enforces CanCanCan via `authorize!` on every API call. Hiding the link is a hint, not a security boundary.
200
+ `subject` on a nav entry checks `permissions.can('read', subject)` — when it fails, the sidebar item is hidden. **This is UX, not authorization.** The backend still enforces permissions on every API call. Hiding the link is a hint, not a security boundary.
200
201
 
201
202
  ## Order of operations
202
203
 
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  title: Permissions
3
3
  sidebarTitle: Permissions
4
- description: How to hide UI behind CanCanCan permissions in your customisations — and why hiding is never the same as authorising.
4
+ description: How to hide UI behind the current admin's permissions in your customisations — and why hiding is never the same as authorising.
5
5
  ---
6
6
 
7
7
  > **NOTE:** Examples here import from `@spree/dashboard`, which re-exports the framework and the design system for host applications. A **distributed plugin** imports `@spree/dashboard-core` and `@spree/dashboard-ui` directly instead — it extends the shell rather than shipping it. See [plugin overview](../plugins/overview.md).
8
8
 
9
- The dashboard exposes the current admin's CanCanCan abilities to every component via the `usePermissions()` hook. The registry surfaces — nav entries, settings entries, custom routes — also accept a `subject` shortcut, and nav entries take a generic `if` predicate that reads from `permissions`.
9
+ The dashboard exposes the current admin's permissions to every component via the `usePermissions()` hook: the catalog keys their role holds on the current store (`read_orders`, `write_products`, …) and the class-level ability rules the server compiles from those keys. The registry surfaces — nav entries, settings entries, custom routes — also accept a `subject` shortcut, and nav entries take a generic `if` predicate that reads from `permissions`.
10
10
 
11
11
  ## Two layers
12
12
 
@@ -15,9 +15,9 @@ UI gating is for **UX**. Backend authorization is for **security**. They are not
15
15
  | Layer | Where | What it does |
16
16
  |---|---|---|
17
17
  | UI gating | Dashboard registries (`subject`, `if`) | Hides menu items, columns, buttons — keeps the interface tidy |
18
- | Authorization | Rails API controllers (CanCanCan, scopes) | Refuses the request when the user doesn't have permission |
18
+ | Authorization | Rails API controllers (per-controller `read_*`/`write_*` key gate, record-level checks) | Refuses the request when the user doesn't have permission |
19
19
 
20
- **Always rely on the backend for security.** Hiding a button is a hint to the user, not a wall. A user with browser dev tools (or the API token) can always hit the endpoint directly — your `authorize!` call is what stops them.
20
+ **Always rely on the backend for security.** Hiding a button is a hint to the user, not a wall. A user with browser dev tools (or the API token) can always hit the endpoint directly — the API's permission check is what stops them.
21
21
 
22
22
  ## The `permissions` object
23
23
 
@@ -87,7 +87,7 @@ Use it for combined checks (permission + store state), feature flags, multi-acti
87
87
 
88
88
  ## Inside components
89
89
 
90
- Use the `usePermissions()` hook — it returns `{ permissions, rules, isLoading }`:
90
+ Use the `usePermissions()` hook — it returns `{ permissions, rules, permissionKeys, isLoading, refresh }`. `permissionKeys` is the flat key list (the same vocabulary as the role editor and API-key scopes), for when `permissionKeys.includes('read_orders')` reads better than a subject check:
91
91
 
92
92
  ```tsx
93
93
  import { usePermissions } from '@spree/dashboard'
@@ -110,4 +110,4 @@ If your customization introduces a new model on the backend, register it as a pe
110
110
  ## Reference
111
111
 
112
112
  - [`Permissions` interface](https://github.com/spree/spree/blob/main/packages/dashboard-core/src/providers/permission-provider.tsx)
113
- - [CanCanCan docs](https://github.com/CanCanCommunity/cancancan) — the Ruby ability definition language
113
+ - [Customize Permissions](../../customization/permissions.md) — the backend permission catalog and `register_scope`
@@ -15,9 +15,9 @@ Your plugin imports React, the dashboard, Tailwind, i18next, etc., but **you don
15
15
  "peerDependencies": {
16
16
  "react": "^19",
17
17
  "react-dom": "^19",
18
- "@spree/dashboard-core": "^0.10.0",
19
- "@spree/dashboard-ui": "^0.10.0",
20
- "@spree/admin-sdk": "^0.6.0",
18
+ "@spree/dashboard-core": "^1.0.0-beta.3",
19
+ "@spree/dashboard-ui": "^1.0.0-beta.3",
20
+ "@spree/admin-sdk": "^1.0.0-beta.2",
21
21
  "i18next": "^26",
22
22
  "react-i18next": "^17",
23
23
  "@tanstack/react-query": "^5",
@@ -27,7 +27,7 @@ Your plugin imports React, the dashboard, Tailwind, i18next, etc., but **you don
27
27
  }
28
28
  ```
29
29
 
30
- Use ranges, not exact versions — pin too tightly and consumers can't upgrade the dashboard without you cutting a release. One Developer Preview caveat: for `0.x` versions, `^` only allows patch-level drift (`^0.10.0` matches `0.10.x`, not `0.11.0`), so expect to bump your `@spree/dashboard-*` peers alongside dashboard releases until 1.0. `spree plugin new` scaffolds these ranges for you, matched to the current release.
30
+ Use ranges, not exact versions — pin too tightly and consumers can't upgrade the dashboard without you cutting a release. One pre-release caveat: a range like `^1.0.0-beta.3` accepts later `1.0.0` betas and every stable `1.x` release, but not betas of other versions — so keep your `@spree/dashboard-*` peers in step with the dashboard until 1.0 ships. Check the ranges `spree plugin new` scaffolds against the versions your host app installs.
31
31
 
32
32
  `dependencies` is for things your plugin *uses* that the host *doesn't* — small utilities (e.g., `clsx`, `date-fns` if you need a specific version, your own helper packages). When in doubt, peer-dep it. The trade-off is "user has to install one more thing" vs. "user has two copies of React" — always pay the first cost.
33
33
 
@@ -15,7 +15,7 @@ your-plugin/
15
15
  ├── .gitignore
16
16
  └── packages/
17
17
  └── dashboard/
18
- ├── package.json # @your-scope/dashboard-your-plugin
18
+ ├── package.json # @your-scope/your-plugin-dashboard
19
19
  ├── tsconfig.json
20
20
  ├── biome.json
21
21
  └── src/
@@ -84,7 +84,7 @@ import {
84
84
  ```ts
85
85
  import {
86
86
  useAuth, // current admin user
87
- usePermissions, // CanCanCan abilities
87
+ usePermissions, // current admin's permissions
88
88
  useStore, // current store
89
89
  useCommandPalette, // ⌘K palette open/close state
90
90
  useGlobalSearch, // global search query state