@spree/docs 0.1.85 → 0.1.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-reference/admin-api/introduction.md +0 -12
- package/dist/developer/agentic/agent-skills.md +64 -0
- package/dist/developer/agentic/llm-docs.md +55 -0
- package/dist/developer/agentic/mcp.md +75 -0
- package/dist/developer/agentic/overview.md +31 -0
- package/dist/developer/cli/quickstart.md +5 -0
- package/dist/developer/contributing/developing-spree.md +3 -5
- package/dist/developer/core-concepts/events.md +80 -74
- package/dist/developer/customization/api.md +4 -5
- package/dist/developer/customization/checkout.md +11 -13
- package/dist/developer/customization/decorators.md +11 -13
- package/dist/developer/customization/quickstart.md +8 -10
- package/dist/developer/getting-started/quickstart.md +12 -1
- package/dist/developer/sdk/extending.md +2 -2
- package/dist/developer/sdk/quickstart.md +5 -1
- package/dist/developer/tutorial/admin.md +87 -17
- package/dist/developer/tutorial/{store-api.md → api.md} +118 -14
- package/dist/developer/tutorial/events.md +166 -0
- package/dist/developer/tutorial/extending-models.md +26 -22
- package/dist/developer/tutorial/introduction.md +30 -22
- package/dist/developer/tutorial/model.md +80 -19
- package/dist/developer/tutorial/sdk.md +6 -5
- package/dist/developer/tutorial/testing.md +48 -31
- package/package.json +1 -1
- package/dist/developer/tutorial/file-uploads.md +0 -121
- package/dist/developer/tutorial/rich-text.md +0 -73
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Installing Spree"
|
|
3
3
|
sidebarTitle: Installation
|
|
4
|
-
description: "
|
|
4
|
+
description: "Install Spree Commerce locally with Docker or Ruby on Rails, run your first store, and deploy it to production with the official starter template."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Installation options
|
|
@@ -73,6 +73,17 @@ Upon successful authentication, you should see the admin screen:
|
|
|
73
73
|
|
|
74
74
|
<img src="/images/spree_admin_dashboard.png" />
|
|
75
75
|
|
|
76
|
+
## Next Steps
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
- [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.
|
|
80
|
+
- [Customization Tutorial](../tutorial/introduction.md) — Build a complete custom feature — model, admin UI, API, and TypeScript SDK — step by step.
|
|
81
|
+
- [Core Concepts](../core-concepts/architecture.md) — Understand how Spree models commerce — stores, products, orders, payments.
|
|
82
|
+
- [Next.js Storefront](../storefront/nextjs/quickstart.md) — Customize and extend the headless storefront.
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
> **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).
|
|
86
|
+
|
|
76
87
|
---
|
|
77
88
|
|
|
78
89
|
Congrats! You've set up your Spree Commerce and it's looking amazing!
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Custom Endpoints
|
|
3
3
|
sidebarTitle: Custom Endpoints
|
|
4
|
-
description: Call custom Store API endpoints
|
|
4
|
+
description: Call your own custom Store API endpoints from the Spree TypeScript SDK using the built-in request method, with typed responses and shared auth handling.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
The client exposes a `request` method — the same function that powers all built-in resources. Use it to call any Store API endpoint, including ones you've added yourself.
|
|
@@ -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 [
|
|
33
|
+
For a complete walkthrough — creating the API endpoints on the backend, defining TypeScript types, filtering, and expanding associations — see the [API](../tutorial/api.md) and [SDK](../tutorial/sdk.md) tutorials.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Quickstart
|
|
3
|
-
description: Install
|
|
3
|
+
description: Install the Spree TypeScript SDK, configure your Store API client, and make your first calls to products, carts, checkout, and customer endpoints.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## Installation
|
|
@@ -82,3 +82,7 @@ await client.customer.addresses.list({}, options);
|
|
|
82
82
|
await client.categories.products.list(categoryId, params, options);
|
|
83
83
|
await client.wishlists.items.create(wishlistId, params, options);
|
|
84
84
|
```
|
|
85
|
+
|
|
86
|
+
## Pair with AI agents
|
|
87
|
+
|
|
88
|
+
Building your storefront with an AI coding agent? The [Spree agent skills](../agentic/agent-skills.md) include dedicated SDK and storefront skills (typed clients, Ransack filtering, error handling, custom endpoints), and the [docs MCP server](../agentic/mcp.md) gives your agent the full SDK reference on demand. See [Agentic Development](../agentic/overview.md).
|
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Admin Dashboard
|
|
3
|
-
description:
|
|
3
|
+
description: Scaffold the Spree admin UI for the Brands resource and add a rich text description editor, an Active Storage logo upload, and a custom table column.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Now that we've created
|
|
6
|
+
Now that we've created the `Brand` model, let's create an Admin Dashboard interface so admins can manage brands — including editing the rich text description and uploading the logo.
|
|
7
7
|
|
|
8
|
-
## Step 1:
|
|
8
|
+
## Step 1: Scaffold the Admin UI
|
|
9
9
|
|
|
10
|
-
Admin Scaffold generator
|
|
10
|
+
The Admin Scaffold generator creates a complete admin section for a resource:
|
|
11
11
|
|
|
12
|
-
Run the following command in our Spree application root directory:
|
|
13
12
|
|
|
14
|
-
```bash
|
|
13
|
+
```bash Spree CLI (Docker)
|
|
14
|
+
spree generate spree:admin:scaffold Spree::Brand
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```bash Without Spree CLI
|
|
15
18
|
bin/rails g spree:admin:scaffold Spree::Brand
|
|
16
19
|
```
|
|
17
20
|
|
|
21
|
+
|
|
18
22
|
This will create the following files:
|
|
19
23
|
|
|
20
24
|
| File Type | Path | Description |
|
|
21
25
|
|-----------|------|-------------|
|
|
22
26
|
| Controller | `app/controllers/spree/admin/brands_controller.rb` | Handles the logic for the brands resource. |
|
|
23
|
-
| View | `app/views/spree/admin/brands/index.html.erb` | Displays the list of brands using the
|
|
27
|
+
| View | `app/views/spree/admin/brands/index.html.erb` | Displays the list of brands using the tables system. |
|
|
24
28
|
| View | `app/views/spree/admin/brands/new.html.erb` | Displays the new brand form. |
|
|
25
29
|
| View | `app/views/spree/admin/brands/edit.html.erb` | Displays the edit brand form. |
|
|
26
30
|
| Partial | `app/views/spree/admin/brands/_form.html.erb` | The form partial used in new and edit views. |
|
|
@@ -37,7 +41,54 @@ end
|
|
|
37
41
|
|
|
38
42
|
You will now be able to access the brands resource at `http://localhost:3000/admin/brands` in your browser. To reference this route in your code, you can use the `spree.admin_brands_path` helper.
|
|
39
43
|
|
|
40
|
-
## Step 2:
|
|
44
|
+
## Step 2: Add the Description Editor and Logo Upload
|
|
45
|
+
|
|
46
|
+
The generated form only knows about the `name` column. Let's add the rich text editor for `description` and the upload field for `logo` using Spree's [admin form builder](../admin/form-builder.md):
|
|
47
|
+
|
|
48
|
+
```erb app/views/spree/admin/brands/_form.html.erb {4,9-16}
|
|
49
|
+
<div class="card mb-6">
|
|
50
|
+
<div class="card-body">
|
|
51
|
+
<%= f.spree_text_field :name, required: true, autofocus: true %>
|
|
52
|
+
<%= f.spree_rich_text_area :description %>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="card mb-6">
|
|
57
|
+
<div class="card-header">
|
|
58
|
+
<h5 class="card-title"><%= Spree.t(:logo) %></h5>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="card-body">
|
|
61
|
+
<%= f.spree_file_field :logo, width: 300, height: 300 %>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- **`spree_rich_text_area`** renders a WYSIWYG editor (Trix) for the Action Text description.
|
|
67
|
+
- **`spree_file_field`** handles drag-and-drop upload, image preview, and direct upload to storage. Add `crop: true` to enable image cropping with a recommended-size indicator.
|
|
68
|
+
|
|
69
|
+
Then permit the two new attributes in the controller:
|
|
70
|
+
|
|
71
|
+
```ruby app/controllers/spree/admin/brands_controller.rb {7-11}
|
|
72
|
+
module Spree
|
|
73
|
+
module Admin
|
|
74
|
+
class BrandsController < ResourceController
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def permitted_resource_params
|
|
78
|
+
params.require(:brand).permit(
|
|
79
|
+
:name,
|
|
80
|
+
:description,
|
|
81
|
+
:logo
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Open `http://localhost:3000/admin/brands/new`, create a brand with a formatted description and a logo — everything saves through the standard form.
|
|
90
|
+
|
|
91
|
+
## Step 3: Customize the Table Columns
|
|
41
92
|
|
|
42
93
|
The scaffold generator creates a table initializer at `config/initializers/spree_admin_brands_table.rb` with default columns:
|
|
43
94
|
|
|
@@ -71,21 +122,36 @@ Rails.application.config.after_initialize do
|
|
|
71
122
|
end
|
|
72
123
|
```
|
|
73
124
|
|
|
74
|
-
|
|
125
|
+
### Show the logo next to the name
|
|
75
126
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
127
|
+
To render the logo thumbnail in the name column — the same pattern the Products table uses — switch the column to a custom partial:
|
|
128
|
+
|
|
129
|
+
```ruby config/initializers/spree_admin_brands_table.rb
|
|
130
|
+
Spree.admin.tables.brands.add :name,
|
|
131
|
+
label: :name,
|
|
132
|
+
type: :custom,
|
|
133
|
+
partial: 'spree/admin/tables/columns/brand_name',
|
|
134
|
+
sortable: true,
|
|
81
135
|
filterable: true,
|
|
82
136
|
default: true,
|
|
83
|
-
position:
|
|
137
|
+
position: 10
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
And create the partial:
|
|
141
|
+
|
|
142
|
+
```erb app/views/spree/admin/tables/columns/_brand_name.html.erb
|
|
143
|
+
<%# locals: (record:, column:, value:) %>
|
|
144
|
+
<%= link_to spree.edit_admin_brand_path(record), class: 'flex items-center gap-3 no-underline', data: { turbo_frame: '_top' } do %>
|
|
145
|
+
<% if record.logo.attached? %>
|
|
146
|
+
<%= spree_image_tag record.logo, width: 48, height: 48, class: 'rounded' %>
|
|
147
|
+
<% end %>
|
|
148
|
+
<span class="text-gray-900 font-medium"><%= record.name %></span>
|
|
149
|
+
<% end %>
|
|
84
150
|
```
|
|
85
151
|
|
|
86
152
|
> **INFO:** For complete table customization options including column types, filtering, sorting, and bulk actions, see the [Admin Tables](../admin/tables.md) guide.
|
|
87
153
|
|
|
88
|
-
## Step
|
|
154
|
+
## Step 4: Customize Navigation
|
|
89
155
|
|
|
90
156
|
The scaffold generator also creates a navigation initializer at `config/initializers/spree_admin_brands_navigation.rb`:
|
|
91
157
|
|
|
@@ -131,4 +197,8 @@ end
|
|
|
131
197
|
|
|
132
198
|
After restarting your server, you'll see the new "Brands" navigation link in the admin sidebar!
|
|
133
199
|
|
|
134
|
-
> **INFO:** For complete navigation API documentation including all available options, submenu creation, badges, and more, see the [Admin Navigation](../admin/navigation.md) guide.
|
|
200
|
+
> **INFO:** For complete navigation API documentation including all available options, submenu creation, badges, and more, see the [Admin Navigation](../admin/navigation.md) guide. The form helpers used in Step 2 are documented in [Form Builder](../admin/form-builder.md) and [Components](../admin/components.md).
|
|
201
|
+
|
|
202
|
+
## Next Step
|
|
203
|
+
|
|
204
|
+
Brands are manageable in the admin. Now let's connect them to Products: [Extending Core Models](extending-models.md).
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
title:
|
|
3
|
-
description: Expose your Brand model through the Store
|
|
2
|
+
title: API
|
|
3
|
+
description: Expose your custom Brand model through the Spree Store and Admin REST APIs with serializers, controllers, routes, and the spree:api_resource generator.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
In this tutorial, we'll
|
|
6
|
+
In this tutorial, we'll expose our Brand model through Spree's v3 API — the customer-facing **Store API** that storefronts read from, and the back-office **Admin API** with full CRUD for apps and integrations. We'll also extend the existing Product serializer to include brand data.
|
|
7
7
|
|
|
8
8
|
> **INFO:** This guide assumes you've completed the [Model](model.md), [Admin](admin.md), and [Extending Core Models](extending-models.md) tutorials.
|
|
9
9
|
|
|
@@ -11,11 +11,43 @@ In this tutorial, we'll create Store API endpoints for our Brand model so that s
|
|
|
11
11
|
|
|
12
12
|
By the end of this tutorial, you'll have:
|
|
13
13
|
|
|
14
|
-
- `GET /api/v3/store/brands` —
|
|
15
|
-
-
|
|
14
|
+
- `GET /api/v3/store/brands` and `GET /api/v3/store/brands/:id` — customer-facing, read-only, lookup by prefixed ID or slug
|
|
15
|
+
- Full CRUD on `/api/v3/admin/brands` — for back-office apps and integrations
|
|
16
16
|
- Brand data included in Product responses via `?expand=brand`
|
|
17
17
|
- Understanding of how to add new API endpoints and extend existing serializers
|
|
18
18
|
|
|
19
|
+
## The Fast Path: One Generator Command
|
|
20
|
+
|
|
21
|
+
Everything this page builds by hand can be generated in one command with `spree:api_resource`:
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
```bash Spree CLI (Docker)
|
|
25
|
+
spree generate api_resource Brand
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```bash Without Spree CLI
|
|
29
|
+
bin/rails g spree:api_resource Brand
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
Because the Brand model already exists, the generator leaves it (and its migration) untouched and produces only the API surface — no conflict prompts, no overwrites. Your model is "owned once": after creation, domain code belongs to you, and the generator only ever adds API files around it. You'll see this in the output:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
skip model app/models/spree/brand.rb (owned-once; already exists)
|
|
37
|
+
skip migration (model already exists; add a new migration for schema changes)
|
|
38
|
+
create app/controllers/spree/api/v3/store/brands_controller.rb
|
|
39
|
+
create app/controllers/spree/api/v3/admin/brands_controller.rb
|
|
40
|
+
create app/serializers/spree/api/v3/brand_serializer.rb
|
|
41
|
+
create app/serializers/spree/api/v3/admin/brand_serializer.rb
|
|
42
|
+
create spec/factories/spree/brand_factory.rb
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For a brand-new resource you'd pass the attributes too (`spree generate api_resource Brand name:string:uniq`) and get the model and migration in the same run.
|
|
46
|
+
|
|
47
|
+
If you just want a working API, run the generator and skip ahead to [Step 5: Test the Endpoints](#step-5-test-the-endpoints). The rest of this page builds the Store side by hand so you understand what the generator produces and how to customize it — the [Admin API section](#the-admin-api) then shows how little the back-office surface adds on top.
|
|
48
|
+
|
|
49
|
+
> **TIP:** Using an AI agent? The [Spree agent skills](../agentic/agent-skills.md) include a dedicated resource-generator skill — your agent knows the field syntax, the flags, and the generated-file contract.
|
|
50
|
+
|
|
19
51
|
## How the Store API Works
|
|
20
52
|
|
|
21
53
|
Every Store API endpoint follows the same pattern:
|
|
@@ -29,22 +61,28 @@ Every Store API endpoint follows the same pattern:
|
|
|
29
61
|
|
|
30
62
|
Store API requires two things from models:
|
|
31
63
|
|
|
32
|
-
1. **Prefixed IDs** — Stripe-style IDs like `brand_k5nR8xLq` instead of raw database IDs
|
|
64
|
+
1. **Prefixed IDs** — Stripe-style IDs like `brand_k5nR8xLq` instead of raw database IDs. The `spree:model` generator already added `has_prefix_id :brand` in the [Model step](model.md), so this is done.
|
|
33
65
|
2. **Slugs** — human-readable URL identifiers like `nike` for `GET /brands/nike`
|
|
34
66
|
|
|
35
|
-
|
|
67
|
+
Add a `slug` column:
|
|
36
68
|
|
|
37
|
-
|
|
69
|
+
|
|
70
|
+
```bash Spree CLI (Docker)
|
|
71
|
+
spree generate migration AddSlugToSpreeBrands slug:string:uniq
|
|
72
|
+
spree migrate
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```bash Without Spree CLI
|
|
38
76
|
bin/rails g migration AddSlugToSpreeBrands slug:string:uniq
|
|
39
77
|
bin/rails db:migrate
|
|
40
78
|
```
|
|
41
79
|
|
|
42
|
-
Then update the Brand model with `PrefixedId` and `FriendlyId`:
|
|
43
80
|
|
|
44
|
-
|
|
81
|
+
Then add `FriendlyId` to the Brand model:
|
|
82
|
+
|
|
83
|
+
```ruby app/models/spree/brand.rb {3,6}
|
|
45
84
|
module Spree
|
|
46
|
-
class Brand < Spree
|
|
47
|
-
include Spree::PrefixedId
|
|
85
|
+
class Brand < Spree.base_class
|
|
48
86
|
extend FriendlyId
|
|
49
87
|
|
|
50
88
|
has_prefix_id :brand
|
|
@@ -52,10 +90,14 @@ module Spree
|
|
|
52
90
|
|
|
53
91
|
has_many :products, class_name: 'Spree::Product', dependent: :nullify
|
|
54
92
|
|
|
55
|
-
has_one_attached :logo
|
|
56
93
|
has_rich_text :description
|
|
94
|
+
has_one_attached :logo
|
|
57
95
|
|
|
58
96
|
validates :name, presence: true
|
|
97
|
+
|
|
98
|
+
self.whitelisted_ransackable_attributes = %w[name]
|
|
99
|
+
self.whitelisted_ransackable_associations = %w[]
|
|
100
|
+
self.whitelisted_ransackable_scopes = %w[]
|
|
59
101
|
end
|
|
60
102
|
end
|
|
61
103
|
```
|
|
@@ -64,7 +106,7 @@ Now:
|
|
|
64
106
|
- `Spree::Brand.first.prefixed_id` returns `brand_k5nR8xLq`
|
|
65
107
|
- `Spree::Brand.find_by_prefix_id!('brand_k5nR8xLq')` finds by prefixed ID
|
|
66
108
|
- `Spree::Brand.friendly.find('nike')` finds by slug
|
|
67
|
-
- Slugs are auto-generated from the `name` via `slug_candidates` (inherited from
|
|
109
|
+
- Slugs are auto-generated from the `name` via `slug_candidates` (inherited from the Spree base class)
|
|
68
110
|
|
|
69
111
|
## Step 2: Create the Serializer
|
|
70
112
|
|
|
@@ -261,6 +303,67 @@ List response:
|
|
|
261
303
|
}
|
|
262
304
|
```
|
|
263
305
|
|
|
306
|
+
## The Admin API
|
|
307
|
+
|
|
308
|
+
The Admin API is the other half of v3 — same protocol, same serializer/controller patterns, but authenticated with secret keys (`sk_*`) or admin JWTs, and **full CRUD by default**. The `spree:api_resource` generator produces both pieces; here's what they look like:
|
|
309
|
+
|
|
310
|
+
```ruby app/serializers/spree/api/v3/admin/brand_serializer.rb
|
|
311
|
+
module Spree
|
|
312
|
+
module Api
|
|
313
|
+
module V3
|
|
314
|
+
module Admin
|
|
315
|
+
class BrandSerializer < V3::BrandSerializer
|
|
316
|
+
attributes :created_at, :updated_at
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
```ruby app/controllers/spree/api/v3/admin/brands_controller.rb
|
|
325
|
+
module Spree
|
|
326
|
+
module Api
|
|
327
|
+
module V3
|
|
328
|
+
module Admin
|
|
329
|
+
class BrandsController < ResourceController
|
|
330
|
+
protected
|
|
331
|
+
|
|
332
|
+
def model_class
|
|
333
|
+
Spree::Brand
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def serializer_class
|
|
337
|
+
Spree::Api::V3::Admin::BrandSerializer
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def permitted_params
|
|
341
|
+
params.permit(:name)
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Two conventions to notice:
|
|
351
|
+
|
|
352
|
+
- **The Admin serializer extends the Store serializer** — public fields stay in sync automatically, and the Admin side adds back-office data (timestamps here; cost prices, internal notes, and audit fields on richer resources). Customers never see those fields because storefronts use the Store serializer.
|
|
353
|
+
- **`Admin::ResourceController` ships full CRUD** — `index`, `show`, `create`, `update`, and `destroy` are inherited; `permitted_params` lists the writable attributes with flat params (no nested `brand: {...}` wrapping).
|
|
354
|
+
|
|
355
|
+
With the routes registered (`resources :brands` under the `admin` namespace — the generator injects this), back-office clients get:
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
# Create a brand with a secret API key
|
|
359
|
+
curl -X POST -H "X-Spree-API-Key: sk_YOUR_KEY" \
|
|
360
|
+
-H "Content-Type: application/json" \
|
|
361
|
+
-d '{"name": "Adidas"}' \
|
|
362
|
+
http://localhost:3000/api/v3/admin/brands
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
> **INFO:** Secret keys carry scopes (`read_brands`, `write_brands` style) and JWT admin users go through CanCanCan abilities — see [API authentication](../customization/api.md) for the full model.
|
|
366
|
+
|
|
264
367
|
## Step 6: Add Brand to Product Responses
|
|
265
368
|
|
|
266
369
|
Now let's extend the Product serializer so that brand data is included when a storefront requests `?expand=brand`.
|
|
@@ -494,6 +597,7 @@ Spree.ransack.add_association(Spree::Product, :brand)
|
|
|
494
597
|
## Related Documentation
|
|
495
598
|
|
|
496
599
|
- [Extending Core Models](extending-models.md) - Adding the Brand association to Product
|
|
600
|
+
- [Events & Webhooks](events.md) - Lifecycle events for Brand and external-system integration (next step)
|
|
497
601
|
- [Using Brands with the SDK](sdk.md) - Consuming brand endpoints from TypeScript
|
|
498
602
|
- [Decorators](../customization/decorators.md) - Full decorator reference
|
|
499
603
|
- [Dependencies](../customization/dependencies.md) - Swapping services and serializers
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Events & Webhooks
|
|
3
|
+
description: React to Spree store activity and connect to external systems like an OMS, warehouse, or ERP using event subscribers, the subscriber generator, and webhooks.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
> **INFO:** This guide assumes you've completed the [Model](model.md) and [API](api.md) tutorials — we'll reuse the Brand serializer created there.
|
|
7
|
+
|
|
8
|
+
Almost every real store talks to other systems: an order management system (OMS), a warehouse (WMS), an ERP, a CRM, a marketing platform. In Spree, the integration surface is the **events system** — models publish events as things happen, and you react to them without touching core code.
|
|
9
|
+
|
|
10
|
+
There are two ways to consume events, and they serve different audiences:
|
|
11
|
+
|
|
12
|
+
| Mechanism | Code lives | Best for |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| **Subscriber** | In your Spree app | Calling external APIs with your own client code, internal side effects, anything needing app context |
|
|
15
|
+
| **Webhook** | In the external system | Letting a third party receive HTTP callbacks — no Ruby in your app, endpoints managed from the admin |
|
|
16
|
+
|
|
17
|
+
We'll do both: push completed orders to an OMS with a subscriber, give the Brand model its own lifecycle events, and set up an outbound webhook.
|
|
18
|
+
|
|
19
|
+
## Step 1: Subscribe to a Core Event
|
|
20
|
+
|
|
21
|
+
When an order completes, send it to the OMS. Generate a subscriber:
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
```bash Spree CLI (Docker)
|
|
25
|
+
spree generate subscriber OmsOrderSync order.completed
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```bash Without Spree CLI
|
|
29
|
+
bin/rails g spree:subscriber OmsOrderSync order.completed
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
This creates the subscriber, a spec stub, and — crucially — **registers it** in `config/initializers/spree.rb` (injected into the existing `after_initialize` block every Spree app ships with). Subscribers are not auto-discovered; a subscriber that never gets appended to `Spree.subscribers` is a silent no-op, which is why the generator owns that step (re-runs are idempotent, and each new subscriber appends to the same initializer).
|
|
34
|
+
|
|
35
|
+
Fill in the handler:
|
|
36
|
+
|
|
37
|
+
```ruby app/subscribers/oms_order_sync_subscriber.rb
|
|
38
|
+
class OmsOrderSyncSubscriber < Spree::Subscriber
|
|
39
|
+
subscribes_to 'order.completed'
|
|
40
|
+
|
|
41
|
+
def handle(event)
|
|
42
|
+
order = Spree::Order.find_by_prefix_id(event.payload['id'])
|
|
43
|
+
return unless order
|
|
44
|
+
|
|
45
|
+
OmsClient.create_order(
|
|
46
|
+
number: order.number,
|
|
47
|
+
email: order.email,
|
|
48
|
+
line_items: order.line_items.map { |li| { sku: li.sku, quantity: li.quantity } }
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Two things worth understanding:
|
|
55
|
+
|
|
56
|
+
- **Payloads carry prefixed IDs**, not raw database IDs — always look records up with `find_by_prefix_id`. The payload itself is the resource serialized with its v3 API serializer, so `event.payload['number']`, `['email']`, etc. are available directly when you don't need the full record.
|
|
57
|
+
- **Subscribers run async by default** — each `handle` call is an ActiveJob on the events queue, so a slow OMS API never blocks checkout. Pass `subscribes_to 'order.completed', async: false` only when you genuinely need synchronous execution.
|
|
58
|
+
|
|
59
|
+
Restart the server, complete a test order, and watch the job fire (`spree logs worker` — or your job backend's UI).
|
|
60
|
+
|
|
61
|
+
## Step 2: Give Brand Its Own Lifecycle Events
|
|
62
|
+
|
|
63
|
+
Core models like Payment and Shipment publish `*.created` / `*.updated` / `*.deleted` events automatically. Your models can too — add one line to the Brand model:
|
|
64
|
+
|
|
65
|
+
```ruby app/models/spree/brand.rb {3}
|
|
66
|
+
module Spree
|
|
67
|
+
class Brand < Spree.base_class
|
|
68
|
+
publishes_lifecycle_events
|
|
69
|
+
|
|
70
|
+
# ... existing code ...
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Now `brand.created`, `brand.updated`, and `brand.deleted` fire after the matching transactions commit — and because you created `Spree::Api::V3::BrandSerializer` in the [API step](api.md), the payloads automatically use it (the events system resolves the serializer by naming convention). Anyone — subscriber or webhook — can react to brand changes with the same JSON shape your API serves.
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
```bash Spree CLI (Docker)
|
|
79
|
+
spree generate subscriber BrandSync brand.created brand.updated
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```bash Without Spree CLI
|
|
83
|
+
bin/rails g spree:subscriber BrandSync brand.created brand.updated
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
```ruby app/subscribers/brand_sync_subscriber.rb
|
|
88
|
+
class BrandSyncSubscriber < Spree::Subscriber
|
|
89
|
+
subscribes_to 'brand.created', 'brand.updated'
|
|
90
|
+
|
|
91
|
+
def handle(event)
|
|
92
|
+
SearchIndexer.upsert_brand(event.payload)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Step 3: Publish a Custom Event
|
|
98
|
+
|
|
99
|
+
Lifecycle events cover persistence; custom events express *domain* moments. Say featuring a brand should notify the marketing platform:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
brand.publish_event('brand.featured')
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The payload defaults to the serializer output; pass your own hash as the second argument when the event needs different data. Subscribers consume it like any other event name.
|
|
106
|
+
|
|
107
|
+
## Step 4: Outbound Webhooks — No Code Required
|
|
108
|
+
|
|
109
|
+
When the consumer is an external system you don't deploy code into, use webhooks. In the admin, go to **Settings → Webhooks**, add an endpoint with the destination URL, and pick the events to deliver — `order.completed`, `brand.created`, anything publishing in your store. The endpoint's **signing secret is shown once on creation** — store it in the receiving system.
|
|
110
|
+
|
|
111
|
+
Each delivery is an HTTP POST with this envelope:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
116
|
+
"name": "order.completed",
|
|
117
|
+
"created_at": "2026-06-11T12:00:00Z",
|
|
118
|
+
"data": { "id": "or_m3Rp9wXz", "number": "R123456789", "...": "..." },
|
|
119
|
+
"metadata": {}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
And three headers the receiver should use:
|
|
124
|
+
|
|
125
|
+
| Header | Contents |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `X-Spree-Webhook-Event` | The event name |
|
|
128
|
+
| `X-Spree-Webhook-Timestamp` | Unix timestamp of the delivery |
|
|
129
|
+
| `X-Spree-Webhook-Signature` | `HMAC-SHA256(secret, "{timestamp}.{body}")` |
|
|
130
|
+
|
|
131
|
+
Verify the signature before trusting a payload:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
def verified?(request, secret)
|
|
135
|
+
timestamp = request.headers['X-Spree-Webhook-Timestamp']
|
|
136
|
+
expected = OpenSSL::HMAC.hexdigest('SHA256', secret, "#{timestamp}.#{request.raw_post}")
|
|
137
|
+
ActiveSupport::SecurityUtils.secure_compare(expected, request.headers['X-Spree-Webhook-Signature'])
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Delivery semantics to design around: failed deliveries (timeouts, connection errors, non-2xx responses) are **recorded, not retried automatically** — redeliver from the endpoint's delivery history in the admin, or via `POST /api/v3/admin/webhook_endpoints/:webhook_endpoint_id/deliveries/:id/redeliver`. After **15 consecutive failures** the endpoint auto-disables and store staff get an email; a successful delivery resets the counter.
|
|
142
|
+
|
|
143
|
+
## Testing a Subscriber
|
|
144
|
+
|
|
145
|
+
Subscribers are plain Ruby — test `handle` directly with a constructed event:
|
|
146
|
+
|
|
147
|
+
```ruby spec/subscribers/oms_order_sync_subscriber_spec.rb
|
|
148
|
+
require 'rails_helper'
|
|
149
|
+
|
|
150
|
+
RSpec.describe OmsOrderSyncSubscriber do
|
|
151
|
+
it 'pushes completed orders to the OMS' do
|
|
152
|
+
order = create(:completed_order_with_totals)
|
|
153
|
+
event = Spree::Event.new(name: 'order.completed', payload: { 'id' => order.prefixed_id })
|
|
154
|
+
|
|
155
|
+
expect(OmsClient).to receive(:create_order).with(hash_including(number: order.number))
|
|
156
|
+
|
|
157
|
+
described_class.new.handle(event)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Related Documentation
|
|
163
|
+
|
|
164
|
+
- [Events](../core-concepts/events.md) — the full event catalog, subscriber DSL, and delivery guarantees
|
|
165
|
+
- [Webhooks](../core-concepts/webhooks.md) — endpoint management, security, and the per-event payload schemas
|
|
166
|
+
- [Extending Core Models](extending-models.md) — when a decorator is the right tool instead
|
|
@@ -5,7 +5,7 @@ description: Tutorial — extend Spree's core models with a custom Brand resourc
|
|
|
5
5
|
|
|
6
6
|
In this tutorial, we'll connect our custom Brand model with Spree's core Product model. This is a common pattern when building features that need to integrate with existing Spree functionality.
|
|
7
7
|
|
|
8
|
-
> **INFO:** This guide assumes you've completed the [Model](model.md)
|
|
8
|
+
> **INFO:** This guide assumes you've completed the [Model](model.md) and [Admin](admin.md) tutorials.
|
|
9
9
|
|
|
10
10
|
## What We're Building
|
|
11
11
|
|
|
@@ -77,10 +77,16 @@ The key line is `Product.prepend(ProductDecorator)` - this inserts your module a
|
|
|
77
77
|
|
|
78
78
|
First, add a `brand_id` column to the products table:
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
|
|
81
|
+
```bash Spree CLI (Docker)
|
|
82
|
+
spree generate migration AddBrandIdToSpreeProducts brand_id:integer:index
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```bash Without Spree CLI
|
|
81
86
|
bin/rails g migration AddBrandIdToSpreeProducts brand_id:integer:index
|
|
82
87
|
```
|
|
83
88
|
|
|
89
|
+
|
|
84
90
|
Edit the migration to add an index (but no foreign key constraint, keeping it optional):
|
|
85
91
|
|
|
86
92
|
```ruby db/migrate/XXXXXXXXXXXXXX_add_brand_id_to_spree_products.rb
|
|
@@ -94,20 +100,32 @@ end
|
|
|
94
100
|
|
|
95
101
|
Run the migration:
|
|
96
102
|
|
|
97
|
-
|
|
103
|
+
|
|
104
|
+
```bash Spree CLI (Docker)
|
|
105
|
+
spree migrate
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```bash Without Spree CLI
|
|
98
109
|
bin/rails db:migrate
|
|
99
110
|
```
|
|
100
111
|
|
|
112
|
+
|
|
101
113
|
> **INFO:** We intentionally don't add a foreign key constraint. This keeps the association optional and avoids issues if brands are deleted. Spree follows this pattern for flexibility.
|
|
102
114
|
|
|
103
115
|
## Step 2: Generate the Product Decorator
|
|
104
116
|
|
|
105
117
|
Spree provides a generator to create decorator files with the correct structure:
|
|
106
118
|
|
|
107
|
-
|
|
119
|
+
|
|
120
|
+
```bash Spree CLI (Docker)
|
|
121
|
+
spree generate model_decorator Spree::Product
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```bash Without Spree CLI
|
|
108
125
|
bin/rails g spree:model_decorator Spree::Product
|
|
109
126
|
```
|
|
110
127
|
|
|
128
|
+
|
|
111
129
|
This creates `app/models/spree/product_decorator.rb`:
|
|
112
130
|
|
|
113
131
|
```ruby app/models/spree/product_decorator.rb
|
|
@@ -281,22 +299,7 @@ def normalize_name
|
|
|
281
299
|
end
|
|
282
300
|
```
|
|
283
301
|
|
|
284
|
-
**Events approach** (recommended for side effects):
|
|
285
|
-
|
|
286
|
-
```ruby app/subscribers/my_app/product_sync_subscriber.rb
|
|
287
|
-
module MyApp
|
|
288
|
-
class ProductSyncSubscriber < Spree::Subscriber
|
|
289
|
-
subscribes_to 'product.updated'
|
|
290
|
-
|
|
291
|
-
def handle(event)
|
|
292
|
-
product = Spree::Product.find_by_prefix_id(event.payload['id'])
|
|
293
|
-
return unless product
|
|
294
|
-
|
|
295
|
-
ExternalSyncService.sync(product)
|
|
296
|
-
end
|
|
297
|
-
end
|
|
298
|
-
end
|
|
299
|
-
```
|
|
302
|
+
**Events approach** (recommended for side effects): subscribe to `product.updated` and react in a `Spree::Subscriber` — no decorator, no callback coupling. The [Events & Webhooks tutorial](events.md) builds this out fully, including external-system sync (OMS, warehouse) and outbound webhooks.
|
|
300
303
|
|
|
301
304
|
#### Adding Class Methods
|
|
302
305
|
|
|
@@ -373,13 +376,14 @@ Spree::PermittedAttributes.product_attributes << :brand_id
|
|
|
373
376
|
|
|
374
377
|
Now that Brands are connected to Products, let's expose them through the Store API:
|
|
375
378
|
|
|
376
|
-
- [6.
|
|
379
|
+
- [6. API](api.md) — Create API endpoints for brands and extend the Product API response
|
|
377
380
|
|
|
378
381
|
## Related Documentation
|
|
379
382
|
|
|
380
383
|
- [Model Tutorial](model.md) - Creating the Brand model
|
|
381
384
|
- [Admin Tutorial](admin.md) - Building the admin interface
|
|
382
|
-
- [
|
|
385
|
+
- [API Tutorial](api.md) - Exposing brands through the API
|
|
386
|
+
- [Events & Webhooks tutorial](events.md) - Subscribers, custom events, and webhooks in the Brand storyline
|
|
383
387
|
- [Events](../core-concepts/events.md) - Subscribe to model changes without decorators
|
|
384
388
|
- [Webhooks](../core-concepts/webhooks.md) - HTTP callbacks for external integrations
|
|
385
389
|
- [Decorators](../customization/decorators.md) - Full decorator reference
|