Converting this discussion into an issue for further exploration.
Originally posted by tbouliere-datasolution May 6, 2026
Context
Vendure's data model assumes that variant axes are finite, enumerable sets. A variant must be uniquely identified by a tuple of ProductOption references, each tied to a ProductOptionGroup declared on the parent Product. Every value a customer might pick has to exist as a row in product_option.
The validation in ProductVariantService.validateVariantOptionIds enforces three rules:
- The number of submitted
optionIds matches the number of active option groups on the product.
- Each
optionId belongs to exactly one of those groups.
- No existing variant on the same product has the same option-id combination.
Rule (3) implicitly requires every variant to be discriminated by an enumerated tuple. This works well for products with a small, predefined set of choices (Size: S/M/L, Color: Red/Blue). It breaks down for catalogs where the natural variant axes are unbounded or continuous — and where pre-listing every value would either be impossible or grossly inefficient.
Concrete example: cardboard boxes
Consider a folding cardboard box catalog (e.g. RajaPack):
https://www.rajapack.at/kartons-schachteln-container/faltkartons-aus-wellpappe/faltkartons-1-wellig/braune-wellpapp-faltkartons-rajabox-1-wellig-braun-laenge-400-bis-500_OFF_DE_0140.html
A single product line ("brown corrugated folding box, 1-ply, length 400–500 mm") groups dozens of variants:
| Reference |
Length (mm) |
Width (mm) |
Height (mm) |
Pack qty |
| OFF-DE-0140-A |
400 |
300 |
200 |
25 |
| OFF-DE-0140-B |
410 |
305 |
205 |
25 |
| OFF-DE-0140-C |
430 |
310 |
210 |
25 |
| ... |
... |
... |
... |
... |
Every dimension is a continuous axis: any millimetre value is theoretically valid, even though the catalog only sells a discrete sample of them. The set is open-ended — adding a new size means adding a new value, not picking from a fixed list.
Modeling this in Vendure today is awkward:
- Option groups for length / width / height: every numeric value (400, 410, 430, ...) becomes a
ProductOption row with a code and translations. Hundreds of options per dimension exist solely to satisfy the uniqueness constraint. Worse, ProductOptionGroup is channel-scoped and reusable across products — these dummy rows pollute the global option namespace.
- Single hidden "SKU" option group: workable but produces one
ProductOption per variant per product. Same pollution problem, plus the group code (sku) leaks into the admin UI.
- No option groups, reference as identifier: blocked today by the duplicate-combination check (
error.product-variant-options-combination-already-exists).
What the customer actually selects
For boxes, the customer rarely picks "length = 400" from a dropdown. They search by use case (book, parcel, A4) and either:
- Click a variant row in a table (cart adds the variant by id), or
- Use a configurator with sliders / free numeric input that resolves to the closest variant.
In both cases the storefront does not need a discrete option selector. Dimensions are attributes for display, filtering, and search — not selection axes from a finite set.
The semantic gap
Vendure's ProductOptionGroup conflates two distinct concerns:
| Concern |
Cardinality |
Customer interaction |
Examples |
| Selection axis |
discrete, finite (≤ ~10) |
dropdown / radio / chip selector |
Size, Color, Material |
| Distinguishing attribute |
continuous or unbounded |
range filter, numeric input, faceted search |
Length, weight, capacity, voltage |
The current ProductOptionGroup only fits the first. The second has no first-class concept and ends up either:
- Crammed into option groups (causing the cardinality explosion above), or
- Stored as variant custom fields, which Vendure does not consider when computing variant uniqueness.
The constraint we need to relax is not "the SKU should identify a variant" — it is "variant axes must be enumerated up front". Once that is dropped, the SKU (or any per-variant identifier) naturally takes over the discrimination role.
Proposal: free-input option group
A possible additive concept: ProductOptionGroup with inputMode: 'free'.
- The group has a value type (
string, int, float, boolean, length, ...) and an optional unit.
- No
ProductOption rows are created upfront. Each variant stores its value inline (per variant, scoped to the group).
- Variant uniqueness is enforced on the tuple of free-input values, scoped to the product.
- Storefront and dashboard render these as inputs / range filters instead of selectors.
Converting this discussion into an issue for further exploration.
Discussed in #4702
Originally posted by tbouliere-datasolution May 6, 2026
Context
Vendure's data model assumes that variant axes are finite, enumerable sets. A variant must be uniquely identified by a tuple of
ProductOptionreferences, each tied to aProductOptionGroupdeclared on the parentProduct. Every value a customer might pick has to exist as a row inproduct_option.The validation in
ProductVariantService.validateVariantOptionIdsenforces three rules:optionIdsmatches the number of active option groups on the product.optionIdbelongs to exactly one of those groups.Rule (3) implicitly requires every variant to be discriminated by an enumerated tuple. This works well for products with a small, predefined set of choices (Size: S/M/L, Color: Red/Blue). It breaks down for catalogs where the natural variant axes are unbounded or continuous — and where pre-listing every value would either be impossible or grossly inefficient.
Concrete example: cardboard boxes
Consider a folding cardboard box catalog (e.g. RajaPack):
https://www.rajapack.at/kartons-schachteln-container/faltkartons-aus-wellpappe/faltkartons-1-wellig/braune-wellpapp-faltkartons-rajabox-1-wellig-braun-laenge-400-bis-500_OFF_DE_0140.html
A single product line ("brown corrugated folding box, 1-ply, length 400–500 mm") groups dozens of variants:
Every dimension is a continuous axis: any millimetre value is theoretically valid, even though the catalog only sells a discrete sample of them. The set is open-ended — adding a new size means adding a new value, not picking from a fixed list.
Modeling this in Vendure today is awkward:
ProductOptionrow with acodeand translations. Hundreds of options per dimension exist solely to satisfy the uniqueness constraint. Worse,ProductOptionGroupis channel-scoped and reusable across products — these dummy rows pollute the global option namespace.ProductOptionper variant per product. Same pollution problem, plus the group code (sku) leaks into the admin UI.error.product-variant-options-combination-already-exists).What the customer actually selects
For boxes, the customer rarely picks "length = 400" from a dropdown. They search by use case (book, parcel, A4) and either:
In both cases the storefront does not need a discrete option selector. Dimensions are attributes for display, filtering, and search — not selection axes from a finite set.
The semantic gap
Vendure's
ProductOptionGroupconflates two distinct concerns:The current
ProductOptionGrouponly fits the first. The second has no first-class concept and ends up either:The constraint we need to relax is not "the SKU should identify a variant" — it is "variant axes must be enumerated up front". Once that is dropped, the SKU (or any per-variant identifier) naturally takes over the discrimination role.
Proposal: free-input option group
A possible additive concept:
ProductOptionGroupwithinputMode: 'free'.string,int,float,boolean,length, ...) and an optional unit.ProductOptionrows are created upfront. Each variant stores its value inline (per variant, scoped to the group).