docs/developer/core-concepts/inventory.mdx
import { Since } from '/snippets/since.mdx';
Each Variant has a StockItem that tracks its inventory at a specific location. A variant can have multiple stock items if it's available at multiple stock locations.
When products are sold or returned, individual InventoryUnit records track each unit through the fulfillment process.
Adding new inventory to an out-of-stock product that has backorders will first fill the backorders, then update the available count with the remainder.
During checkout, Spree holds stock with time-limited Stock Reservations to prevent two customers from buying the same last unit simultaneously.
erDiagram
StockLocation {
string name
string admin_name
boolean active
boolean default
boolean backorderable_default
boolean propagate_all_variants
}
StockItem {
integer count_on_hand
boolean backorderable
integer stock_location_id
integer variant_id
}
StockMovement {
integer quantity
string originator_type
integer originator_id
integer stock_item_id
}
StockTransfer {
string number
string reference
integer source_location_id
integer destination_location_id
}
InventoryUnit {
string state
integer variant_id
integer order_id
integer shipment_id
}
StockReservation {
integer quantity
datetime expires_at
integer stock_item_id
integer line_item_id
integer order_id
}
StockLocation ||--o{ StockItem : "has many"
StockLocation ||--o{ StockTransfer : "source"
StockLocation ||--o{ StockTransfer : "destination"
Variant ||--o{ StockItem : "has many"
Variant ||--o{ InventoryUnit : "has many"
StockItem ||--o{ StockMovement : "has many"
StockItem ||--o{ StockReservation : "has many"
StockTransfer ||--o{ StockMovement : "has many"
Order ||--o{ InventoryUnit : "has many"
Order ||--o{ StockReservation : "has many"
Shipment ||--o{ InventoryUnit : "has many"
Key relationships:
count_on_hand) for a specific Variant at a specific Stock LocationStock Locations are the physical locations where your inventory is stored and shipped from.
Stock Locations can be created in the Admin Panel under Settings → Stock Locations, or via the Admin API.
Stock Locations have several attributes that define their properties and behavior within the Spree system. Below is a table outlining these attributes:
| Attribute | Description | Example Value |
|---|---|---|
name | The public name of the stock location. This is returned in Store API | Warehouse 1 |
admin_name | The name used internally for the stock location. This is only returned in Admin API. | WH1 Domestic |
address1 | The primary address line for the stock location. | 5th avenue |
address2 | The secondary address line for the stock location. | Suite 100 |
city | The city where the stock location is based. | New York |
state_id | The ID of the state where the stock location is based. This references the State model. | 1 |
country_id | The ID of the country where the stock location is based. This references the Country model. | 1 |
zipcode | The postal code for the stock location. | 10001 |
phone | The contact phone number for the stock location. | 555-1234 |
active | A boolean indicating whether the stock location is active. Inactive stock locations will not be used in stock calculations or be available for selection during checkout. | true |
default | A boolean indicating whether the stock location is the default one used for new inventory operations. | false |
backorderable_default | A boolean indicating whether new stock items in this location are backorderable by default. | false |
propagate_all_variants | A boolean indicating whether new stock items should be automatically created for all Store variants when a new stock location is added. | false |
Stock Locations can be easily used for tracking warehouses and other physical locations. They can be used to track separate sections of a warehouse (e.g. aisles, shelves, etc.) or to track different warehouses.
You can easily use them with your Point of Sale (POS) system to track inventory at different locations.
Create and manage stock locations via the Admin API:
<CodeGroup>import { createAdminClient } from '@spree/admin-sdk'
const client = createAdminClient({
baseUrl: 'https://store.example.com',
secretKey: 'sk_xxx',
})
const location = await client.stockLocations.create({
name: 'Warehouse 1',
admin_name: 'WH1 Domestic',
default: true,
country_iso: 'US',
propagate_all_variants: true,
})
await client.stockLocations.update('sloc_xxx', { active: false })
spree api post /stock_locations -d '{
"name": "Warehouse 1",
"default": true,
"country_iso": "US",
"propagate_all_variants": true
}'
Stock Items represent the inventory at a stock location for a specific variant. Stock item count on hand can be increased or decreased by creating stock movements.
| Attribute | Description | Example Value |
|---|---|---|
stock_location_id | References the stock location where the stock item belongs. | 1 |
variant_id | References the variant associated with the stock item. | 32 |
count_on_hand | The number of items available on hand. | 150 |
backorderable | Indicates whether the stock item can be backordered. | true |
Stock items are created automatically — for all variants when a location has propagate_all_variants, or via a variant's stock_items on create. To adjust quantity or backorderable status, update the existing stock item via the Admin API. The example below uses a Ransack predicate (stock_location_id_eq) to list a location's stock items before updating one:
// List a location's stock items, then adjust one
const { data: items } = await client.stockItems.list({ stock_location_id_eq: 'sloc_xxx' })
await client.stockItems.update('si_xxx', {
count_on_hand: 150,
backorderable: true,
})
spree api get /stock_items -q stock_location_id_eq=sloc_xxx
spree api patch /stock_items/si_xxx -d '{"count_on_hand": 150, "backorderable": true}'
Stock transfers allow you to move inventory in bulk from one stock location to another stock location. This is handy when you want to integrate with a POS system or other inventory management system. Or you can just rely on Spree being the source of truth for your inventory.
<Info> Stock Transfers can be created in the Admin dashboard or via the Admin API. </Info>Here's the list of attributes for the Stock Transfer model:
| Attribute | Description | Example Value |
|---|---|---|
number | The unique number identifier for the stock transfer, generated automatically. | T123456789 |
reference | An optional reference field for the stock transfer. | Transfer for Event |
source_location_id | The ID of the stock location where the stock is transferred from. | 2 |
destination_location_id | The ID of the stock location where the stock is transferred to. | 3 |
Create a transfer via the Admin API, listing the variants and quantities to move. Omit source_location_id to record an incoming receipt from a vendor:
const transfer = await client.stockTransfers.create({
source_location_id: 'sloc_warehouse',
destination_location_id: 'sloc_store',
reference: 'Transfer for Event',
variants: [
{ variant_id: 'variant_xxx', quantity: 20 },
{ variant_id: 'variant_yyy', quantity: 5 },
],
})
spree api post /stock_transfers -d '{
"source_location_id": "sloc_warehouse",
"destination_location_id": "sloc_store",
"variants": [{ "variant_id": "variant_xxx", "quantity": 20 }]
}'
Stock transfers are crucial for managing inventory across multiple locations, ensuring that stock levels are accurate and up-to-date.
Each Stock Transfer will hold a list of Stock Movements.
Stock Movements track the movement of the inventory:
Here's the list of attributes for the Stock Movement model:
| Attribute | Description | Example Value |
|---|---|---|
stock_item_id | References the stock item that the movement belongs to. | 45 |
quantity | The quantity by which the stock item's count on hand is changed. Positive values indicate stock being added, while negative values indicate stock being removed. | -10 |
originator_type | The type of the originator of the stock movement. This is a polymorphic association. | Spree::Shipment |
originator_id | The ID of the originator of the stock movement. This is used in conjunction with originator_type. | 2 |
Stock Movements are crucial for maintaining accurate inventory levels and for historical tracking of inventory adjustments.
Stock Reservations are a time-limited soft hold on stock during checkout. When a customer enters checkout, Spree holds the items in their cart for a limited time so other shoppers see reduced availability immediately. Two customers can no longer both pass the availability check on the same last unit only to have one of them fail at order completion.
Availability now subtracts the units other customers are holding in active checkouts. Whenever you read whether a variant is in stock — whether for a product page, cart line, or checkout summary — Spree returns the post-reservation number automatically. There's nothing for the storefront to compute and physical stock counts on each StockItem are never modified by reservations; reservations are an independent layer that's consulted at read time and cleaned up by background jobs.
| Trigger | Action |
|---|---|
| Customer enters checkout | A reservation is created for each line item with an expiry timestamp |
| Customer continues mutating the cart while in checkout | The expiry is pushed forward |
| Customer completes the order | The reservation is released; physical stock is decremented as before |
| Customer empties or abandons the cart | The reservation is released or expires automatically |
Reservations attach to each line item; when a line item or order is removed, the reservation goes with it.
| Setting | Default | Purpose |
|---|---|---|
Spree::Config[:stock_reservations_enabled] | true | Global kill switch. When false, reservations are not created and availability ignores them — behavior matches pre-5.5. |
Spree::Config[:default_stock_reservation_ttl_minutes] | 10 | Fallback hold duration when a Store doesn't override. |
store.preferred_stock_reservation_ttl_minutes | 10 | Per-Store override. Falls back to the global default only when explicitly unset/blank. |
TTL is a Store-level setting — it's a checkout-experience policy, not a warehouse property. A multi-location cart never has to merge conflicting TTLs from different warehouses.
When a cart change in checkout would push the order beyond available stock, the change is rejected up front. The customer sees a validation error immediately, instead of progressing through payment only to fail at the final submit.
Abandoned checkouts leave behind expired reservation rows. Spree provides a job to clean them up but does not auto-schedule it — your application's job runner needs to invoke it periodically (every minute is typical). See the 5.4 to 5.5 upgrade guide for sidekiq-cron, solid_queue, and good_job snippets.
If a stock item is marked backorderable, it represents unlimited supply, so reservations are skipped entirely for that item. Availability is unaffected.
As we mentioned above, back-ordered, sold, or shipped products are stored as individual InventoryUnit objects so they can have relevant information attached to them.
We create InventoryUnit objects when:
Here's a list of attributes for the Inventory Unit model:
| Attribute | Description | Example Value |
|---|---|---|
variant_id | References the variant associated with the inventory unit. | 32 |
order_id | References the order associated with the inventory unit. | 123 |
shipment_id | References the shipment associated with the inventory unit. | 77 |
state | The state of the inventory unit | on_hand |
Inventory Units states are:
on_hand - the inventory unit is on handbackordered - the inventory unit is backorderedshipped - the inventory unit is shippedreturned - the inventory unit has been returnedIf you don't need to track inventory, you can disable it:
track_inventory to false on a specific variant via the Admin Panel or Admin APIstockLocations, stockItems, and stockTransfers methods used in the examples abovesk_xxx) used by these calls