mcpField
S
spree-customization

spree-customization

github.com/spreeDevelopment10230 installsUpdated 4w ago
Repository

About spree-customization

spree-customization is a Spree 6 developer skill for choosing extension points without forking the core. It maps common changes to settings, hooks, events, providers, API resources, and other customization patterns.

Key features

  1. 01Choose an extension point for a change
  2. 02Use workflow hooks to validate or alter flows
  3. 03React to events with subscribers or webhooks
  4. 04Configure providers and authentication strategies
  5. 05Extend resources, dashboard screens, and models

Use cases

  1. 01Enforce a policy during checkout
  2. 02Send placed-order events to an external system
  3. 03Add a new API resource or dashboard feature
  4. 04Replace a core workflow with custom behavior

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/spree/agent-skills --skill spree-customization

After installing, type/spree-customizationin Claude Code to confirm it loaded.

Details

From the project README

Spree Customization — Where Does My Code Belong?

Spree 6 is built to be extended without forking. The hard part is picking the pattern. Work down the table: options near the top are data or configuration, cheaper to write and untouched by upgrades; options near the bottom couple you to Spree internals.

Commands use the spree CLI form. On a classic Rails app use bin/rails … from the app root and drop the server/ path prefix — see spree-project.

The decision tree

You want to…Reach forSkill
Change currency, markets, languages, delivery zones/methods, tax rates, payment methods, order numberingStore settings — dashboard → Settings, or the Admin API. Data, no deployspree-dashboard, spree-api-v3
Tune installation-wide limits (password length, JWT expiry, rate limits, cart expiry, webhooks on/off)SPREE_* env vars (typed, validated at boot); Spree.config { |c| … } still works in config/initializers/spree.rb— (docs: customization/configuration)
Give a record type its own typed settings (a Brand "featured" flag, a calculator's rate)Model preference — preference :featured, :boolean, default: false on a model with a preferences columnspree-resource
Veto an operation (purchase limits, region policy, B2B eligibility)Workflow validate hook — Spree.hooks.register('carts.add_item.validate', 'MyApp::CheckLimit'), handler calls workflow.reject!spree-workflows
Run code inside a core flow (after finalize, before cancel, feed pricing/provider context)Workflow lifecycle / context hook — carts.complete.after_finalize, orders.cancel.before_cancel, set_*_contextspree-workflows
React after something happened in your own app (index, sync, email, audit)Event subscriber — Spree::Subscriber on order.placed, product.updated…spree-events-webhooks
Tell an external system something happened, no RubyWebhook endpoint — dashboard → Settings → Webhooks, or Admin APIspree-events-webhooks
Require something before checkout completes (PO number, phone, age check) or add a checkout stepSpree::Checkout::Registry.add_requirement / register_step — surfaces in the Cart API requirements[]spree-checkout
Replace how tax, delivery rates, fulfillment, search, pricing, inventory, payouts, digital delivery or payments workProvider — register a class (Spree.tax_providers, Spree.delivery_rate_providers, Spree.search_provider = …, Spree.payment_methods, …) and select it on the store/market/methodspree-providers, spree-taxes, spree-payments
Sign customers/staff in with your own IdP or tokenAuth strategy — Spree.store_authentication_strategies.add(:idp, MyApp::Auth::IdpStrategy) (also admin_/seller_)spree-auth-permissions
Store extra merchant-facing data (material, care instructions, gift message)Custom fields — CustomFieldDefinition + record.set_custom_field('custom.material', 'Cotton'); typed, filterable, storefront_visiblespree-catalog
Store integration/machine data (ERP id, UTM source, sync state)metadata — schemaless JSON, merge semantics, write-only in Store API, readable in Admin APIspree-api-v3
Make an attribute filterable/sortable via q[...]Spree.ransack.add_attribute / add_association / add_scopespree-api-v3
Add or drop an address rule (PO boxes, phone format)Spree.validators.addresses.register / unregister (from config.to_prepare)spree-customization (below)
Make a new column writable through the Admin APISpree::Product.additional_permitted_attributes += [:brand_id]spree-resource
Gate a new resource behind staff roles / API key scopesSpree.permissions.register_scope(:brands, group: :catalog, resources: -> { [Spree::Brand] }) → read_brands / write_brandsspree-auth-permissions
Add a whole new model + Admin/Store endpointspree generate api_resource Brand …spree-resource
Add a status to a core modelSpree::Return.add_status('on_hold', after: 'approved') + your own workflow to move records therespree-workflows
Change what a core flow fundamentally doesDependency swap — Spree::Dependencies.cart_add_item_workflow = 'MyApp::Carts::AddItem'spree-dependencies
Add an association, scope or helper method to a core modelDecorator (spree generate model_decorator Spree::Product) — last resortspree-decorators
Add admin screens, fields, columns, nav itemsDashboard plugin in apps/dashboard/src/plugins.ts (defineDashboardPlugin)spree-dashboard, spree-dashboard-plugins
Share any of the above across appsExtension gem (Rails engine) + optional dashboard plugin (spree plugin new)spree-extensions
Hook, event or dependency?
  1. Must you block or change the outcome? → workflow hook. Only validate (and before_cancel) hooks can stop a flow, and they run before money moves.
  2. Do you just need to know it happened? → event subscriber (in-app) or webhook (external). Decoupled, async by default, survives upgrades untouched.
  3. Must the flow do something fundamentally different? → dependency swap — and accept that you now own that class across upgrades.

Worked examples

Block checkout for a region — validate hook
# server/config/initializers/spree.rb
Spree.hooks.register('carts.complete.validate', 'MyApp::RegionPolicy')

# server/app/services/my_app/region_policy.rb
module MyApp
  class RegionPolicy
    def call(workflow)
      return unless workflow.cart.ship_address&.country_code == 'KP'

      workflow.reject!('We cannot deliver to this region')
    end
  end
end

Hook keys are validated at boot (Spree::Hooks::UnknownHookError on a typo). Handlers are stored as class-name strings, so registration is reload-safe. Several handlers may share a key — the first reject! wins.

Push placed orders to an ERP — event subscriber
spree generate subscriber ErpOrderSync order.placed   # creates the class AND registers it
# server/app/subscribers/erp_order_sync_subscriber.rb
class ErpOrderSyncSubscriber < Spree::Subscriber
  subscribes_to 'order.placed'

  def handle(event)
    order = Spree::Order.find_by_prefix_id(event.payload['id'])
    MyApp::Erp.push(order) if order
  end
end

Subscribers are not auto-discovered — without the generator, add Spree.subscribers << ErpOrderSyncSubscriber yourself.

Require a PO number for B2B carts — checkout requirement
Spree::Checkout::Registry.add_requirement(
  step: :payment,
  field: :po_number,
  message: 'PO number is required',
  satisfied: ->(cart) { cart.metadata['po_number'].present? },
  applicable: ->(cart) { cart.company.present? }
)

The requirement appears in the Cart API requirements array, so the storefront renders it without duplicating the rule, and Carts::Complete refuses to finish until it is satisfied.

Add a brand_id to products — resource + permitted attribute + decorator
spree generate api_resource Brand name:string
spree generate migration AddBrandIdToSpreeProducts brand_id:bigint:index
spree migrate
# server/config/initializers/spree.rb
Rails.application.config.to_prepare do
  Spree::Product.additional_permitted_attributes += [:brand_id]   # += — the default is frozen
  Spree::Product.storefront_ransackable_associations |= %w[brand]  # Store API filters follow only this list
end
Spree.ransack.add_association(Spree::Product, :brand)            # Admin API filters
Spree.permissions.register_scope(:brands, group: :catalog, resources: -> { [Spree::Brand] })

# server/app/models/spree/product_decorator.rb — structural addition, the one legit decorator job
module Spree
  module ProductDecorator
    def self.prepended(base)
      base.belongs_to :brand, class_name: 'Spree::Brand', optional: true
    end
  end
  Product.prepend(ProductDecorator)
end

If the data is merchant-editable and doesn't need its own table, a custom field is less work than all of the above.

Relax a shipped address rule — validator registry
# server/config/initializers/spree.rb
Rails.application.config.to_prepare do
  Spree.validators.addresses.register(MyApp::PostBoxValidator)          # an ActiveModel::Validator
  Spree.validators.addresses.unregister(Spree::Addresses::PhoneValidator)
end

Several rules are also plain store preferences (address_requires_phone, company_field_enabled, disable_sku_validation) — check those first. There is no general validator registry for other models: add a model validation with a decorator; veto operations with a validate hook.

Replace cart add-to-cart logic — dependency swap
Spree::Dependencies.cart_add_item_workflow = 'MyApp::Carts::AddItem'   # subclass Spree::Carts::AddItem

Keys end in _workflow for workflows (carts_complete_workflow, order_cancel_workflow, cart_recalculate_totals_workflow, fulfillment_update_workflow, …) and _service for plain services (order_update_statuses_service). Prefer a hook when you only need to add behavior.

Anti-patterns

  • Overriding finalize!, after_finalize, cancel or other lifecycle methods in a decorator → register a hook (carts.complete.after_finalize, orders.cancel.after_cancel). There are no state machines left to hook into — state_machine blocks, after_transition and checkout_flow don't exist.
  • after_save / after_commit callbacks added to Spree models for side effects → event subscriber.
  • clear_validators! → wipes every validation on the model, including other extensions'. Use store preferences, Spree.validators.addresses.unregister, or a targeted decorator.
  • Spree::Product.additional_permitted_attributes << :brand_id → raises FrozenError. Always +=.
  • Writing a legacy *_service dependency key (cart_add_item_service, carts_complete_service) → the write is stashed and ignored with a warning; use the *_workflow key.
  • Setting payment_status / fulfillment_status directly → they're derived by Spree::Orders::UpdateStatuses; act on payments/fulfillments instead.
  • Enums or a state machine for a new status field → include Spree::HasStatus; has_status :a, :b, default: :a, transitions in a workflow.
  • Admin changes via Rails views/partials → the admin is the React dashboard; use a dashboard plugin.
  • A private extension gem for one app → put code in server/app/; gems are for sharing across apps.
  • Forking Spree → walk this table first; the seam almost certainly exists.

Where to read further

  • node_modules/@spree/docs/dist/developer/customization/quickstart.md — the canonical recommended order
  • …/customization/{configuration,workflows,dependencies,decorators,validations,metadata,model-preferences,permissions,checkout,api}.md
  • …/providers/overview.md and …/how-to/custom-*.md for each provider contract
  • …/dashboard/customization/quickstart.md for admin UI
  • Online: https://spreecommerce.org/docs/developer/customization/quickstart

Comments

Markdown supported