Overview
Metadata provides simple, unstructured key-value storage on Spree resources — similar to Stripe’s metadata. It’s ideal for storing integration IDs, tracking data, or any arbitrary information that doesn’t need validation or admin UI. Metadata is a permanent, first-class system in Spree. It is designed to coexist alongside Metafields (structured, typed, admin-managed). The two systems serve different purposes and are not interchangeable — think of it as metadata for machines, metafields for humans. Metadata is write-only in the Store API — you can set it when creating or updating resources, but it is never returned in Store API responses. It is visible in Admin API responses for administrative use.For structured, type-safe custom attributes with admin UI support, use Metafields instead.
Store API
Cart creation
Set metadata when creating a new cart:Adding items
Set metadata when adding items to the cart:Updating items
Metadata is merged with existing values on update. Set a key tonull to remove it.
Updating carts
Admin API
Metadata is readable in Admin API responses on orders and line items:null.
Ruby / Backend
Reading and writing
Every model that includesSpree::Metadata has a metadata accessor. Always use metadata in Ruby code — do not call public_metadata= or private_metadata= directly. (SQL queries still reference the underlying private_metadata column name — see querying examples below.)
Querying
Merge semantics
Metadata updates use merge semantics — existing keys are preserved, new keys are added, and keys set tonull are removed. This matches Stripe’s behavior.
Metadata vs Metafields
Spree has two permanent, complementary systems for custom data. They are not interchangeable and neither is going away.| Metadata | Metafields | |
|---|---|---|
| Purpose | Developer escape hatch — integration data, sync state, ad-hoc flags | Merchant-defined structured attributes with admin UI |
| Schema | Schemaless JSON — no definition required | Defined via MetafieldDefinitions (typed, validated) |
| Validation | None — accepts any JSON-serializable data | Type-specific (text, number, boolean, rich text, JSON) |
| Visibility | Write-only in Store API, readable in Admin API | Configurable (front-end, back-end, both) |
| Admin UI | JSON preview only | Dedicated management forms |
| API pattern | Stripe-style: metadata: { key: value } | Expand-based: ?expand=metafields |
| Queryable | Via JSONB operators (PostgreSQL) | Via SQL joins, Ransack scopes, search providers |
When to use metadata
- Storing external system IDs (e.g., Stripe payment intent ID, ERP order ID)
- Tracking attribution data (UTM parameters, referral source)
- Passing context from the storefront that doesn’t need validation
- Any write-and-forget data that only needs to be read by backend systems
- Syncing state with external integrations (webhooks, ETL pipelines)
When to use metafields
- Custom product specifications shown to customers (material, dimensions, certifications)
- Admin-managed fields with validation and type safety
- Data that needs to appear in the admin UI with dedicated form inputs
- Querying/filtering by custom attributes (search facets, product filtering)
- CSV import/export of structured product data
Supported resources
All models that include theSpree::Metadata concern support metadata. This includes all core models: Orders, Line Items, Products, Variants, Taxons, Payments, Shipments, and more.
The Store API currently supports writing metadata on:
- Carts — on creation and update (
POST /api/v3/store/carts,PATCH /api/v3/store/carts/:id) - Items — on create and update (
POST/PATCH /api/v3/store/carts/:id/items)
Deprecation: public_metadata
Thepublic_metadata column was never exposed in Store API responses and in practice served the same purpose as private_metadata. It will be removed in Spree 6.0 and calling public_metadata= will emit a deprecation warning.
Always use the single metadata accessor for all schemaless key-value storage. If you need data visible to customers on the storefront, use Metafields instead.

