TGS12 — Product multi-typing, explicit country support, and typed attributes
TLDR: TGS12 introduces product multi-typing via a new productClassifications junction, an isSupported qualifier on product-country links, a typed-values overhaul of the attributes system, and inverse labels on the relationship enums.
A product is no longer limited to a single type: each product now holds one or more classifications, with exactly one marked primary. Country support links can now state that a product explicitly excludes a market, not just that it serves one. Attribute types now declare how their values are stored — boolean, free text, or a controlled-vocabulary option — via the new valueTypes enum. And two relationship enums can now be labelled correctly from either direction.
- Added
productClassificationsNew junction giving a product multiple product types: productId, productTypeId, isPrimary. Exactly one classification per product has isPrimary = 1 — use it to resolve a product's primary type. - Deprecated
products.productTypeIdDeprecated in favor of productClassifications. The existing productType relationship will still work for now but do migrate to productClassifications; the column will be removed after a deprecation window. - Added
supportsCountries.isSupportedQualifies each product-country link as a supported market or an explicit exclusion. Previously the presence of a row implied support. Now if it is 1, this means it is supported and if 0 this means the country is excluded. Filter isSupported when listing where a product is available. - Added
valueTypesNew enum table declaring how an attribute type stores its value: Boolean stored in attributes.valueBoolean; Free Text stored in attributes.valueText; Option referenced via attributes.attributeOptionId. Referenced by attributeTypes.valueTypeId. - Added
attributeOptionsNew table of permitted values for option-typed attribute types: attributeTypeId, slug (machine key), name (display label), definition. Options are scoped per attribute type. - Added
attributeTypes.valueTypeIdEach attribute type now declares its value type: Boolean, Free Text, or Option. Determines which value field an attribute record populates. References valueTypes. - Added
attributes.attributeOptionIdReference to the selected controlled-vocabulary option, populated when the attribute type's value type is Option. Joined by new typed storage fields attributes.valueBoolean and attributes.valueText. - Added
rootRelationshipTypes.inverseNameNullable label for the relationship read from the child side. Use name for parent → child and inverseName for child → parent, so the same relationship reads correctly from either end. - Added
assetSupportTypes.inverseNameNullable label for the product → asset direction. The existing name is phrased asset → product (`Supported by`, `Native to`, `Managed by`); use inverseName when rendering the relationship from the product side.
Product types in the UI: one product, many types
A product no longer just one type — it has types. Each product carries one or more classifications, with exactly one marked primary. The primary type is the product's identity; the rest are secondary. Sub-product types are represented this way too: not a separate hierarchy, just additional classifications on the same product.
In practice this changes very little on screen and a lot underneath. Wherever you showed a product's type before, show the primary. Cards, list rows, and headers look exactly as they did. Secondary types appear as supporting chips on detail views, answering "what else does this product do" without competing with the identity.
The payoff is in filtering: a product matches a type filter if any of its classifications match. A non-custodial wallet with secondary On/Off Ramp and Swaps classifications now surfaces for users filtering on ramps or swaps, instead of being invisible because it could only hold one type. Grouping and counting still use the primary only, so each product appears once and totals stay honest.
productClassifications {
id
productTypeId
isPrimary
productType {
id
name
}
}Relationships that read correctly from both ends
A relationship enum used to store only one side's phrasing. assetSupportTypes.name is written asset → product (Supported by, Native to, Managed by), so rendering the same row on a product page produced a label that read backwards. rootRelationshipTypes had the same problem between parent and child profiles.
Both enums now carry a nullable inverseName. Pick the label by the direction you are rendering: name when you are looking from the side the enum was written for, inverseName when you are looking from the other. It is nullable, so fall back to name when it is unset rather than rendering an empty label.
Migration note: products.productTypeId keeps working through the deprecation window — the change is additive. When you migrate, resolve a product's canonical type via productClassifications where isPrimary = 1.