@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.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: "Installing Spree"
3
3
  sidebarTitle: Installation
4
- description: "Follow the instructions below to learn how to build and deploy your Spree application."
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 using the SDK's built-in request method
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 [Store API](../tutorial/store-api.md) and [SDK](../tutorial/sdk.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/api.md) and [SDK](../tutorial/sdk.md) tutorials.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Quickstart
3
- description: Install and start using the Spree SDK in your project
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: Learn how to create Admin Dashboard UI to manage Brands
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 a complete "Brand" model, let's create an Admin Dashboard interface so our admins can manage the brands.
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: Create the boilerplate Admin Dashboard UI
8
+ ## Step 1: Scaffold the Admin UI
9
9
 
10
- Admin Scaffold generator is a tool that helps us create a complete Admin Dashboard UI for our new resource.
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 new tables system. |
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: Customize the Table Columns
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
- You can customize this file to add more columns based on your model's attributes. For example, to add a description column:
125
+ ### Show the logo next to the name
75
126
 
76
- ```ruby
77
- Spree.admin.tables.brands.add :description,
78
- label: :description,
79
- type: :string,
80
- sortable: false,
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: 15
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 3: Customize Navigation
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: Store API
3
- description: Expose your Brand model through the Store API with serializers, controllers, and routes
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 create Store API endpoints for our Brand model so that storefronts can list and display brands. We'll also extend the existing Product serializer to include brand data.
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` — list all brands
15
- - `GET /api/v3/store/brands/:id` — get a single brand by prefixed ID or slug
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
- First, add a `slug` column if you haven't already:
67
+ Add a `slug` column:
36
68
 
37
- ```bash
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
- ```ruby app/models/spree/brand.rb {3-7}
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::Base
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 `Spree::Base`)
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), [Admin](admin.md), and [File Uploads](file-uploads.md) tutorials.
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
- ```bash
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
- ```bash
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
- ```bash
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. Store API](store-api.md) — Create API endpoints for brands and extend the Product API response
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
- - [Store API Tutorial](store-api.md) - Exposing brands through the API
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