dist/codex/spree-commerce/skills/spree-extensions/SKILL.md
Build Spree extensions as Rails engines — gem scaffolding, `bin/rails g spree:extension`, mounting routes/migrations/assets, the modern `prepend` decorator pattern (`*_decorator.rb` with `self.prepended(base)`), generators (`spree:model_decorator`, `spree:controller_decorator`), the four customization surfaces in preference order (Events > Webhooks > Dependencies > Decorators), Spree::Dependencies for swapping service objects, gem release/versioning, and the deprecated Deface engine. Use when building a reusable Spree extension or adding non-trivial customization to an app.
npx skillsauth add orcaqubits/agentic-commerce-claude-plugins spree-extensionsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Fetch live docs:
prepend pattern.spree_stripe, spree_klaviyo) for current engine scaffolding.Spree::Dependencies registry by reading lib/spree/dependencies.rb in the live gem.A Spree extension is a Rails engine packaged as a gem. It can ship:
Spree::Dependencies and event busUsed the same way as spree_stripe, spree_klaviyo, spree_paypal_checkout.
Spree::Event publications via Spree::Subscriber or external HTTP receivers. Stable across upgrades.Spree::Dependencies.foo_service = MyService. Spree publishes a well-defined registry of replaceable services.Spree.admin.navigation API + partial injection slots. No code modification.prepend to extend a core class. Most fragile; ties you to internals.Reach for the highest-numbered tool only when lower-numbered tools can't express what you need.
gem install spree_cmd # or use the bundled generator
bin/rails g spree:extension spree_my_extension
(Verify the current generator name and flags — spree_cmd may have been replaced by the in-spree generator suite added in v5.2.)
Produces:
spree_my_extension/
├── app/
│ ├── controllers/
│ ├── models/
│ ├── views/
│ ├── subscribers/ # event subscribers
│ ├── decorators/ # _decorator.rb files
│ └── assets/
├── config/
│ ├── routes.rb
│ ├── locales/
│ └── initializers/spree_my_extension.rb
├── db/migrate/ # migrations
├── lib/
│ ├── spree_my_extension/
│ │ ├── engine.rb
│ │ └── version.rb
│ └── spree_my_extension.rb
├── spec/ # RSpec test suite
├── spree_my_extension.gemspec
└── Gemfile
# lib/spree_my_extension/engine.rb
module SpreeMyExtension
class Engine < ::Rails::Engine
require 'spree/core'
isolate_namespace SpreeMyExtension if defined?(SpreeMyExtension)
engine_name 'spree_my_extension'
initializer 'spree_my_extension.environment', before: :load_config_initializers do |app|
# Boot-time wiring (registries, dependencies)
end
def self.activate
cache_klasses = "#{config.root}/app/**/*_decorator*.rb"
Dir.glob(cache_klasses) do |c|
Rails.configuration.cache_classes ? require(c) : load(c)
end
end
config.to_prepare(&method(:activate).to_proc)
end
end
The to_prepare hook ensures decorators reload in development.
Modern Spree decorators use Module#prepend, not class_eval reopening. File naming convention: app/models/spree/product_decorator.rb (suffix _decorator.rb).
# app/models/spree/product_decorator.rb
module SpreeMyExtension::ProductDecorator
def self.prepended(base)
base.has_many :promotional_videos, class_name: 'SpreeMyExtension::PromotionalVideo'
base.validates :seo_title, length: { maximum: 70 }, allow_nil: true
base.scope :featured, -> { where(featured: true) }
end
def display_name
seo_title.presence || super # `super` walks up the prepend chain
end
end
Spree::Product.prepend(SpreeMyExtension::ProductDecorator) unless Spree::Product.include?(SpreeMyExtension::ProductDecorator)
Why prepend not include: prepend inserts the module above the class in the method lookup chain — super walks back to the original implementation. This is what allows overriding methods.
bin/rails g spree:model_decorator Spree::Product
bin/rails g spree:controller_decorator Spree::Admin::ProductsController
Generates the boilerplate file with the right naming convention.
# config/initializers/spree.rb
Spree::Dependencies.cart_add_item_service = MyApp::CartAddItemService
Spree::Dependencies.shipping_rate_estimator = MyApp::CustomEstimator
Spree::Dependencies.order_updater_class = MyApp::OrderUpdater
Common replaceable services (verify against lib/spree/dependencies.rb):
cart_add_item_service, cart_remove_item_service, cart_update_serviceorder_updater_class, order_recalculator, order_cancellershipping_rate_estimator, stock_splitter, package_factorytax_calculatorpayment_create_service, payment_capture_servicepricing_options_factoryAlways implement the same public contract as the service you replace.
| Need | Approach |
|------|----------|
| Add a column to Product | Migration (or Metafield, no decorator) |
| Add a validation to Product | Decorator |
| Add a method to Product | Decorator |
| Change cart-add behavior globally | Spree::Dependencies.cart_add_item_service = ... |
| React to an order completing | Subscriber |
| Change how shipping rates are computed | Replace shipping_rate_estimator |
| Add an admin menu item | Spree.admin.navigation.register(...) |
| Override an admin view | Partial injection slots; avoid Deface in v5 |
Deface used CSS-selector view overrides on ERB:
# DEPRECATED — only works on legacy v4 ERB frontend
Deface::Override.new(
virtual_path: 'spree/admin/products/index',
name: 'add_button',
insert_top: "table.product-list",
text: '<th>Custom Column</th>'
)
In v5+:
store_products_nav_partials, etc.)Pin the gemspec to Spree version ranges:
# spree_my_extension.gemspec
spec.add_dependency 'spree', '~> 5.4'
Test against multiple Spree minor versions in CI before releasing.
First, try Metafields (no migration, upgrade-safe):
product.metafields.create!(namespace: 'my_app', key: 'editor_pick', value: 'true')
If you need indexed / queryable data, write a migration:
# db/migrate/20260512_add_editor_pick_to_spree_products.rb
class AddEditorPickToSpreeProducts < ActiveRecord::Migration[7.1]
def change
add_column :spree_products, :editor_pick, :boolean, default: false, null: false
add_index :spree_products, :editor_pick
end
end
Plus a thin decorator if you need scopes/validations:
module MyApp::ProductDecorator
def self.prepended(base)
base.scope :editor_picks, -> { where(editor_pick: true) }
end
end
Spree::Product.prepend(MyApp::ProductDecorator)
# app/services/my_app/cart_add_item_service.rb
module MyApp
class CartAddItemService < Spree::Cart::AddItem
def call(order:, variant:, quantity: 1, options: {})
# custom logic
result = super
# post-processing
result
end
end
end
# config/initializers/spree.rb
Spree::Dependencies.cart_add_item_service = MyApp::CartAddItemService
Always extend rather than rewrite — call super to preserve core behavior.
Spree::Order and add methods directly — wrap in a decorator module so it's reloadable.unless Spree::Foo.include? guard — double-prepend on autoload chains causes infinite loops.# spec/models/spree/product_decorator_spec.rb
require 'rails_helper'
RSpec.describe Spree::Product do
describe '#display_name' do
let(:product) { build(:product, seo_title: 'Premium Tee') }
it 'returns seo_title when present' do
expect(product.display_name).to eq('Premium Tee')
end
end
end
to_prepare reload hook — decorators don't reload in dev; only fire once at boot.include instead of prepend — method override doesn't work; super goes wrong way.activate pattern._decorator.rb outside app/ — won't be picked up.gem 'spree', '5.4.2' breaks on every patch. Use ~> 5.4 instead.Always verify the engine scaffolding and generator names against current spree gem docs — the generator suite is evolving (v5.2 added Admin SDK generators).
development
Build with Spree's headless Next.js storefront — the official `spree/storefront` repo (Next.js 16 App Router with Server Actions and Turbopack, React 19 Server Components, Tailwind CSS 4, TypeScript 5, `@spree/sdk`, Sentry), server-only auth (httpOnly JWT cookies + publishable key), MeiliSearch faceted catalog, one-page checkout with Apple/Google Pay/Klarna/Affirm/SEPA, multi-region market routing, GA4 + JSON-LD SEO, and Vercel/Docker deployment. Use when forking or customizing the storefront, or evaluating headless adoption.
development
Build with Spree's event bus and Webhooks 2.0 — `Spree::Events` publication, `Spree::Subscriber` DSL with `subscribes_to` and `on`, wildcard matching, lifecycle events (`{model}.created/.updated/.deleted` via `publishes_lifecycle_events`), the canonical event catalog (order.*, payment.*, shipment.*, product.*), Webhooks 2.0 endpoints, HMAC-SHA256 signing (`X-Spree-Webhook-Signature`), exponential-backoff retries, and Sidekiq job orchestration. Use when wiring event-driven business logic, building webhook consumers, or replacing ActiveSupport callback chains.
tools
Cross-cutting Spree development patterns — the customization preference hierarchy (Events > Webhooks > Dependencies > Decorators), `Spree::Dependencies` service-object swapping, the `_decorator.rb` + `prepend` + `self.prepended` idiom, idempotent subscribers and webhook receivers, multi-store scoping discipline, prefixed IDs, calculator polymorphism (shipping/promotion/tax share the base), service-object composition with `dry-monads` or simple results, why to avoid `class_eval` reopening and Deface, and Spree-on-Rails idioms (Hotwire/Turbo Stimulus, ActiveStorage, Action Cable, Sidekiq). Use when designing the architecture of a Spree extension or solving cross-cutting concerns.
development
Deploy Spree to production — PostgreSQL + Redis + Sidekiq stack, Docker multi-arch images on GHCR, the `spree-starter` Dockerfile + Compose, Heroku/Render/Fly.io/AWS targets, env-var conventions, RAILS_MASTER_KEY, asset precompilation (Tailwind 4 + Propshaft), Action Cable, MeiliSearch indexing, S3 / ActiveStorage for media, log/observability setup, zero-downtime deploys, and migration strategy. Use when going from local dev to production, scaling Spree, or troubleshooting deploys.