dist/codex/spree-commerce/skills/spree-catalog/SKILL.md
Build and customize Spree's catalog — Products with Variants and OptionTypes/OptionValues, Taxonomies and Taxons (nested set), Properties, Images via ActiveStorage, multi-currency Prices, the v5.3+ PriceList feature for customer/segment overrides, MeiliSearch faceted search (v5.4+), product archiving/activation, CSV import/export, and SEO. Use when modeling products, customizing the catalog UI, indexing search, or importing inventory.
npx skillsauth add orcaqubits/agentic-commerce-claude-plugins spree-catalogInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Fetch live docs:
spree meilisearch faceted search v5.4 for the current search backend wiring.Spree::Product / Spree::Variant source for column names — they shift between minors.Product — the catalog entity (T-shirt, novel, subscription box). Has a master_variant.Variant — the sellable SKU. Even simple products have one master variant; products with options have additional variants per OptionValue combination.A Variant has:
StockItem per StockLocationPrice per currencyOptionValues defining its position on the option axesOptionType: size
OptionValues: small, medium, large
OptionType: color
OptionValues: red, blue, black
A T-shirt product with [size, color] option types has 3 × 3 = 9 variants (or fewer if some combos are unavailable).
A Taxonomy is a category tree (e.g., "Categories", "Collections"). A Taxon is a node in that tree using acts_as_nested_set (or awesome_nested_set). Products are tagged with many Taxons via Spree::Classification.
Taxonomy: Categories
├── Taxon: Apparel
│ ├── Taxon: Shirts
│ └── Taxon: Pants
└── Taxon: Accessories
Free-form attribute table:
Product → ProductProperty(property: Property("Material"), value: "Cotton")
Properties show up on the storefront's spec table. Distinct from Metafield — properties are user-visible product details; metafields are typically internal custom data.
Spree uses ActiveStorage for images attached to Variants and Products. Variant-level images override product-level for that specific SKU. Image variants (resizing) are computed lazily by ActiveStorage with libvips / ImageMagick.
Price row per (Variant, Currency).PriceList (v5.3+) lets you define an override list scoped to a CustomerGroup, Country, or Store. The pricing resolver picks the right price by precedence: user's CustomerGroup → Store's PriceList → default Price.Activate currencies on the Store; create one Price per Variant per Currency. The storefront and APIs select the right Price based on Store#default_currency or the user's selected currency.
| Backend | Notes | |---------|-------| | MeiliSearch (v5.4+, recommended) | Faceted, typo-tolerant, fast | | Algolia (community gem) | Hosted, mature | | PostgreSQL full-text | Fallback / dev |
The v5.4 spree gem wires MeiliSearch by default when the env vars are present.
| Event | Cause |
|-------|-------|
| product.activate | Product made available |
| product.archive | Product hidden (not deleted — preserves order history) |
| product.out_of_stock | Inventory hit zero |
| product.back_in_stock | Inventory restored |
Use archive instead of destroy — preserves order history.
v5.0+ ships CSV import/export for products, customers, orders. Background-job-driven (Sidekiq), reports errors per row.
product = Spree::Product.create!(
name: 'Classic Tee',
description: '100% cotton',
price: 19.99,
shipping_category: Spree::ShippingCategory.first,
tax_category: Spree::TaxCategory.find_by(name: 'Clothing'),
available_on: Time.current,
stores: [Spree::Store.default]
)
Then add option types, option values, and variants:
size = Spree::OptionType.create!(name: 'tshirt-size', presentation: 'Size')
size.option_values.create!([
{ name: 'small', presentation: 'S' },
{ name: 'medium', presentation: 'M' },
{ name: 'large', presentation: 'L' }
])
product.option_types << size
product.variants.create!(
sku: 'TEE-S',
option_values: [size.option_values.find_by(name: 'small')],
price: 19.99
)
taxon = Spree::Taxon.find_by(name: 'Shirts')
product.taxons << taxon
product.master.price_in('USD').amount # 19.99
product.master.price_in('EUR').display_price # "€18.50"
price = Spree::Pricing::PriceFinder.new(
variant: variant, store: store, user: user, currency: 'USD'
).call
Verify the exact service class name in the version you're on.
MeiliSearch indexing is event-driven — product.updated, product.activate, etc., enqueue background jobs. Force reindex:
bin/rails spree:search:reindex
Verify the exact rake task name in the current release.
bin/rails console
> Spree::ImportCsvJob.perform_later('products', file_path: '/path/to/products.csv')
Or via admin: Products → Import. Verify the job class name in the live code.
In the Next.js storefront, the product page is built from API v3 calls:
// Server component
const product = await spree.products.find(slug, { include: 'variants,images,taxons' });
(Pseudo — verify against @spree/sdk docs.)
product.updated events; throttle the search-indexing subscriber for batch operations.Spree::Cache.clear or version-bumped cache keys.Always verify model relationships and column names against the live spree gem source — catalog modeling evolves more than the order graph.
development
Build with Spree's headless Next.js storefront — the official `spree/storefront` repo (Next.js 16 App Router with Server Actions and Turbopack, React 19 Server Components, Tailwind CSS 4, TypeScript 5, `@spree/sdk`, Sentry), server-only auth (httpOnly JWT cookies + publishable key), MeiliSearch faceted catalog, one-page checkout with Apple/Google Pay/Klarna/Affirm/SEPA, multi-region market routing, GA4 + JSON-LD SEO, and Vercel/Docker deployment. Use when forking or customizing the storefront, or evaluating headless adoption.
tools
Build Spree extensions as Rails engines — gem scaffolding, `bin/rails g spree:extension`, mounting routes/migrations/assets, the modern `prepend` decorator pattern (`*_decorator.rb` with `self.prepended(base)`), generators (`spree:model_decorator`, `spree:controller_decorator`), the four customization surfaces in preference order (Events > Webhooks > Dependencies > Decorators), Spree::Dependencies for swapping service objects, gem release/versioning, and the deprecated Deface engine. Use when building a reusable Spree extension or adding non-trivial customization to an app.
development
Build with Spree's event bus and Webhooks 2.0 — `Spree::Events` publication, `Spree::Subscriber` DSL with `subscribes_to` and `on`, wildcard matching, lifecycle events (`{model}.created/.updated/.deleted` via `publishes_lifecycle_events`), the canonical event catalog (order.*, payment.*, shipment.*, product.*), Webhooks 2.0 endpoints, HMAC-SHA256 signing (`X-Spree-Webhook-Signature`), exponential-backoff retries, and Sidekiq job orchestration. Use when wiring event-driven business logic, building webhook consumers, or replacing ActiveSupport callback chains.
tools
Cross-cutting Spree development patterns — the customization preference hierarchy (Events > Webhooks > Dependencies > Decorators), `Spree::Dependencies` service-object swapping, the `_decorator.rb` + `prepend` + `self.prepended` idiom, idempotent subscribers and webhook receivers, multi-store scoping discipline, prefixed IDs, calculator polymorphism (shipping/promotion/tax share the base), service-object composition with `dry-monads` or simple results, why to avoid `class_eval` reopening and Deface, and Spree-on-Rails idioms (Hotwire/Turbo Stimulus, ActiveStorage, Action Cable, Sidekiq). Use when designing the architecture of a Spree extension or solving cross-cutting concerns.