Spree Customization — Where Does My Code Belong?
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 thespree-projectskill —bin/rails/bundle exec rakefrom the app root, paths without thebackend/prefix.
Spree is heavily customizable. The work of any Spree project is mostly customization — wiring in external services, adding custom models, tweaking behavior, extending the admin. The thing that's hard isn't how to customize; it's which pattern fits a given problem.
This skill is a decision tree. It maps a customization need to the right specific skill — read those for the deep dive. Walk the table top to bottom; the higher options are simpler and survive upgrades better than the lower ones.
The decision tree
| What you're trying to do | Reach for | Deep-dive skill |
|---|---|---|
| Change merchant-facing settings (currencies, languages, tax zones, shipping methods, payment methods) | Admin Settings UI | — |
| Tweak Spree's runtime behavior globally | Spree.config block (config.<setting> = …) in config/initializers/spree.rb; read anywhere via Spree::Config[:key] |
(configuration is straightforward — see docs link below) |
| React to something happening in Spree (order completed, product updated, customer registered, stock changed) | Events subscriber | spree-events-webhooks |
| Notify an external service (ERP, CRM, fulfillment, analytics, Slack) when something happens | Events subscriber OR outbound webhook | spree-events-webhooks |
| Replace how a core service computes (cart add, tax calculation, search, checkout flow, ability checks) | Dependency injection via Spree.dependencies |
spree-dependencies |
| Add a menu item / nav entry to the admin | Spree.admin.navigation.sidebar.add |
spree-admin |
| Add a section / form field to an existing admin page | Spree.admin.partials.<page> << '...' |
spree-admin |
| Customize an admin table (columns, sort) | Spree.admin.tables.<key>.add ... |
spree-admin |
| Make a new attribute searchable / filterable in the API or admin | Spree.ransack.add_attribute(Class, :attr) |
spree-api-v3 |
| Customize the checkout flow (skip a step, add a step, change validation) | checkout_flow block on a Spree::Order decorator |
spree-checkout |
| Add a brand-new model + API endpoint (Brand, Vendor, etc.) | spree:api_resource generator |
spree-resource |
| Add a Spree model with no API surface (internal record, lookup table, supporting model) | spree:model generator |
spree-resource |
| Add an association / validation / scope / method to an existing Spree model | Decorator via spree:model_decorator |
spree-decorators |
| Add a before_action / new action / override existing action on an existing controller | Decorator via spree:controller_decorator |
spree-decorators |
| Pull in a third-party gem (Stripe, Adyen, search, i18n, social login) | gem 'spree_x' + install generator |
spree-extensions |
| Package customization to share across multiple Spree apps | Build an extension (Rails engine as a gem) | spree-extensions |
The priority order, in one sentence
Settings → Configuration → Events → Dependencies → Admin / Ransack APIs → Generators (resource or model) → Decorators → Extensions.
Lower-numbered options are easier to write, easier to test, and survive Spree upgrades cleanly. Decorators are reserved for structural changes to existing Spree classes (associations, validations, scopes, methods) — for behavioral changes (callbacks, side effects, sync), use Events instead.
Worked examples
"I need to sync orders to my ERP when they complete"
That's a side effect that fires when an order finishes. Don't decorate Spree::Order to add after_save — write a subscriber:
# app/subscribers/erp_order_sync_subscriber.rb
class ErpOrderSyncSubscriber < Spree::Subscriber
subscribes_to 'order.completed'
def handle(event)
ErpClient.sync_order(event.payload['id'])
end
end
Then register it — subscribers are not auto-discovered (or skip both steps with spree generate subscriber ErpOrderSync order.completed, which creates the class and the registration in one go):
# config/initializers/spree.rb
Rails.application.config.after_initialize do
Spree.subscribers << ErpOrderSyncSubscriber
end
→ See the spree-events-webhooks skill for the full event catalog and async/sync behavior.
"I need to add a Brand model that products belong to"
Brand is a brand-new resource with its own API surface. Use the generator:
spree generate api_resource Brand name:string:uniq active:boolean
Add the brand_id column to products:
spree generate migration AddBrandIdToSpreeProducts brand_id:bigint:index
spree migrate
Then add the belongs_to :brand to Spree::Product via a decorator:
spree generate model_decorator Spree::Product # bare generator names auto-prefix to spree:
module Spree
module ProductDecorator
def self.prepended(base)
base.belongs_to :brand, class_name: 'Spree::Brand', optional: true
end
end
Product.prepend(ProductDecorator)
end
→ See the spree-resource skill for the generator details and the spree-decorators skill for the decorator pattern.
"I need to make external_id searchable in the admin orders table"
That's a Ransack allowlist concern — not a decorator job. Use the Ransack configuration API:
# config/initializers/spree.rb
Spree.ransack.add_attribute(Spree::Order, :external_id)
→ See the spree-api-v3 skill for Ransack details.
"I need to change how the cart calculates totals"
That's a service swap. Subclass Spree::Cart::Recalculate and register your replacement:
# config/initializers/spree.rb
Spree.cart_recalculate_service = MyApp::Cart::Recalculate
→ See the spree-dependencies skill for the full dependency injection pattern, the catalog of 70+ core and 300+ API injection points, and the spree:dependencies:list / :overrides / :validate rake tasks.
"I need to add a 'Loyalty Points' page to the admin sidebar"
Use the admin navigation API — no decorator on the admin controller required:
# config/initializers/spree.rb
Rails.application.config.after_initialize do
Spree.admin.navigation.sidebar.add :loyalty_points,
label: :loyalty_points,
url: :admin_loyalty_points_path,
icon: 'award',
position: 80
end
→ See the spree-admin skill for the full extension API.
"I need to add a 'preferred carrier' column to the products admin form"
Use the admin partials API to inject a section — no view override required:
# config/initializers/spree.rb
Spree.admin.partials.product_form << 'spree/admin/products/preferred_carrier'
Then drop the partial at app/views/spree/admin/products/_preferred_carrier.html.erb. Permit the new attribute via:
Rails.application.config.after_initialize do
Spree::PermittedAttributes.product_attributes << :preferred_carrier
end
→ See the spree-admin skill.
"I need to override how Spree::Product#available? decides availability"
That's a structural change — a behavioral override on an existing model method. Decorate:
module Spree
module ProductDecorator
def available?
return false if discontinued?
super
end
end
Product.prepend(ProductDecorator)
end
Call super so you extend Spree's logic instead of replacing it.
→ See the spree-decorators skill.
Anti-patterns
These are tempting but wrong — the table above gives you a better answer for each.
after_savecallbacks in a decorator → use an Events subscriber instead. Decorator callbacks couple to model save mechanics and can break on minor upgrades.- Reaching into a Spree controller to add a sidebar item → use
Spree.admin.navigation.sidebar.add. No controller decorator needed. - Overriding a Spree serializer to add a field → register the field via dependency injection or use
Spree.api.<resource>_serializer = 'MyApp::FooSerializer'. - Decorating a model to add
ransackable_attributes→ useSpree.ransack.add_attributeinstead. Same outcome, no coupling to model internals. - Building a private extension gem for one-app customization → put the code directly in
app/(subscribers, decorators, services). Extensions are for sharing across apps. - Forking Spree → almost never necessary. If you find yourself wanting to, work through this table from the top first — the right pattern almost certainly exists.
Where to read further
- Spree's customization docs (the canonical decision tree):
node_modules/@spree/docs/dist/developer/customization/quickstart.md - Each specific pattern's deep dive: the linked
spree-Xskill in the table above - Configuration reference:
node_modules/@spree/docs/dist/developer/customization/configuration.md