> ## Documentation Index
> Fetch the complete documentation index at: https://spreecommerce.org/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Upgrading to Spree 5.5

> Step-by-step guide to upgrading a Spree 5.4 application to Spree 5.5, including gem updates, migrations, and breaking changes to review.

<Info>
  Before proceeding to upgrade, please ensure you're at [Spree 5.4](/developer/upgrades/5.3-to-5.4).
</Info>

## Upgrade steps

### Update gems

```bash theme={"theme":"night-owl"}
bundle update
```

### Fetch and run missing migrations

```bash theme={"theme":"night-owl"}
bin/rake spree:install:migrations && bin/rails db:migrate
```

### Migrate legacy variant-pinned media

In 5.5 the [product is the default owner of media](/developer/core-concepts/media#product-level-gallery). Existing variant-pinned images keep rendering, but new admin uploads attach to the product. To consolidate both into a single gallery, run:

```bash theme={"theme":"night-owl"}
bin/rails spree:media:migrate_master_images_to_product_media
```

The task enqueues one `Spree::Media::MigrateProductAssetsJob` per product onto the `images` queue — make sure your job runner is processing that queue. Each job is idempotent, so re-running the task is safe; it skips products that no longer have variant-pinned assets.

For larger catalogs, tune the batching with `BATCH_SIZE`:

```bash theme={"theme":"night-owl"}
bin/rails spree:media:migrate_master_images_to_product_media BATCH_SIZE=1000
```

<Warning>
  Run the task locally and on production. It does not block storefront rendering — new uploads attach to the product immediately — but until the enqueued jobs finish, old assets remain pinned to variants.
</Warning>

### Run the Channels upgrade

Spree 5.5 introduces [Sales Channels](/developer/core-concepts/channels) — a per-store distribution surface (online storefront, POS, marketplace integration, wholesale portal). Products are published to a channel via the new `spree_product_publications` join table, and orders are attributed to a channel via `spree_orders.channel_id`.

The migrations add a `default` boolean on `spree_channels`, a `store_id` column on `spree_products`, and create the new `spree_product_publications` table — **but they do not seed default channels, attach existing products to a store, or backfill order channels**. That work is done by an idempotent rake task you must run after `db:migrate`:

```bash theme={"theme":"night-owl"}
bin/rake spree:channels:upgrade
```

The task runs four sub-tasks in order:

1. `spree:channels:create_defaults` — creates the default "Online Store" channel for every existing store (via `Store#ensure_default_channel`).
2. `spree:upgrade:populate_publications` — for every product that doesn't yet have a `store_id`, picks a "home" store from the legacy `spree_products_stores` join (preferring the store flagged `default: true`, otherwise the earliest row), sets `spree_products.store_id`, and creates a `spree_product_publications` row on each attached store's default channel. Runs in a transaction per product so a partial failure leaves nothing half-applied.
3. `spree:channels:backfill_order_channel_ids` — sets `spree_orders.channel_id` from the legacy `spree_orders.channel` string column. Unknown codes auto-create a new channel under that store. NULL/blank values map to the default channel.
4. `spree:channels:backfill_product_publication_dates` — copies the deprecated `Product.available_on` and `Product.discontinue_on` columns into each publication's `published_at` / `unpublished_at` (only where the publication's date is currently NULL).

The task is fully idempotent — safe to re-run if it fails partway, and a no-op on stores/products/orders that have already been upgraded.

<Warning>
  Until `spree:channels:upgrade` runs, every product has `store_id IS NULL` and is invisible to `Product.for_store(store)`. The admin product list, storefront catalog, and search indexer all return empty. Run the task immediately after `db:migrate`.
</Warning>

#### Multi-store catalogs

If you have products attached to multiple stores via the legacy `spree_products_stores` join, the `populate_publications` task picks **one** "home" store per product and creates publications on every store's default channel. The `spree_products_stores` table is **kept** as legacy compat surface — the upcoming `spree_multi_store` extension restores the full `Product has_many :stores` association on top of it.

For single-store deployments this is invisible; you can move on without touching `spree_products_stores` again.

#### Behavioral changes

* Newly-created products in **the dashboard SPA** are auto-published on the store's default channel; the merchant can untick channels post-create via the Publishing card.
* Newly-created products via the **Admin API** are NOT auto-published — the caller must supply `product_publications: [{ channel_id }]` on create or use `POST /api/v3/admin/channels/:id/add_products` afterwards.
* `Product.available_on=` and `Product.discontinue_on=` setters emit deprecation warnings and now write to every per-channel publication's `published_at` / `unpublished_at`. Reading these attributes on a product prefers the current-channel publication's value over the legacy column.
* `Spree::Channel#add_products(product_ids)` is idempotent and **preserves existing publication windows** when called without `published_at`/`unpublished_at` kwargs.

Orders modified after the upgrade auto-set `channel_id` via the model's `before_validation :ensure_channel_presence` callback, so `backfill_order_channel_ids` is only strictly required for orders that aren't touched again post-upgrade — but running it at upgrade time avoids surprises later. The legacy `channel` string column is **kept** on `spree_orders` and ignored by ActiveRecord (`Spree::Order` declares it in `ignored_columns`). It will be dropped in a later Spree release once everyone has had a chance to run the backfill.

### Schedule the Stock Reservations expiry job

Spree 5.5 introduces time-limited stock reservations during checkout to prevent two customers from buying the same last unit at the same time. Abandoned checkouts leave behind expired reservation rows, and Spree does **not** auto-schedule the cleanup — your application's job runner must run `Spree::StockReservations::ExpireJob` periodically (every minute is the recommended cadence).

If you skip this step, expired reservations accumulate in the table indefinitely. The Quantifier still ignores them at availability-check time (so customers see correct stock), but the table grows unbounded.

#### sidekiq-cron

```yaml theme={"theme":"night-owl"}
# config/sidekiq_cron.yml
expire_stock_reservations:
  cron: "* * * * *"
  class: "Spree::StockReservations::ExpireJob"
  queue: default
```

#### solid\_queue

```yaml theme={"theme":"night-owl"}
# config/recurring.yml
expire_stock_reservations:
  schedule: every minute
  class: Spree::StockReservations::ExpireJob
```

#### good\_job

```ruby theme={"theme":"night-owl"}
# config/initializers/good_job.rb
Rails.application.configure do
  config.good_job.cron = {
    expire_stock_reservations: {
      cron: '* * * * *',
      class: 'Spree::StockReservations::ExpireJob'
    }
  }
end
```

### (Optional) Tune the reservation TTL

The default reservation TTL is **10 minutes**. To override globally:

```ruby theme={"theme":"night-owl"}
# config/initializers/spree.rb
Spree::Config[:default_stock_reservation_ttl_minutes] = 15
```

To override per Store, set the preference on the Store record:

```ruby theme={"theme":"night-owl"}
store.update!(preferred_stock_reservation_ttl_minutes: 20)
```

The per-Store value, when set, takes precedence over the global default.

### (Optional) Disable Stock Reservations

Stock reservations are enabled by default. To opt out and revert to pre-5.5 behavior (no holds during checkout, Quantifier returns raw `count_on_hand`):

```ruby theme={"theme":"night-owl"}
# config/initializers/spree.rb
Spree::Config[:stock_reservations_enabled] = false
```

The Quantifier short-circuits before any reservation query when this is `false`, so there's no runtime cost and no table growth.

### (Optional) Opt out of rules-based Order Routing

Spree 5.5 introduces [Order Routing](/developer/core-concepts/shipments#order-routing) — a configurable, per-channel pipeline that decides which stock locations fulfill an order. Every store and every channel ships with three default rules (Preferred Location → Minimize Splits → Default Location) that produce sensible behavior out of the box, with no migration work required.

If you've heavily customized fulfillment in Spree 5.4 and aren't ready to adopt the new rules engine, you can keep the legacy pre-5.5 routing by switching the store's strategy to `Spree::OrderRouting::Strategy::Legacy`:

```ruby theme={"theme":"night-owl"}
store.update!(preferred_order_routing_strategy: 'Spree::OrderRouting::Strategy::Legacy')
```

The Legacy strategy delegates to `Spree::Stock::Coordinator`, which is the exact pre-5.5 packing pipeline — every active stock location is packed, the Prioritizer distributes inventory units across the resulting packages, and no merchant routing rules are consulted. Your existing customizations on `Coordinator`, `Packer`, `Prioritizer`, and the splitters keep working unchanged.

<Warning>
  `Spree::OrderRouting::Strategy::Legacy` and `Spree::Stock::Coordinator` are slated for removal in Spree 6.0. Use this only as a temporary escape hatch while you evaluate the new Rules strategy. New 5.5+ installations should stay on the default Rules strategy.
</Warning>

## Behavior changes worth knowing

### Cart changes during checkout can now fail with insufficient stock

When a customer is in checkout and tries to add an item, increase a quantity, or remove a line item, Spree now re-checks whether the cart still fits in available stock (subtracting what other customers are holding in their own active checkouts). If it doesn't, the change is rejected up front instead of silently completing and failing later at order submission.

Storefronts and custom integrations that act on the cart should expect this new failure path and surface the error to the customer.

### Storefront availability drops faster under contention

Other customers now see availability reduced by all active reservations, not just by completed orders. This is the intended fix to overselling — but if you have a real-time inventory dashboard that reads `count_on_hand` directly (rather than going through Spree's availability checks), you'll want to expose a "Reserved" axis to merchants so they can see in-checkout demand.

### Payment method `type` on the wire is now a shorthand, not a Rails class name

The `type` attribute on `Spree::PaymentMethod` (returned by both the Store API and the Admin API) used to be the full Rails STI class name — `"SpreeStripe::Gateway"`, `"SpreeAdyen::Gateway"`, `"Spree::PaymentMethod::Check"`. In 5.5 it switches to a stable shorthand derived from `Spree::Base.api_type`:

| Class                               | Pre-5.5 `type`                      | 5.5+ `type`         |
| ----------------------------------- | ----------------------------------- | ------------------- |
| `Spree::PaymentMethod::Check`       | `Spree::PaymentMethod::Check`       | `check`             |
| `Spree::PaymentMethod::StoreCredit` | `Spree::PaymentMethod::StoreCredit` | `store_credit`      |
| `SpreeStripe::Gateway`              | `SpreeStripe::Gateway`              | `stripe`            |
| `SpreeAdyen::Gateway`               | `SpreeAdyen::Gateway`               | `adyen`             |
| `SpreePaypalCheckout::Gateway`      | `SpreePaypalCheckout::Gateway`      | `paypal_checkout`   |
| `SpreeRazorpayCheckout::Gateway`    | `SpreeRazorpayCheckout::Gateway`    | `razorpay_checkout` |

The shorthand is also what `POST /api/v3/admin/payment_methods` now expects as `type` when creating a new method, and what `GET /api/v3/admin/payment_methods/types` returns in its `type` field.

This is a breaking change for any storefront or integration that **string-matches the payment method type** to pick which payment-gateway SDK to load. The official Spree Next.js storefront resolves the gateway in `src/lib/utils/payment-gateway.ts`; update its map to key on the new shorthand:

```ts theme={"theme":"night-owl"}
const GATEWAY_TYPE_MAP: Record<string, GatewayId> = {
  // 5.5+ shorthands
  stripe: "stripe",
  adyen: "adyen",
  paypal_checkout: "paypal",
  razorpay_checkout: "razorpay",
  // Pre-5.5 Rails class names — keep while you have older backends in
  // the field; drop once everyone is on 5.5+.
  "SpreeStripe::Gateway": "stripe",
  "SpreeAdyen::Gateway": "adyen",
  "SpreePaypalCheckout::Gateway": "paypal",
  "SpreeRazorpayCheckout::Gateway": "razorpay",
};
```

If you maintain a custom storefront, search it for the legacy class strings (`"SpreeStripe::Gateway"`, `"SpreePaypalCheckout::Gateway"`, etc.) and add the corresponding 5.5+ shorthand alongside each one. The symptom of missing this is checkout rendering a generic "this payment method is not yet supported" placeholder instead of the gateway's SDK form.

### Order Routing chooses location order via merchant rules instead of database order

The default routing strategy (`Spree::OrderRouting::Strategy::Rules`) packs the same set of stock locations as before, but the **order** in which locations are tried is now determined by the routing rules — Preferred Location → Minimize Splits → Default Location — rather than by raw database row order. The unit distribution (Prioritizer + Adjuster) is unchanged: top-ranked location's packages get first pick of on-hand inventory, the rest spills over.

For most stores this is invisible: when one location can fulfill the entire cart, that location now wins consistently (instead of depending on database iteration order). When the cart needs to split across locations, the same multi-location split happens — just with the location order driven by rules.

If you rely on the legacy "every location packed in iteration order, no rule consulted" behavior, see [Opt out of rules-based Order Routing](#optional-opt-out-of-rules-based-order-routing) above.
