About spree-upgrade-5-to-6
A migration playbook for upgrading Spree 5.x to 6.0. It covers prerequisites, upgrade steps, data backfills, breaking code changes, and post-migration checks for development teams.
Key features
- 01Spree 6.0 upgrade prerequisites
- 02Ordered migration and backfill steps
- 03Breaking-change overview
- 04Code search and repair guidance
- 05Post-upgrade data checks
Use cases
- 01Planning a Spree 5.6 to 6.0 upgrade
- 02Preparing Rails and extension dependencies
- 03Reviewing migrated orders and customer data
- 04Finding outdated APIs in application code
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ spree/ agent-skills --skill spree-upgrade-5-to-6After installing, type/spree-upgrade-5-to-6in Claude Code to confirm it loaded.
Bundled filesreferences/grep-recipes.md
Details
From the project READMEUpgrading Spree 5.x → 6.0
Spree 6.0 is the breaking-change release. Cart and Order are separate models, state machines are gone (statuses + workflows), adjustments became typed rows (TaxLine / Discount / Fee), the master variant is gone, shipping became fulfillment/delivery, taxons became categories/collections, users became Spree::Customer, staff permissions became data, and the Rails admin, Rails storefront and API v2 were removed in favor of the React dashboard (spree_dashboard) and API v3.
The upgrade is: preconditions → gems → migrations → backfills → code fixes → behavioral review. Budget real time for the last two; the rake part is the easy part.
Generic mechanics (spree upgrade flags, STEP=, production release phase) are in the spree-upgrade skill. This skill is the 6.0-specific playbook.
1. Preconditions — do these before touching the Gemfile
- Be on 5.6.x, fully upgraded. The 6.0 manifest assumes the 5.6 schema. Land on 5.6 first (
spree upgrade --to 5.6, or bump to~> 5.6and runbundle exec rake spree:upgrade). The 6.0 run also replays the 5.4→5.5 and 5.5→5.6 backfills, but that doesn't replace being on 5.6. - Rails 8.1.
spree_core6.0 requiresrails >= 8.1, < 8.2; Ruby ≥ 3.2. Do the Rails bump (and its own deprecations) as a separate commit if you can. - Back up the database and rehearse on a restored copy. Several backfills rewrite money, stock and customer rows; measure how long they take.
- Gemfile swaps:
- remove
spree_adminandspree_storefront(gone — replaced by the React dashboard and the Next.js storefront); - add
spree_dashboard(serves the admin at/dashboard) — skip it for API-only deployments; - add
spree_meilisearchifSpree.search_providerpoints at Meilisearch; - every other
spree_*extension (payments, i18n, your own) needs a 6.0-compatible release — check before you start, orbundle updatewon't resolve.spree_stripe,spree_easypostandspree_meilisearchnow ship from the Spree monorepo in lock-step versions.
- remove
- Action Text / Action Cable.
spree_coreno longer requires them. If your code useshas_rich_text, Action Text helpers, or Action Cable, addrequire 'action_text/engine'/require 'action_cable/engine'toconfig/application.rb. - Widen the rich-text allowlist first, if needed. Descriptions/notes are re-sanitized to what the dashboard editor emits (paragraphs, headings, inline formatting, code, blockquote, lists, links, images). Tables,
div/span, inlinestyleand arbitrary classes are stripped. If you need them, setSpree::RichTextSanitizer.allowed_tags += %w[…]/allowed_attributes += %w[…]in an initializer beforemigrate_rich_text_to_columnsruns and before anything is saved under 6.0. - Recreate staff roles as data. Permission sets are removed with no bridge. A pre-existing role row comes up with no permissions — staff holding it are locked out (fail closed). Seed them before deploying:
Keys arestore.roles.find_or_create_by!(name: 'support').update!(permissions: %w[read_orders read_customers]) # roles belong to a storeread_<resource>/write_<resource>from the catalog (Spree::ApiKey.known_scopeslists them). - Delete PermissionSets initializers.
Spree::PermissionSets::*raisesNameErrorandSpree.permissions.assignraises at boot. Extensions register resources withSpree.permissions.register_scope(:reviews, group: :catalog, resources: -> { [SpreeReviews::Review] }). - Customer migration check (Devise apps).
spree:upgrade:migrate_users_to_customerscopiesspree_usersintospree_customersand maps Devise'sencrypted_passwordtopassword_digest. It only works if no Devise pepper was configured; with Devise no longer loaded it aborts until you passCONFIRM_NO_PEPPER=true. Rows with blank/duplicate emails abort it too (SKIP_INVALID_ROWS=trueto skip). Peppered/SSO/non-bcrypt installs keep a customSpree.customer_classinstead. - Initializer settings that moved to the store (
track_inventory_levels,auto_capture,company, …) are copied bystore_settings_backfill_from_config— leave them in the initializer until that step has run, then remove them. - Enable Active Record encryption. Spree 6 encrypts webhook signing secrets, gateway customer IDs and OAuth identity tokens — only when keys are configured. Make
config/application.rbread them (inside theApplicationclass):
Keys only in credentials, without this, leave webhook secrets and gateway customer IDs plaintext (their%i[primary_key deterministic_key key_derivation_salt].each do |key| value = ENV["ACTIVE_RECORD_ENCRYPTION_#{key.upcase}"].presence || credentials.dig(:active_record_encryption, key).presence config.active_record.encryption[key] = value if value endencryptschecksconfig.active_record.encryption). Generate a separate set per environment (spree encryption initfor dev.env,spree encryption init --print/bin/rails db:encryption:initfor production), set them on the host before deploying, back them up, never change them. Existing plaintext webhook secrets and gateway customer IDs become unreadable once encryption is on: deploy first withconfig.active_record.encryption.support_unencrypted_data = trueandextend_queries = true, run[Spree::WebhookEndpoint, Spree::GatewayCustomer, Spree::UserIdentity].each { |m| m.find_each(&:encrypt) }, then remove both settings. Seespree-security. - Secret API keys minted with
settingslose/admin_users,/invitations,/roles— they now needread_staff/write_staff. Re-mint integration keys that manage staff.
2. Run the upgrade
# create-spree-app (ejected dev stack; Rails app in server/, older projects backend/)
spree upgrade
spree upgrade --plan # after the gem bump: list every step
# classic Rails app / no CLI
bundle update
bundle exec rake spree:install:migrations && bin/rails db:migrate
DRY_RUN=1 bundle exec rake spree:upgrade
CONFIRM_NO_PEPPER=true bundle exec rake spree:upgrade
Production: deploy runs db:migrate, then the release phase runs bundle exec rake spree:upgrade (pass CONFIRM_NO_PEPPER=true there too if applicable). Take a maintenance window — in-flight checkouts are converted into carts by the backfills.
3. What the 5.6 → 6.0 manifest runs (in order)
From spree_core/lib/spree/upgrades/5_6_to_6_0/manifest.yml. All idempotent; re-run one with spree upgrade --step <id>.
| # | Step id | What it does |
|---|---|---|
| 1 | consolidate_metadata | Safety net: merges any leftover public_metadata/private_metadata pairs into the single metadata column |
| 2 | migrate_media_class_names | Rewrites stored Spree::Asset/Spree::Image class names to Spree::Media |
| 3 | migrate_master_images_to_product_media | Moves variant-pinned images onto the product gallery (enqueues jobs — let them drain) |
| 4 | backfill_media_store_ids | Gives every media row a store_id |
| 5 | remove_master_variant | Backfills default_variant_id, retires master variants |
| 6 | migrate_taxons_to_categories_and_collections | Automatic taxons → Spree::Collection; the rest stay Spree::Category; rewrites Spree::Taxon* strings |
| 7 | backfill_library_media_placements | Puts category/collection images into the media library |
| 8 | migrate_users_to_customers | spree_users → spree_customers, repoints user_type columns (see Devise pepper precondition) |
| 9 | migrate_rich_text_to_columns | Action Text bodies → text columns, sanitized |
| 10 | migrate_shipping_to_delivery | Shipment → fulfillment strings/statuses, fulfillment providers, display_on → storefront_visible |
| 11 | migrate_delivery_profiles | Shipping categories → store-owned delivery profiles |
| 12 | migrate_fulfillment_statuses | Remaps to unfulfilled / fulfilled / delivered |
| 13 | migrate_deliveries | Tracking numbers → spree_deliveries rows |
| 14 | migrate_zones_to_delivery_zones | Delivery-referenced zones → Spree::DeliveryZone |
| 15 | migrate_country_state_codes | Fills ISO country_code/state_code columns (countries/states are reference data now) |
| 16 | migrate_promotion_option_value_rules | Option-value promo rules → stable option value ids |
| 17 | migrate_adjustments_to_typed_rows | spree_adjustments → TaxLine / Discount / Fee; flags non-reconciling orders |
| 18 | backfill_reason_store_ids | Store-owns return/refund reasons |
| 19 | migrate_returns | ReturnAuthorization → Spree::Return / Spree::Exchange (aborts on failed rows; SKIP_FAILED_ROWS=true) |
| 20 | migrate_stock_movements_to_typed_rows | Types stock movements; re-allocates open fulfillments (stock now leaves at dispatch) |
| 21 | backfill_delivery_and_stock_store_ids | Store-owns delivery methods and stock locations |
| 22 | migrate_tax_zones | Tax rate zones → country/state on the rate |
| 23 | backfill_tax_store_ids | Store-owns tax rates/categories |
| 24 | backfill_order_coupon_codes | Fills the new spree_orders.coupon_code on historical placed orders |
| 25 | backfill_order_markets | Assigns default market to orders |
| 26 | migrate_incomplete_orders_to_carts | Incomplete orders → Spree::Cart (same token) |
| 27 | migrate_calculator_bounds_to_delivery_method_rules | FlatRate min/max bounds → DeliveryMethodRule |
| 28 | store_settings_backfill_from_config | Copies moved Spree::Config settings onto every store |
| 29 | migrate_capture_methods | auto_capture → capture_method on payment methods |
| 30 | product_types_backfill | Store-owns product types |
| 31 | search_reindex | Rebuilds indexing search providers (Meilisearch); no-op for the DB provider |
| 32 | fold_store_credit_categories | Store credit category → memo |
| 33 | migrate_stripe_webhook_keys | (optional, spree_stripe only) webhook secrets onto the gateway |
| 34 | backfill_import_export_tenancy | Gives imports a store_id (+ seller_id) |
| 35 | backfill_custom_field_definition_stores | Store-owns custom field definitions |
| 36 | package_types | Store default box → package type row |
| 37 | migrate_external_receives_to_purchase_orders | 5.x supplier receives → purchase orders; gives transfers a store/status |
| 38 | backfill_actor_types | Fills the new actor *_type columns (canceler, approver, created_by, …) with the admin user class; until it runs, old rows resolve with a deprecation warning |
| 39 | recount_stock_levels | Fills reserved_count / incoming_count |
Always trust spree upgrade --plan (run after the gem bump) over this table — the gem you install is the source of truth.
Post-check: frozen orders
migrate_adjustments_to_typed_rows never changes money. Orders whose typed sums don't reconcile with stored totals are left untouched and flagged in metadata (the guide says private_metadata — that column is now metadata). Review each:
# spree console
Spree::Order.where.not(completed_at: nil).find_each.select { |o| o.metadata['typed_adjustments_frozen'].present? }
.map { |o| [o.number, o.metadata['typed_adjustments_frozen']] }
Reasons include totals_do_not_reconcile and return-authorization freezes (handled by migrate_returns).
4. Fix your code — grep recipes
Run these over server/app server/config server/lib server/spec (classic apps: app config lib spec), plus apps/storefront, apps/dashboard and any integration code. The full pattern → replacement table is in references/grep-recipes.md. The hits that break boot or checkout first:
cd server # or the app root
grep -rnE "checkout_flow|go_to_state|insert_checkout_step|remove_checkout_step|state_machine|before_transition|after_transition" app config lib
grep -rnE "PermittedAttributes|permitted_[[:alnum:]_]+_attributes" app config lib
grep -rnE "PermissionSets|permissions\.assign|ApiKey::SCOPES" app config lib
grep -rnE "Spree::Adjustment|\.adjustments\b|promo_total|ship_total|item_count" app config lib
grep -rnE "\.master\b|is_master|default_price|DefaultPrice|variant\.price\b|\.price =" app config lib
grep -rnE "finalize!|order\.completed|OrderUpdater|register_update_hook|\bresume!?\b" app config lib
grep -rnE "ReturnAuthorization|CustomerReturn|Reimbursement|ReturnItem" app config lib
grep -rnE "_service\s*=" config
Deprecated-but-working names (Shipment, ShippingMethod, Taxon, metafields, firstname, Spree.user_class, …) log Spree::Deprecation warnings — fix them now; they're removed in 6.1. Run the suite with deprecations visible and treat each warning as a to-do.
5. Behavioral review checklist
These don't raise — they change what happens. Walk through each.
- Completed carts are read-only. Post-checkout edits belong to the
Spree::Order. Anything that mutated an order after completion through cart APIs must move to order-side admin services. -
line_item.ordermay be nil. LineItem, Fulfillment, Payment, TaxLine, Discount, Fee and StockReservation havecart_id+order_id(exactly one set). Use#owner. - Statuses are derived.
payment_status/fulfillment_statusare written only bySpree::Orders::UpdateStatuses; direct assignment is overwritten.payment_state/shipment_state/stateare read aliases until 6.1. Order filter:q[status_eq]=placed. - Placed orders are money-frozen. Recalculation re-sums typed rows; it never regenerates them. Post-placement money edits go through
Spree::Orders::Discounts::*/Orders::Fees::*. - 5.x extensions call Order names on carts. Gateway gems written when a cart was an incomplete order call
promo_total,item_count,shipments,user,available_payment_methods… onSpree::Cart. Those deprecated names now warn and forward on Cart as well as Order until 6.1 (see the grep recipes). Watch the deprecation log and update the gem. - Free gifts don't count toward thresholds. A
create_line_itemsgift no longer counts towarditem_totalrules or the base of a percentage-off order discount, including its own promotion's threshold. "Spend over $100, get a gift" takes the gift back once the paid items fall to $100. - Cancel is final. No
resumefor orders or fulfillments;order.resumed/fulfillment.resumedevents are gone. A fully recalled order readsfulfillment_status: unfulfilled. -
Spree::Store.defaultcan benil. SetSpree::Current.storein jobs, rake tasks, seeds and tests that run outside a request; ensure a default store exists before creating store-scoped records. -
belongs_tois required by default in anything inheritingSpree::Base— including your own models. Addoptional: truewhere blank is legitimate. The error reads "must exist", not "can't be blank" (fix assertions and clients parsing 422details). - Storefront contract.
requirements[]entries carry{ step, field, code, message }; the delivery requirement isfield: 'delivery_method',code: 'delivery_method_required'. Rich-text reads return plain text plus*_html(description_html,internal_note_html) — hydrate editors from*_html. Cartnumbermirrors the prefixedcart_ID for one release. - Webhooks/events. Subscribe to
order.placed(order.completedis a deprecated alias until 6.1).report.*,order.resumed,fulfillment.resumedare removed. New:return.*,exchange.*,claim.*,digital_asset.*,digital_link.downloaded. - Rich-text allowlist narrowed (see preconditions). Content is re-sanitized on next save.
- Digital downloads answer
302to a short-lived signed URL (≤ 1 hour,digital_asset_link_expire_time). Clients must follow redirects. Customers now get a download-links email fromspree_emails— suppress your own if you sent one. - Custom field definitions are store-scoped. Multi-store installs end up with all definitions on the default store; recreate them per store. Product CSV columns use the
custom_field.prefix (wasmetafield.) andproduct_type(wasshipping_category). - Staff API permissions. JWT staff pass the same per-controller gate as secret keys (403 with
details.required_permission). Staff endpoints needread_staff/write_staff. - Store API authorization no longer consults CanCanCan — customer access is ownership scoping plus
Spree::Storefront::AccessPolicy. Move storefrontcanrules into an access-policy subclass or a workflowvalidatehook. - Countries and states are reference data (plain objects from the
countriesgem, no.where/.find). Records name them by ISOcountry_code/state_code. - Stock leaves at dispatch, not at placement (open fulfillments hold an allocation). Inventory reports built on
count_on_handalone will read differently. - Jobs. New projects run Solid Queue inside Puma (Mission Control at
/jobs). An upgraded app keeps its ownconfig.active_job.queue_adapter— Sidekiq keeps working; just move any Spree recurring jobs into whatever scheduler you use. - SDK majors. Bump
@spree/sdkto 2.x in the storefront and@spree/admin-sdkto 1.x in integrations; replace the Rails admin customizations withapps/dashboardplugins (spree add dashboard).
Gotchas
- Don't reorder backfills. Several steps depend on earlier ones (media before master-variant removal; taxons and customers before rich text; adjustments before carts and returns; returns before stock movements). The manifest order is load-bearing.
migrate_users_to_customersaborts by design withoutCONFIRM_NO_PEPPER=trueonce Devise is gone. Confirm no pepper was used rather than forcing it.- Overriding a
*_serviceDI key does nothing now. Legacy writes likeSpree::Dependencies.carts_complete_service = 'MyComplete'are stashed, not applied. Move to the*_workflowkey (subclass the shipped workflow) or, better, aSpree.hooks.registerhandler — see spree-dependencies and spree-workflows. Spree::Product.additional_permitted_attributes << :xraisesFrozenError. Use+= [:x].- Legacy tables stay until 6.1 (
spree_adjustments, return-authorization tables,spree_state_changes,spree_log_entries,spree_users, Action Text rows) as rollback sources. Export anything you want to keep before 6.1.
Where to read further
node_modules/@spree/docs/dist/developer/upgrades/5.6-to-6.0.md— the official guide (https://spreecommerce.org/docs/developer/upgrades/5.6-to-6.0)references/grep-recipes.md— full pattern → replacement table- Related skills: spree-upgrade, spree-workflows, spree-checkout, spree-order-totals, spree-fulfillment, spree-returns, spree-auth-permissions, spree-catalog, spree-pricing, spree-dashboard-plugins, spree-typescript-sdk