Back to Spree

Metadata

docs/developer/customization/metadata.mdx

5.4.27.3 KB
Original Source

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.

<Info> For structured, type-safe custom attributes with admin UI support, use [Metafields](/developer/core-concepts/metafields) instead. </Info>

Store API

Cart creation

Set metadata when creating a new cart:

bash
POST /api/v3/store/carts
Content-Type: application/json

{
  "metadata": {
    "source": "mobile_app",
    "campaign": "summer_sale"
  }
}
typescript
// @spree/sdk
const cart = await client.carts.create({
  metadata: { source: 'mobile_app', campaign: 'summer_sale' }
})

Adding items

Set metadata when adding items to the cart:

bash
POST /api/v3/store/carts/:cart_id/items
Content-Type: application/json

{
  "variant_id": "variant_k5nR8xLq",
  "quantity": 1,
  "metadata": {
    "gift_note": "Happy Birthday!",
    "engraving": "J.D."
  }
}
typescript
const order = await client.carts.items.create(cartId, {
  variant_id: 'variant_k5nR8xLq',
  quantity: 1,
  metadata: { gift_note: 'Happy Birthday!' },
})

Updating items

Metadata is merged with existing values on update. Set a key to null to remove it.

bash
PATCH /api/v3/store/carts/:cart_id/items/:id
Content-Type: application/json

{
  "metadata": {
    "engraving": "A.B.",
    "gift_note": null
  }
}

You can update metadata without changing quantity, or update both at once:

typescript
// Metadata only
await client.carts.items.update(cartId, lineItemId, {
  metadata: { engraving: 'A.B.' },
})

// Both quantity and metadata
await client.carts.items.update(cartId, lineItemId, {
  quantity: 3,
  metadata: { gift_note: 'Happy Birthday!' },
})

Updating carts

bash
PATCH /api/v3/store/carts/:id
Content-Type: application/json

{
  "metadata": {
    "utm_source": "google",
    "utm_campaign": "summer_sale"
  }
}
typescript
await client.carts.update(cartId, {
  metadata: { utm_source: 'google' },
}, { spreeToken })

Admin API

Metadata is readable in Admin API responses on orders and line items:

json
{
  "id": "or_m3Rp9wXz",
  "number": "R123456",
  "metadata": {
    "source": "mobile_app",
    "utm_campaign": "summer_sale"
  },
  "items": [
    {
      "id": "li_x8Kp2qWz",
      "metadata": {
        "gift_note": "Happy Birthday!"
      }
    }
  ]
}

When there is no metadata, the field is null.

Ruby / Backend

Reading and writing

Every model that includes Spree::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.)

ruby
order = Spree::Order.find_by!(number: 'R123456')

# Write
order.metadata = { 'source' => 'mobile_app' }
order.save!

# Read
order.metadata['source'] # => "mobile_app"

# Merge
order.metadata = order.metadata.merge('campaign' => 'summer')
order.save!

Querying

ruby
# Find orders with specific metadata value (PostgreSQL)
Spree::Order.where("private_metadata->>'source' = ?", "mobile_app")

# Check for key existence
Spree::Order.where("private_metadata ? 'source'")

Merge semantics

Metadata updates use merge semantics — existing keys are preserved, new keys are added, and keys set to null are removed. This matches Stripe's behavior.

# Initial metadata
{ "source": "mobile_app", "campaign": "summer" }

# Update with
{ "campaign": "winter", "new_key": "value" }

# Result
{ "source": "mobile_app", "campaign": "winter", "new_key": "value" }

Metadata vs Metafields

Spree has two permanent, complementary systems for custom data. They are not interchangeable and neither is going away.

MetadataMetafields
PurposeDeveloper escape hatch — integration data, sync state, ad-hoc flagsMerchant-defined structured attributes with admin UI
SchemaSchemaless JSON — no definition requiredDefined via MetafieldDefinitions (typed, validated)
ValidationNone — accepts any JSON-serializable dataType-specific (text, number, boolean, rich text, JSON)
VisibilityWrite-only in Store API, readable in Admin APIConfigurable (front-end, back-end, both)
Admin UIJSON preview onlyDedicated management forms
API patternStripe-style: metadata: { key: value }Expand-based: ?expand=custom_fields
QueryableVia 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 the Spree::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

<Warning> `public_metadata` is deprecated and will be removed in Spree 6.0. Use `metadata` instead. </Warning>

The public_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.

ruby
# Deprecated — will be removed in 6.0
order.public_metadata = { 'gift_message' => 'Happy Birthday!' }

# Use metadata for internal storage
order.metadata = { 'gift_message' => 'Happy Birthday!' }

# Use metafields for customer-visible structured data
order.set_metafield('custom.gift_message', 'Happy Birthday!')