---
title: Quickstart
og:title: Spree Customization Quickstart
description: Recommended ways to customize the Spree backend — configuration, workflow hooks, events, checkout steps, providers, dependencies, and decorators as a last resort.
---

Spree is designed to be customized without forking it. This guide presents the options **in order of recommendation** — start at the top and only move down when a simpler option doesn't fit.

> **NOTE:** Business logic is extended through **workflow hooks** rather than by replacing classes wholesale, and the admin is a React application with its own extension points rather than a Rails engine extended through partials.

## Quick reference

| What you want to do | Recommended approach |
|---|---|
| Change store settings (currency, markets, languages) | [Store settings](#store-settings) |
| Tweak Spree behavior globally | [Configuration](#configuration) |
| Run your own logic inside checkout, cancellation, refunds | [Workflow hooks](#workflow-hooks) |
| Block an operation from happening | [Workflow hooks](#workflow-hooks) — `validate` |
| React after something happened (sync, notifications) | [Events & subscribers](#events-and-subscribers) |
| Add or reorder checkout steps | [Checkout registry](#checkout-steps) |
| Swap tax, delivery rate, search or fulfillment behavior | [Providers](#providers) |
| Add searchable/filterable fields | [Search & filtering](#search-and-filtering) |
| Use your own user model or identity provider | [Authentication](#authentication) |
| Notify external services | [Webhooks](#webhooks) |
| Replace a whole workflow or service | [Dependencies](#dependencies) |
| Add associations/validations to models | [Decorators](#decorators) (last resort) |

> **INFO:** **Customizing the admin?** This page covers the backend — models, API and business logic. The admin is a separate React application with its own extension points. See [Dashboard Customization](../dashboard/customization/quickstart.md).

**Best for:** currency, markets, delivery zones, languages, and other business settings.

    Most day-to-day configuration is data, not code. Change it in the dashboard under **Settings** — no deploy required.
  <details>
<summary>Configuration</summary>

**Best for:** tweaking Spree's behavior globally.

    ```ruby config/initializers/spree.rb
    Spree.config do |config|
      config.minimum_password_length = 10
    end
    ```

    See [Configuration](configuration.md).

</details>

  <details>
<summary>Workflow hooks</summary>

**Best for:** running your own logic *inside* a core flow — checkout completion, order cancellation, refunds, fulfillment creation.

    > **INFO:** Hooks are the **headline extension point in Spree**, so you don't have to replace an entire class and keep your copy in sync with core forever.

    Register a handler against a hook key — `<workflow key>.<hook name>`:

    ```ruby config/initializers/spree.rb
    Spree.hooks.register('carts.complete.validate') do |flow|
      flow.reject!('We cannot deliver to this region') unless deliverable?(flow.cart)
    end
    ```

    Pass a class instead of a block for anything longer than a line:

    ```ruby
    Spree.hooks.register('carts.complete.after_finalize', 'MyApp::PushOrderToERP')
    ```

    There are three families of hook:

    | Family | Naming | What it can do |
    |---|---|---|
    | **validate** | `validate` | Veto the operation with `flow.reject!(message)` |
    | **lifecycle** | past tense — `after_finalize`, `after_cancel` | React to what happened; read-only |
    | **context** | `set_*_context`, `get_provider_data` | Return a hash that gets merged into the flow's data |

    Hook keys are validated after boot, so a typo fails startup rather than silently never running.

    See [Services & Workflows](workflows.md) for the full list of flows and their hooks.

</details>

  <details>
<summary>Events and subscribers</summary>

**Best for:** reacting *after* something happened — syncing to external systems, notifications, audit logging.

    ```ruby app/subscribers/spree/order_placed_subscriber.rb
    module Spree
      class OrderPlacedSubscriber < Spree::Subscriber
        subscribes_to 'order.placed'

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

          MyApp::ERP.push(order)
        end
      end
    end
    ```

    **Events or hooks?** A hook runs *inside* the flow and can influence or block it. An event fires *after* the fact and cannot. Reach for an event unless you need to change the outcome.

    See [Events](../core-concepts/events.md).

</details>

  <details>
<summary>Checkout steps</summary>

**Best for:** adding, removing or reordering checkout steps.

    Spree has no checkout state machine. A cart reports what it still needs, and you can add to that list:

    ```ruby config/initializers/spree.rb
    Spree::Checkout::Registry.register_step(
      name: :loyalty,
      before: :payment,
      satisfied: ->(cart) { cart.metadata['loyalty_number'].present? },
      requirements: ->(cart) {
        [{ step: 'loyalty', field: 'loyalty_number', message: 'Loyalty number is required' }]
      }
    )
    ```

    Or attach a requirement to an existing step:

    ```ruby
    Spree::Checkout::Registry.add_requirement(
      step: :payment,
      field: :po_number,
      message: 'PO number is required',
      satisfied: ->(cart) { cart.metadata['po_number'].present? }
    )
    ```

    Registered requirements appear in the Cart API's `requirements` array, so your storefront renders them without duplicating any rules.

    See [Carts](../core-concepts/carts.md#checkout-requirements).

</details>

  <details>
<summary>Providers</summary>

**Best for:** swapping a whole area of behavior for your own implementation.

    Spree exposes pluggable providers for the domains that vary most between businesses:

    | Provider | Controls |
    |---|---|
    | Tax provider | How tax is calculated, per market |
    | Delivery rate provider | How delivery is priced — flat rates or live carrier rates |
    | Fulfillment provider | How a delivery type behaves — shipping, digital, pickup |
    | Search provider | Database or Meilisearch |
    | Payout provider | Where marketplace payouts go |

    Each is a class you register and select on the relevant record, so no conditional code lands in core.

    See [Fulfillments](../core-concepts/fulfillments.md#delivery-types) and [Taxes](../core-concepts/taxes.md).

</details>

  <details>
<summary>Search and filtering</summary>

**Best for:** making custom fields searchable and sortable in the dashboard and API.

    ```ruby config/initializers/spree.rb
    Spree.ransack.add_attribute(Spree::Product, :erp_id)
    Spree.ransack.add_association(Spree::Product, :brand)
    Spree.ransack.add_scope(Spree::Product, :featured)
    ```

    See [Search & Filtering](../core-concepts/search-filtering.md).

</details>

  <details>
<summary>Authentication</summary>

**Best for:** using your own user model or identity provider.

    Spree owns its authentication stack rather than depending on Devise, and separates the customer identity from the staff identity. Point Spree at your own classes with `Spree.customer_class` and `Spree.admin_user_class`.

    See [Authentication](../providers/sso.md).

</details>

  <details>
<summary>Webhooks</summary>

**Best for:** notifying external services — ERPs, CRMs, fulfillment systems — without writing Ruby.

    Configure them in the dashboard under **Settings → Webhooks**, or through the Admin API.

    See [Webhooks](../core-concepts/webhooks.md).

</details>

  <details>
<summary>Dependencies</summary>

**Best for:** replacing an entire workflow or service with your own class.

    ```ruby config/initializers/spree.rb
    Spree::Dependencies.cart_add_item_workflow = 'MyApp::Carts::AddItem'
    ```

    > **WARNING:** Seam names end in `_workflow`. The old `*_service` names still resolve with a warning, but **writes to them are ignored** — a class written against the old service contract isn't interchangeable with a workflow. Update the key if your code still uses the old name.

    Prefer a hook when you only need to add behavior. Replace the class only when you need to change what the flow fundamentally does.

    See [Dependencies](dependencies.md).

</details>

  <details>
<summary>Decorators</summary>

**Best for:** adding associations, validations and scopes to Spree models. Use as a last resort.

    > **WARNING:** Decorators couple your code to Spree internals and are the most common cause of painful upgrades.
> 
>       **Do not use decorators for:**
>       - Logic inside a core flow → use [workflow hooks](#workflow-hooks)
>       - After-save callbacks → use [events](#events-and-subscribers)
>       - External service sync → use [webhooks](#webhooks)
>       - Replacing a service → use [dependencies](#dependencies)
>       - Admin UI changes → use [dashboard customization](../dashboard/customization/quickstart.md)

    They remain appropriate for structural additions:

    ```ruby app/models/spree/product_decorator.rb
    module Spree
      module ProductDecorator
        def self.prepended(base)
          base.belongs_to :brand, class_name: 'MyApp::Brand', optional: true
          base.scope :featured, -> { where(featured: true) }
        end

        def full_title
          "#{brand&.name} #{name}"
        end
      end

      Product.prepend(ProductDecorator)
    end
    ```

    See [Decorators](decorators.md).

</details>


## Choosing between hooks, events and dependencies

These three overlap, and picking the wrong one is the most common source of upgrade pain:

**Step 1: Do you need to block or change the outcome?**

Use a **workflow hook**. Only `validate` hooks can stop an operation, and only they run early enough to do so safely — before money moves.

  **Step 2: Do you just need to know it happened?**

Use an **event subscriber**. It's decoupled, testable, and survives upgrades untouched.

  **Step 3: Do you need the flow to do something fundamentally different?**

Use **dependencies** to replace the class — and accept that you now own keeping it current.


## Related

- [Services & Workflows](workflows.md) — the workflow model and every hook
- [Carts](../core-concepts/carts.md) — checkout requirements and completion
- [Dashboard Customization](../dashboard/customization/quickstart.md) — extending the admin
