spree/agent-skills

spree-decorators

Use when the user wants to extend a Spree model, controller, helper, or service class without forking — add an association to Spree::Product, add a method to Spree::Order, override a validation, add a scope, prepend a beforeaction, hook into create.

Ver código fuente
Documento original del Skill

Contenido del repositorio de origen con títulos, ejemplos, código, tablas, enlaces e imágenes preservados.

Spree Decorators

Commands below use the Spree CLI form (spree …, Docker). On a classic Rails app without the CLI (typical pre-5.4), use the native mapping in the spree-project skill — bin/rails / bundle exec rake from the app root, paths without the backend/ prefix.

Decorators let you change existing Spree classes (models, controllers, helpers, services) from your own app without modifying gem source. They're the standard Ruby Module#prepend pattern with a Spree filename convention and a generator.

Read the warning first

Decorators tightly couple your code to Spree internals. They will probably survive a minor upgrade and might survive a major one. The Spree docs are explicit: decorators are for structural changes (add an association, validation, scope, new method). For behavioral changes (callbacks, side effects, post-save sync), use a modern alternative instead.

Pick the right tool

Use caseUse this instead of a decorator
React to a save / create / update / deleteEvents subscriber — see the spree-events-webhooks skill
Notify an external service when something changesWebhook or events subscriber
Swap how a service computes (cart add, tax, search, checkout)Spree.dependencies — see the spree-dependencies skill
Replace a serializer or abilitySpree.dependencies
Add an admin menu itemAdmin navigation API — see the spree-admin skill
Add a section to an admin formAdmin partial injection / slot — see the spree-admin skill
Add a searchable/filterable fieldSpree.ransack.add_attribute(Spree::Product, :brand_id) in an initializer (also add_association / add_scope) — no decorator needed
Add an association, validation, scope, or new methodDecorator (this skill)

If your job is "react to product update by syncing to an ERP," write a subscriber on product.updated, not a after_save callback in a decorator. The decorator path will break the next time Spree changes how Product saves.

The pattern in three lines

A decorator is just a Ruby module prepended to an existing Spree class. The file lives in your app at the same path Spree uses, with _decorator appended.

ruby
# app/models/spree/product_decorator.rb
module Spree
  module ProductDecorator
    # methods, prepended hook, etc.
  end

  Product.prepend(ProductDecorator)
end

Host-app decorators are loaded by an explicit glob, not by plain autoloading: spree-starter (and the spree:install generator) put a config.to_prepare block in config/application.rb that loads every app/**/*_decorator*.rb file, so the prepend line runs at boot and again after every code reload in development. If your app has neither (check config/application.rb for the block), add it — Zeitwerk alone will never load an unreferenced decorator module in development, and your decorators will silently not apply. Once loaded, your module enters the method-lookup chain ahead of Spree::Product's own definitions: your methods are found first and can super to call the original.

Generate the file

Spree ships two generators — one for models, one for controllers. Use them — they produce the exact filenames, modules, and prepend lines the autoloader expects.

Models

bash
spree generate model_decorator Spree::Product
# or, without the @spree/cli wrapper:
bin/rails g spree:model_decorator Spree::Product

The CLI auto-prefixes spree: for Spree generators (spree g model_decorator ... also works as a shorthand).

Output at app/models/spree/product_decorator.rb:

ruby
module Spree
  module ProductDecorator
    def self.prepended(base)
      # base.belongs_to :brand
    end

    # add custom methods here
  end
end

Spree::Product.prepend Spree::ProductDecorator

The argument accepts either Spree::Product or Product — the generator strips the prefix. Works for singular model names under Spree:: (the vast majority), including nested ones. But the generator runs the name through classify, which singularizes the last segment — for plural-named classes (e.g. Spree::Exports::Products, Spree::Promotion::Actions::CreateItemAdjustments) it emits a prepend against a non-existent singular constant and the file raises NameError on load. Write those decorators by hand instead.

Controllers

bash
spree generate controller_decorator Spree::Admin::ProductsController
# or, without the @spree/cli wrapper:
bin/rails g spree:controller_decorator Spree::Admin::ProductsController

Output at app/controllers/spree/admin/products_controller_decorator.rb:

ruby
module Spree::Admin
  module ProductsControllerDecorator
    def self.prepended(base)
      # base.before_action :my_filter
    end

    # add custom methods here
  end
end

Spree::Admin::ProductsController.prepend Spree::Admin::ProductsControllerDecorator

The generator handles arbitrary namespace depth:

  • Spree::ProductsControllerapp/controllers/spree/products_controller_decorator.rb
  • Spree::Admin::ProductsControllerapp/controllers/spree/admin/products_controller_decorator.rb
  • Spree::Api::V3::Store::ProductsControllerapp/controllers/spree/api/v3/store/products_controller_decorator.rb

The final .prepend line is always fully qualified — no surprises about which constant is being decorated.

Model decorator patterns

Add an association

Run the migration first (no foreign key constraint — keep it Spree-style):

bash
bin/rails g migration AddBrandIdToSpreeProducts brand_id:bigint:index
ruby
class AddBrandIdToSpreeProducts < ActiveRecord::Migration[7.2]
  def change
    add_column :spree_products, :brand_id, :bigint
    add_index :spree_products, :brand_id
  end
end

Then the decorator:

ruby
# app/models/spree/product_decorator.rb
module Spree
  module ProductDecorator
    def self.prepended(base)
      base.belongs_to :brand, class_name: 'Spree::Brand', optional: true
      base.has_many :videos, class_name: 'Spree::Video', dependent: :destroy
    end
  end

  Product.prepend(ProductDecorator)
end

Class-level additions (associations, validations, scopes, callbacks, extends) always go inside `self.prepended(base)` and are called on base. Instance methods go at module level.

Add a validation

ruby
module Spree
  module ProductDecorator
    def self.prepended(base)
      base.validates :external_id, presence: true, uniqueness: true
      base.validates :weight, numericality: { greater_than: 0 }, allow_nil: true
    end
  end

  Product.prepend(ProductDecorator)
end

Add a scope

ruby
module Spree
  module ProductDecorator
    def self.prepended(base)
      base.scope :featured, -> { where("public_metadata->>'featured' = ?", 'true') }
      base.scope :recently_added, -> { where('created_at > ?', 30.days.ago) }
    end
  end

  Product.prepend(ProductDecorator)
end

If you want this scope queryable from the API, also allowlist it via Ransack — see the Ransack note at the bottom.

Note the SQL string names the real jsonb column: public_metadata or private_metadata. In Ruby, metadata is an alias method for private_metadata — but there is no metadata column, so where("metadata->>…") raises PG::UndefinedColumn. (For anything the storefront filters on, a real boolean column beats metadata anyway.)

Add a new instance method

ruby
module Spree
  module ProductDecorator
    def featured?
      metadata[:featured] == true
    end

    def days_until_available
      return 0 if available_on.nil? || available_on <= Time.current
      (available_on.to_date - Date.current).to_i
    end
  end

  Product.prepend(ProductDecorator)
end

Override an existing method (call super)

ruby
module Spree
  module ProductDecorator
    def available?
      return false if discontinued?
      super
    end
  end

  Product.prepend(ProductDecorator)
end

Always consider whether you need `super`. Omitting it replaces the original method entirely — which can silently break behavior the rest of Spree assumes is there.

Add class methods

Use extend from inside prepended:

ruby
module Spree
  module ProductDecorator
    def self.prepended(base)
      base.extend ClassMethods
    end

    module ClassMethods
      def search_by_name(query)
        where('LOWER(name) LIKE ?', "%#{query.downcase}%")
      end
    end
  end

  Product.prepend(ProductDecorator)
end

Usage: Spree::Product.search_by_name('shirt').

Make a new attribute available via Ransack

If you added an association or column and want it queryable from the API (?q[brand_id_eq]=...), allowlist it. The preferred, no-decorator way is Spree.ransack from an initializer:

ruby
# config/initializers/spree.rb
Spree.ransack.add_attribute(Spree::Product, :brand_id)
Spree.ransack.add_attribute(Spree::Product, :external_id)
Spree.ransack.add_association(Spree::Product, :brand)
Spree.ransack.add_scope(Spree::Product, :featured)

If you're already inside a decorator (e.g. you just added the association there), appending to the model allowlists works too:

ruby
module Spree
  module ProductDecorator
    def self.prepended(base)
      base.whitelisted_ransackable_attributes += %w[brand_id external_id]
      base.whitelisted_ransackable_associations += %w[brand videos]
    end
  end

  Product.prepend(ProductDecorator)
end

Without this, filter params on the attribute are silently dropped — the API returns 200 as if that filter were never sent, not an error. See the spree-api-v3 skill for the full Ransack story.

Permit a new attribute on writes

If the new attribute should be settable via the admin or the API, register it in the permitted-attributes list:

ruby
# config/initializers/spree.rb
Rails.application.config.after_initialize do
  Spree::PermittedAttributes.product_attributes << :brand_id
end

This makes the attribute writable in the Rails admin, which builds its strong params from this list. It does not automatically reach every API v3 endpoint: the v3 base ResourceController defaults permitted_params to the matching Spree::PermittedAttributes list, but controllers that enumerate their own params.permit(...) — including Spree::Api::V3::Admin::ProductsController — ignore the global list. To accept the attribute on those endpoints, decorate the controller's permitted_params (or contribute the attribute upstream).

Controller decorator patterns

First check whether you can avoid this. A new controller that inherits from a Spree base class is more upgrade-safe than a decorator on an existing controller. Controller decorators that override existing actions are the most fragile decorator type — they couple to instance variables and method signatures that can change between Spree minor releases.

Add a before_action

Note: Spree no longer ships a Rails storefront — Spree::CheckoutController / Spree::ProductsController only exist in legacy apps using the separate storefront gem. Modern storefronts are headless via the Store API, so the controllers you'll decorate are the admin and API ones:

ruby
# app/controllers/spree/admin/products_controller_decorator.rb
module Spree::Admin
  module ProductsControllerDecorator
    def self.prepended(base)
      base.before_action :check_editable, only: [:update]
    end

    private

    def check_editable
      if params[:id].blank?
        flash[:error] = Spree.t(:not_found)
        redirect_to spree.admin_products_path
      end
    end
  end

  ProductsController.prepend(ProductsControllerDecorator)
end

Use bin/rails g spree:controller_decorator Spree::Admin::ProductsController (or Spree::Api::V3::Store::CartsController for API controllers) to scaffold the correct file path and prepend line — the generator handles arbitrary namespace depth. Note: API controller decorators must render JSON errors (e.g. render json: { error: ... }, status: :unprocessable_entity), not flash/redirect.

Add a new action

ruby
# app/controllers/spree/admin/products_controller_decorator.rb
module Spree::Admin
  module ProductsControllerDecorator
    def self.prepended(base)
      base.before_action :load_product, only: [:quick_view]
    end

    def quick_view
      respond_to do |format|
        format.html { render partial: 'quick_view', locals: { product: @product } }
        format.json { render json: @product }
      end
    end

    private

    def load_product
      @product = current_store.products.friendly.find(params[:id])
    end
  end

  ProductsController.prepend(ProductsControllerDecorator)
end

And the route — Spree controllers live in the engine, so the route must be added to the engine, not your app. Admin routes are drawn through the core engine inside the admin namespace:

ruby
# config/routes.rb
Spree::Core::Engine.add_routes do
  namespace :admin, path: Spree.admin_path do
    get 'products/:id/quick_view', to: 'products#quick_view', as: :product_quick_view
  end
end

Modifying an existing action

The most fragile decorator pattern. If you must:

ruby
module Spree
  module Admin
    module ProductsControllerDecorator
      def create
        log_product_creation_attempt
        super
        notify_team_of_new_product if @product.persisted?
      end

      private

      def log_product_creation_attempt
        Rails.logger.info "Product creation attempted by #{try_spree_current_user&.email}"
      end

      def notify_team_of_new_product
        ProductNotificationJob.perform_later(@product)
      end
    end

    ProductsController.prepend(ProductsControllerDecorator)
  end
end

The example above is also a case where the better answer is a subscriber on `product.created` — same outcome, no coupling to controller internals.

Common pitfalls

Forgot to call super

ruby
# ❌ Replaces all of Spree's availability logic — easy to silently break
def available?
  in_stock? && active?
end

# ✅ Extends, doesn't replace
def available?
  super && custom_availability_check
end

Instance variables in prepended

ruby
# ❌ Doesn't do what it looks like — @custom_setting lives on the decorator module, not on instances
def self.prepended(base)
  @custom_setting = true
end

# ✅ Use class_attribute when you want a setting on instances
def self.prepended(base)
  base.class_attribute :custom_setting, default: true
end

Circular dependencies via constant references

When decorators reference each other (or other Spree models that haven't been loaded yet), constant lookups can fail at boot. Use string class names for association class_name: arguments:

ruby
# ❌ Variant might not be loaded yet at decorator boot
base.has_many :variants

# ✅ String form — resolved lazily
base.has_many :variants, class_name: 'Spree::Variant'

File path / module name mismatch

The autoloader is strict about names. Spree::ProductDecorator MUST live at app/models/spree/product_decorator.rb. The generator gets this right; if you hand-write the file, match it exactly.

Organizing multiple decorators

If you have many customizations on Spree::Product, splitting into focused modules is fine — group by concern:

app/models/spree/
├── product_decorator.rb           # Main file, prepends the others
├── product/
│   ├── brand_decorator.rb         # Brand association
│   ├── inventory_decorator.rb     # Inventory customizations
│   └── seo_decorator.rb           # SEO methods
ruby
# app/models/spree/product_decorator.rb
require_dependency 'spree/product/brand_decorator'
require_dependency 'spree/product/inventory_decorator'
require_dependency 'spree/product/seo_decorator'

This is purely organizational — each child file uses the same prepend pattern, just on smaller modules.

Migrating from decorators to modern patterns

If you inherited a decorator that uses after_save for side effects, migrate it to an Events subscriber. Same outcome, no coupling to model internals, won't break when Spree changes how Spree::Product saves.

Before:

ruby
# app/models/spree/product_decorator.rb
module Spree
  module ProductDecorator
    def self.prepended(base)
      base.after_save :sync_to_external_service
    end

    private

    def sync_to_external_service
      ExternalSyncJob.perform_later(self) if saved_change_to_name?
    end
  end

  Product.prepend(ProductDecorator)
end

After:

ruby
# app/subscribers/product_sync_subscriber.rb
class ProductSyncSubscriber < Spree::Subscriber
  subscribes_to 'product.updated'

  def handle(event)
    product = Spree::Product.find_by_prefix_id(event.payload['id'])
    return unless product

    ExternalSyncJob.perform_later(product)
  end
end

Subscribers are not auto-discovered from app/subscribers/ — only classes in the Spree.subscribers array get wired to the event registry (Spree's engines add their built-in subscribers there; your app must add its own). Register yours in an initializer:

ruby
# config/initializers/spree.rb
Rails.application.config.after_initialize do
  Spree.subscribers << ProductSyncSubscriber
end

Async by default. Testable in isolation. See the spree-events-webhooks skill for the full event catalog and the subscriber API.

When NOT to use a decorator

  • You want a whole new model + API endpoint → use the spree:api_resource generator. See the spree-resource skill.
  • You want to swap how a service computes → use Spree.dependencies. See the spree-dependencies skill.
  • You want to react to a Spree event → write a subscriber. See the spree-events-webhooks skill.
  • You want to customize the admin UI → use the admin partial / slot system. See the spree-admin skill.
  • You want a custom payment gateway → subclass Spree::PaymentMethod and register it with Spree.payment_methods << MyGateway. See the spree-payments skill.
  • You want to override admin tables or navigation → use the admin extension APIs (Spree.admin.tables, the navigation registry). See the spree-admin skill.

The decorator is the last resort for structural changes the modern APIs don't cover. When in doubt, check the table at the top of this skill — there's a high chance the modern alternative exists.

Where to read further

  • Decorator docs: node_modules/@spree/docs/dist/developer/customization/decorators.md (also at https://spreecommerce.org/docs/developer/customization/decorators)
  • Extending models tutorial: node_modules/@spree/docs/dist/developer/tutorial/extending-models.md — the canonical brand-on-product walkthrough
  • Events (for behavioral customizations): the spree-events-webhooks skill
  • Dependencies (for swappable services): the spree-dependencies skill
  • API resource generator (for whole new models): the spree-resource skill
del mismo repositorio

Más Skills

Todos los Skills
spree
Comunidad

spree-deployment

Use when the user is deploying a Spree application to production — Heroku, Render, Fly, AWS, K8s, plain Docker. Covers required environment variables, Sidekiq setup, the release-phase command (rake spree:upgrade), ActiveStorage backend config, asset precompile, the docker-entrypoint behavior. Common phrasings include "deploy Spree", "Heroku deploy", "Render deploy", "Spree on Kubernetes", "Spree environment variables", "Spree release command", "Procfile", "Sidekiq deployment", "S3 setup", "Cloudflare R2", "asset precompile failed". Spree-specific bits only — generic Rails deployment is out of scope.

instalaciones
1
GitHub Stars
8
Actualizado
6 jul
spree
Comunidad

spree-extensions

Use when the user wants to install a specific third-party Spree gem (Stripe, Adyen, PayPal, i18n, search, social login, etc.), build their own Spree extension to share across apps, or swap a core Spree service via Spree.dependencies. Common phrasings include "add Stripe", "install spreeX", "what payment gateways", "create a Spree extension", "build a gem for Spree", "spreedevtools", "Spree.dependencies", "service swap". For deciding which customization pattern to use, see the spree-customization skill first — extensions are the last two rows of its decision tree and most single-app work doesn't need one.

instalaciones
1
GitHub Stars
8
Actualizado
6 jul
spree
Comunidad

spree-performance

Use when the user is investigating or improving Spree performance — slow product listings, slow cart updates, search latency, image processing bottlenecks, Sidekiq queue tuning, N+1 in admin pages, cache invalidation strategies. Common phrasings include "slow PDP", "slow cart", "N+1 queries in Spree", "Sidekiq queue backlog", "search slow", "Meilisearch tuning", "image processing slow", "Spree cache". Provides the Spree-specific performance hotspots and the tools to address them.

instalaciones
1
GitHub Stars
8
Actualizado
6 jul
spree
Comunidad

spree-pricing

Use when the user is working with Spree pricing — variant prices, currency-specific pricing, sale/compare-at prices, price lists (5.3), price rules, EU Omnibus compliance (PriceHistory + priorprice), tax-inclusive pricing. Common phrasings include "set price", "compare at price", "sale price", "price list", "multi-currency pricing", "prior price", "Omnibus", "EU pricing law", "lowest price in 30 days", "tax inclusive", "VAT". Provides the price graph and the regulatory bits.

instalaciones
1
GitHub Stars
8
Actualizado
6 jul