Agent Skill · Spree Commerce

spree-data-model

Use when the user is asking how Spree's domain models relate — Orders, LineItems, Variants, Products, Stores, Channels, Markets, Payments, Shipments, Customers, Adjustments. Architecture and relationships only. Common phrasings include "how does X connect to Y", "what's the relationship between", "where does Spree store X", "how do I query orders across stores", "how do channels work", "what's the difference between Cart and Order", "Store vs Channel vs Market". For adding new models / new API resources, use the `spree-resource` skill. For field-level detail, see `docs/developer/core-concepts/` in the installed `@spree/docs` package.

Provider: Spree Commerce Path in repo: skills/spree-data-model/SKILL.md

Skill body

Spree Data Model

A relationship map for the most-asked-about Spree models. Field-level documentation lives in the installed @spree/docs package at node_modules/@spree/docs/dist/developer/core-concepts/.

The catalog → cart pipeline

Product → Variant → LineItem → Order

Variants relate to stock via StockItem (one per Variant per StockLocation) and the StockMovement history.

Master vs default variant

A Product has a master variant (legacy concept, is_master: true) and a computed default_variant method: when Spree::Config[:track_inventory_levels] is on, the first purchasable variant; otherwise the first non-master variant by position; master is only the fallback when the product has no other variants. product.default_variant_id just returns that computed variant’s id. Neither is a database column in 5.5 — don’t query or migrate against default_variant_id (a default_variant_id FK on spree_products is planned for 6.0, implementation not started; see docs/plans/6.0-remove-master-variant.md). Use product.variants for the non-master sellable variants and product.variants_including_master only when you genuinely need the master row included.

The multi-channel / multi-store axis

Store → Channel → ProductPublication → Product

Available since Spree 5.5.

The Store API resolves a channel per request from the X-Spree-Channel header (matched against channels.code or a ch_… prefixed ID); without it the store’s default channel is used. The Admin API does not consume X-Spree-Channel — admin queries return data across all channels for the current store.

Markets (regional config)

Market has_many :countries
Market  columns:  currency (string), default_locale (string)
Order belongs_to :market

A Market is a regional configuration: its set of countries, currency, and default locale. Stores typically get a default Market created automatically (when a default country is known at creation), but markets are optional — check store.has_markets?; currency and locale fall back to store-level defaults when no market exists. Orders are placed in a Market — that’s what controls the currency the customer sees and what tax rules apply.

For full Market documentation see node_modules/@spree/docs/dist/developer/core-concepts/markets.md.

Cart vs Order

In Spree, Spree::Order is both the in-progress cart and the completed transaction. The state column tracks which phase: cart, address, delivery, payment, confirm, complete. Filter on state to distinguish:

Spree::Order.where(state: 'cart')      # in-progress carts
Spree::Order.where(state: 'complete')  # finalized orders
Spree::Order.complete                  # named scope — NOT equivalent: defined as where.not(completed_at: nil), so it matches any order that ever completed checkout, including ones later canceled or returned

Order#token (has_secure_token :token, length: 35) identifies an anonymous cart across requests. Logged-in carts are owned via the user_id FK.

Checkout-side models

Order → Payment → PaymentMethod
Order → Shipment → ShippingRate → ShippingMethod
Order → Address (bill_address, ship_address)

Customer / User

Spree.user_class (typically Spree::User)
  ↓
Address (many, via spree_addresses)
CreditCard (many)
GiftCard (many)
StoreCredit (many)

Use Spree.user_class and Spree.admin_user_class to reference user models — never Spree::User directly. Apps can swap in their own user model via configuration.

Adjustments (polymorphic)

Adjustable (Order, LineItem, Shipment) ← Adjustment

Adjustment is polymorphic — it attaches to any Order, LineItem, or Shipment via adjustable_type + adjustable_id. Each Adjustment has a source (the thing that created it: a TaxRate, PromotionAction, ReturnAuthorization, etc.) and built-in scopes to filter by source type:

order.adjustments.tax                     # source_type: 'Spree::TaxRate'
order.adjustments.promotion               # source_type: 'Spree::PromotionAction'
order.adjustments.return_authorization
order.all_adjustments                     # adjustments on order + its line_items + shipments

Prefixed IDs

Every Spree model exposed via the v3 API has a Stripe-style prefixed ID:

product.prefixed_id  # => "prod_86Rf07xd4z"
order.prefixed_id    # => "or_m3Rp9wXz"
variant.prefixed_id  # => "variant_k5nR8xLq"

IDs are computed from the integer PK via Sqids — no database column. The prefix is declared per-class via has_prefix_id :<prefix> on the model. The v3 API accepts and emits prefixed IDs everywhere; find_by_prefix_id! resolves them back to integer PKs.

Conventions for the prefix:

Never expose raw integer PKs in API responses.

state vs status (mixed on 5.5)

Different models use different column names depending on when they were introduced:

When writing model code, follow the convention of the column the model actually has. When querying, check the model’s source if you’re not sure.

Spree::Current (per-request context)

Avoid passing store / currency / locale around as arguments. Use the ambient context:

Spree::Current.store      # The store handling this request
Spree::Current.currency   # The currency to display prices in
Spree::Current.locale     # The locale for translations
Spree::Current.channel    # The resolved sales channel (falls back to the store's default channel)
Spree::Current.market     # The resolved market (falls back to the store's default market)

Available in models, controllers, jobs, and services. Set automatically by controller before_actions on the API (with built-in fallbacks to store defaults inside Spree::Current); you set it manually in jobs and rake tasks that need to address a specific store.

When to read further