multi_select attributes — can act as variant axes backed by Medusa’s native global product options.
Data models
ProductAttribute
Table product_attribute, ID prefix pattr.
| Field | Type | Notes |
|---|---|---|
name | text | Searchable |
handle | text | Nullable; unique when set; auto-generated for global attributes |
description | text | Nullable |
type | enum | AttributeType; immutable after creation |
is_required | boolean | Default false |
is_filterable | boolean | Default false; filterable attributes are tokenized into the search index |
is_variant_axis | boolean | Default false; only valid for multi_select |
rank | number | Default 0; display ordering |
is_active | boolean | Default true |
product_id | text | Nullable — null means global, set means product-scoped (inline) |
product_option_id | text | Nullable; mirror to the native ProductOption for axis attributes |
created_by | text | Nullable |
metadata | json | Nullable |
ProductAttributeValue
Table product_attribute_value, ID prefix pattrval. Unique on (attribute_id, handle) when the handle is set.
| Field | Type | Notes |
|---|---|---|
name | text | Display label |
handle | text | Nullable; auto-generated for global select attributes |
rank | number | Default 0 |
is_active | boolean | Default true |
product_option_value_id | text | Nullable; mirror to the native ProductOptionValue |
metadata | json | Nullable |
Enums
How each type is stored
| Type | Variant axis? | Backing store |
|---|---|---|
multi_select (axis) | yes | Native Medusa ProductOption + option values; the product links a subset of values |
multi_select / single_select (non-axis) | no | Product ↔ value link rows |
text / unit | no | A value record is created for the entered text, then linked |
toggle | no | Two seeded fixed values (true / false); the matching one is linked |
multi_select attributes may be variant axes. A global axis maps to a shared (is_exclusive: false) product option; an inline, product-scoped axis maps to an exclusive option.
Links
| Link | Purpose |
|---|---|
product_category_attribute | Restricts an attribute to categories (many-to-many) |
product_attribute_value_link | Product ↔ selected values (non-axis selections, and a mirror of axis selections) |
scoped_attributes (read-only) | Product → its product-scoped attributes via product_id |
| Option mirror (read-only) | ProductAttribute.product_option_id → native ProductOption |
| Option-value mirror (read-only) | ProductAttributeValue.product_option_value_id → native ProductOptionValue |
Service
ProductAttributeModuleService extends MedusaService with auto-generated CRUD, plus:
| Method | Behavior |
|---|---|
createProductAttributes(data) | Auto-generates handles for global attributes; seeds true/false values for toggle attributes |
updateProductAttributes(data) | Rejects any attempt to change an attribute’s type |
listProductAttributes / listAndCountProductAttributes | Attach values only for select/toggle types; other types return values: [] |
createProductAttributeValues(data) | Auto-generates value handles for global select attributes |
Attaching attributes to products
Attributes are applied to products through the batch endpoint rather than the module service directly:{ add?, remove?, update? }, applied in the order remove → add → update by the createAndLinkProductAttributesToProductWorkflow. Product create and update payloads also accept a unified attributes[] array.
Product GET responses expose the attribute graph as native options (variant axes), product_attribute_values (each with its parent attribute and, for global selects, all_values), and scoped_attributes.
Related endpoints
GET/POST /admin/product-attributes, value sub-routes — catalog managementGET /vendor/product-attributes— category-filtered read access for sellersGET /store/product-attributes— storefront filter facets