Skip to main content

Attributes

An Attribute is a single named property that can be attached to a Product — color, gear_ratio, material, weight. Attributes are the lower-level primitive consumed by everything that decorates a product: the storefront filter sidebar, the PDP "Parametry produktu" tab, the Compare page, the Promotion Rule editor, the search index, and the configurable-product variant picker.

Attributes are grouped into Attribute Sets which are then pinned to a Product so the admin editor renders just the fields that family actually needs. This page covers the attribute itself; the set ↔ product wiring lives on the Attribute Sets page.

Anatomy​

Every attribute carries:

FieldPurpose
keyStable, URL-safe identifier — ^[a-z][a-z0-9_]*$, unique platform-wide (case-insensitive). Used in filter URLs, search payloads, and the product JSONB key.
labelRecord<bcp47-tag, string> per-locale label map.
labelDefaultFallback label used when the active storefront / admin locale is missing from label.
valueTypeOne of string, number, boolean, price, date, select, multiselect, enum.
options[]Ordered list of selectable options — only for select / multiselect / enum. See Option lists below.

Plus the behavioural flags and a numeric position:

FlagDefaultConsumed by
isFilterablefalseStorefront filter sidebar
isSearchablefalseSearch indexer (Meilisearch)
isComparablefalseStorefront Compare page
isVariantAxisfalseConfigurable-product variant picker
isRequiredfalseProduct-save validator (only when the attribute is in the assigned Attribute Set)
isPromoRulefalsePromotion Rule criterion picker
isVisibleOnProductPagefalsePDP "Parametry produktu" tab
displayAsSliderfalseStorefront sidebar — renders a range slider; only valid for valueType ∈ ('number','price')
filterPosition0Storefront sidebar sort key (ascending; ties broken alphabetically by the resolved label)

Value types​

TypeStorageNotes
stringstringFree-text.
numbernumberNumeric; supports displayAsSlider for range filters.
booleanbooleanTwo-state.
pricenumberMoney amount — formatted in the active currency / locale. Supports displayAsSlider.
dateISO 8601 date string
selectoption valueSingle choice from options[]. At most one option may carry isDefault = true.
multiselectarray of option valuesMultiple choices from options[]. Any number of options may carry isDefault = true.
enumoption valueSame storage shape as select; renders as a compact pill / segmented control on storefront filters and the PDP rather than a dropdown.

Changing valueType while any product carries a value the new type cannot represent is refused with attribute_type_change_unsafe.

Option lists​

For select / multiselect / enum attributes the option list is authored inline on the attribute editor. Each option carries:

FieldPurpose
valueStable identifier — ^[a-z0-9_-]{1,200}$, unique within the attribute. Encoded into filter URLs and stored on every product that carries that value.
labelRecord<bcp47-tag, string> per-locale label map.
labelDefaultFallback label when the active locale is missing from label.
isDefaultOptional pre-selection on new products. select / enum allow at most one; multiselect allows any number.
sortOrderRender order; ties broken by value ASC.

Option values are immutable while any product still carries them — the operator must migrate dependent values first. Option labels can always be renamed. Deleting an option is refused with 409 option_in_use while any product still carries that value.

Public surface​

Admin routes are gated by catalog:read (list / get) / catalog:write (mutations).

Verb + PathAudiencePurpose
GET /api/v1/admin/catalog/attributesadminList attributes
GET /api/v1/admin/catalog/attributes/by-flag?flag=isPromoRule|isComparable|...adminPicker payload — every attribute carrying the requested flag
GET /api/v1/admin/catalog/attributes/:idOrKeyadminSingle attribute read
POST /api/v1/admin/catalog/attributesadminCreate attribute (accepts the flags + inline options[] for select-style types)
PATCH /api/v1/admin/catalog/attributes/:keyadminUpdate labels and hot-toggle isFilterable / isSearchable / isVariantAxis / isPromoRule / isComparable / isVisibleOnProductPage / isRequired / filterPosition. Re-emits attribute.updated.v1.
DELETE /api/v1/admin/catalog/attributes/:idOrKeyadminDelete; refused with 409 attribute_in_use_by_set while any Attribute Set still references it
GET /api/v1/admin/catalog/attributes/:idOrKey/optionsadminList option rows for select / enum / multiselect attributes
POST /api/v1/admin/catalog/attributes/:idOrKey/optionsadminAppend an option
PATCH /api/v1/admin/catalog/attribute-options/:optionIdadminPatch label / labelDefault / isDefault / sortOrder (option value is immutable)
DELETE /api/v1/admin/catalog/attribute-options/:optionIdadminRemove; refused with 409 option_in_use while any product still carries the value

Filter sidebar reads consume GET /api/v1/catalog/filters (defined on the Catalog page) — that endpoint resolves the currently visible filterable attributes per Sales Channel and orders them by filterPosition.

Errors​

CodeStatusWhen
attribute_key_invalid400key does not match ^[a-z][a-z0-9_]*$
duplicate_key409Attribute key already exists (case-insensitive)
attribute_in_use_by_set409Delete blocked: at least one Attribute Set still references the attribute
attribute_type_change_unsafe409valueType change refused — some product carries a value the new type cannot represent
default_option_ambiguous409More than one option flagged isDefault = true on a select / enum attribute
option_in_use409Delete (or value rename) blocked: a product still carries this option value
attribute_not_found404:idOrKey missing
option_not_found404:optionId missing

Storefront integration​

Filter sidebar​

Category and search pages render a chip per isFilterable attribute that has at least one value across the currently visible products. Chips appear ordered by filterPosition ascending, ties broken alphabetically by the resolved per-locale label. Attributes with no values across the current page are omitted (no empty filter).

PDP "Parametry produktu" tab​

Every PDP carries a Parametry produktu tab that lists every attribute meeting both:

  • the product has a value for the attribute, and
  • the attribute is flagged isVisibleOnProductPage = true.

For select / multiselect / enum values the tab renders the option's per-locale label, not the raw value. The detail payload field is assembled by CatalogQueryService.buildVisibleAttributesProjection().

Search index​

Toggling isSearchable propagates into the Meilisearch indexer's payload on the next refresh cycle. Textual types (string, plus the option labels of select / multiselect / enum) feed the lexical and (when enabled) semantic index; numeric / boolean / price / date types feed range / exact-match filters.

Compare page​

Comparison rows on the storefront Compare page list every attribute flagged isComparable = true for which at least one product in the comparison carries a value. The list is sourced via CatalogQueryService.comparableAttributeKeys().

Variant picker​

Configurable products expose a variant picker whose axes come from the attributes flagged isVariantAxis = true on the product's currently assigned Attribute Set.

Storage​

Since migration 102 an attribute is split between the generic Custom Fields layer that the custom_fields module owns and a catalog-owned extension row. The API shape above is unchanged — the admin surface composes the two back into the legacy form.

custom_field_definitions (owned by custom_fields, rows with entity_type = 'product'):

  • id uuid PK, key (unique per entity type), label jsonb, label_default, value_type (generic six-type set: text | number | boolean | date | select | multiselect), required, sort_order. The legacy eight-value valueType form is derived bijectively from the generic type plus the extension refinements below (enum = select + select_display='pill', price = number + numeric_kind='price', ...).

custom_field_options (owned by custom_fields):

  • Option rows keyed UNIQUE (definition_id, value) with per-locale label, label_default, is_default, sort_order. The catalog-owned attribute_options table (migration 032) was dropped by migration 102 after its rows moved here.

product_attributes (owned by catalog) — the 1:1 extension:

  • id uuid PK (stable — admin API attribute ids survived the migration), custom_field_definition_id uuid NOT NULL UNIQUE FK → custom_field_definitions.id ON DELETE RESTRICT.
  • Boolean flags: is_searchable, is_filterable, is_variant_axis, is_comparable, quick_searchable, is_promo_rule, is_visible_on_product_page, display_as_slider, channel_scoped, language_scoped, mass_editable.
  • filter_position int NOT NULL DEFAULT 0.
  • Presentation refinements: select_display varchar(16) NULL (pill = legacy enum, dropdown = legacy select) and numeric_kind varchar(8) NULL (number | price).
  • The duplicated definition columns (key, label, label_default, value_type, is_required) were dropped by migration 102 — the definition row is the single source of truth for them.

products.attribute_values jsonb carries the per-product map keyed by attribute key; the values themselves have never moved out of it. Values are retained server-side even when the attribute leaves the product's currently assigned Attribute Set — switching back surfaces them again.

All attribute and option mutations flow through the catalog Commands behind /catalog/attributes — the generic Custom Fields admin surface lists product definitions read-only and refuses mutations with 409 host_managed.

Events emitted​

  • attribute.updated.v1 — picked up by the search indexer and bridged to webhook subscribers.

Audit log​

Attribute and option CRUD writes one AuditLogEntry per mutation with stateBefore and stateAfter so the audit page surfaces who flipped which flag.

Cross-module consumers​

Three methods on CatalogQueryService are the documented service ports other modules call — a module never reaches into catalog internals:

  • comparableAttributeKeys(): string[] — the storefront Compare page.
  • promoRuleAttributeKeys(): string[] + getAttributeWithOptions(key) — the Promotion Rule criterion picker and resolver.
  • buildVisibleAttributesProjection() — internal, used by the PDP detail response to assemble the visibleAttributes[] payload.

See also​

  • Attribute Sets — bundling attributes into per-family schemas pinned to a Product.
  • Catalog — the parent module, including the storefront GET /api/v1/catalog/filters endpoint that consumes filterPosition.
  • The search module's Meilisearch indexer, which consumes the isSearchable flag.
  • The promotions module's Promotion Rule editor, which consumes the isPromoRule flag.