Changelog
    TGS
    API
    tgs-012

    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.

    • AddedproductClassificationsNew 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.
    • Deprecatedproducts.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.
    • AddedsupportsCountries.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.
    • AddedvalueTypesNew 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.
    • AddedattributeOptionsNew table of permitted values for option-typed attribute types: attributeTypeId, slug (machine key), name (display label), definition. Options are scoped per attribute type.
    • AddedattributeTypes.valueTypeIdEach attribute type now declares its value type: Boolean, Free Text, or Option. Determines which value field an attribute record populates. References valueTypes.
    • Addedattributes.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.
    • AddedrootRelationshipTypes.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.
    • AddedassetSupportTypes.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.

    GraphQL
    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.