> ## Documentation Index
> Fetch the complete documentation index at: https://docs.frontic.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Product Models

Frontic treats **products as a first-class concept**, not as generic CRUD records that happen to have a "price" field. When you create a Data Storage with the **Products** schema type, the stack gives you a parent/child product + variant model out of the box — with built-in composites for pricing, options, media, and SEO that every commerce experience needs.

This page covers how that model is shaped, how variants relate to products, and how to work with it across [Data Storages](/data-integration/data-storages), [Detail Blocks](/api-builder/blocks), and [Search Listings](/api-builder/listings).

## Products vs. Content

When you create a [Data Storage](/data-integration/data-storages), you pick a schema type:

<CardGroup cols={2}>
  <Card title="Products" icon="boxes-stacked">
    For sellable things. Products can have **variants**, each variant can
    have its own **options** and **price**, and the stack knows how to
    surface both levels at the API layer.
  </Card>

  <Card title="Content" icon="file-lines">
    For everything else — articles, categories, landing pages, editorial
    content. Flat records without the product/variant structure.
  </Card>
</CardGroup>

Pick **Products** whenever you're modeling something a customer can buy. Pick **Content** for anything else.

## Product and variant

In Frontic's model, the **variant** is the buyable thing — every SKU, every cart line, every price sits at the variant level. The **product** is a virtual grouping around one or more variants, carrying the attributes they share (name, description, brand). Every product has at least one variant; a product with a single variant (a book, a non-configurable item) is still modeled this way.

<Steps>
  <Step title="Variant (buyable)" icon="shirt">
    The actual thing a customer purchases — a **red medium t-shirt**, a
    **size 10 sneaker in black**. Every variant carries its own `options`
    (color, size, material), `price`, and stock. Every buyable item in the
    catalog is a variant.
  </Step>

  <Step title="Product (grouping)" icon="box">
    The shared wrapper around one or more variants. Attributes that stay
    the same across variants — name, description, brand, category — live
    here so you maintain them once instead of duplicating per variant.
  </Step>
</Steps>

Every Products storage automatically includes three variant-level fields you can't remove:

* **`options`** — the list of options that define this variant (e.g. `color: red`, `size: M`)
* **`price`** — the variant's price, typed as the built-in [Price composite](/reference/storage-field-types#composites)
* **`parentKey`** — links this variant to its parent product

Plus the `key` and `active` fields every storage has.

## Sharing fields across variants

When you add custom fields to a Products storage, you decide whether each field lives at the variant level (different per variant) or is **shared across variants** (same for every variant of the product).

A good rule of thumb:

| Lives at variant level                 | Shared across variants           |
| -------------------------------------- | -------------------------------- |
| Price, stock, SKU, barcode             | Product name, description, brand |
| Specific media (variant photos)        | Category, tags                   |
| Variant-specific options (color, size) | SEO title, shared hero image     |
| Availability, weight, dimensions       | Care instructions, warranty text |

The **Shared Across Variants** flag is set per field when you configure the storage. Set it once, and the stack takes care of rolling the right values up to the product or down to the variant automatically.

## Built-in composites

Frontic ships with composite field types designed for commerce. You use these the same way you use primitives — add them to your storage schema, map source data into them via the [Value Composer](/data-integration/value-composer), and they become first-class parts of your API.

<CardGroup cols={2}>
  <Card title="Price" icon="tag">
    Prices with currency information. Automatically filtered by the caller's
    region currency via [Request Context](/api-builder/overview#request-context).
  </Card>

  <Card title="PriceScale" icon="tags">
    Price ranges and scaled pricing — quantity discounts, tiered pricing,
    before/after sale prices.
  </Card>

  <Card title="Option" icon="palette">
    Product options (size, color, material) with values and metadata. The
    stack uses these to build dynamic facets in listings automatically.
  </Card>

  <Card title="Swatch" icon="droplet">
    Color or material swatches with preview images and metadata — for
    variant pickers and product grids.
  </Card>

  <Card title="Media" icon="image">
    Images and assets with captions, thumbnails, and variants. Handles the
    awkward shape commerce media usually comes in.
  </Card>

  <Card title="SEO" icon="magnifying-glass">
    Meta titles, descriptions, keywords — consumed by [URLs, Redirects &
    SEO](/commerce-concepts/urls-seo).
  </Card>

  <Card title="Availability" icon="circle-check">
    Enum for `in_stock`, `out_of_stock`, `preorder`, `backorder`. Google
    Merchant Center-compatible by default.
  </Card>

  <Card title="StockLevel" icon="chart-bar">
    `low`, `medium`, `high` enum for simplified stock display without
    exposing real inventory counts.
  </Card>
</CardGroup>

These aren't bolted on — they're part of the stack. Your blocks and listings can reference any of them directly, and the API Builder knows how to shape them into useful responses.

## Product-level vs. variant-level blocks

When you create a [Detail Block](/api-builder/blocks) connected to a commerce storage, you pick which level the block operates on. The key thing to understand: **shared fields are stable** (one value per product), **variant fields rotate** (one value per variant). A variant block can read the product name, but a product block can't pick one variant's price — it needs a strategy to derive it.

<Tabs>
  <Tab title="Product-level block">
    Returns one record per product. Has access to **shared fields** only (name, description, brand, categories). Variant-specific data is not directly available because the block wouldn't know which variant to choose from.

    To surface variant data from a product block, use the special field types:

    * **Price** — derive a price from the variants using a strategy (`first`, `lowest`, `highest`)
    * **Variant field** — aggregate a single field across all variants (e.g. all available colors, all EANs for search indexing)
    * **Variant list** — embed the full list of variants, each shaped by a variant-level block

    Use product-level blocks for category tiles, search results, and product cards.
  </Tab>

  <Tab title="Variant-level block">
    Returns one record per variant. Has access to **variant fields** (price, options, SKU, stock) *and* **shared fields** — variants inherit everything from the parent product.

    Use variant-level blocks for cart line items, wishlist rows, variant pickers, and any UI that shows a specific buyable item.
  </Tab>
</Tabs>

Most experiences use both: a product-level block for grids and search, a variant-level block for cart and detail views. Both connect to the same storage.

## Request context on products

Every product response is shaped by [Request Context](/api-builder/overview#request-context) before it hits your frontend. For a product storage specifically, that means:

* **Prices** are returned in the caller's currency, based on the resolved region
* **Translatable fields** (name, description, marketing copy) are returned in the resolved locale
* **Scoped fields** (e.g. B2B-only descriptions, wholesale-only specs) are returned only if the caller's scope matches

You don't write any of this logic. You mark fields `Translatable` / `Scoped` / `Shared` in the storage schema, and the stack does the rest.

## Mapping from source systems

When you connect a source system via a [Connector](/connectors/overview), its product data shape almost certainly doesn't match Frontic's clean parent/variant model. Shopify thinks about products one way, commercetools another, an ERP a third way, and a PIM a fourth. A Data Sync is where those source shapes get mapped into the canonical Frontic structure — splitting flat records into parent + variants, combining variant-level fields, normalizing prices into the Price composite, and so on. The per-field reshaping lives in the [Value Composer](/data-integration/value-composer), where Computed fields chain operations to produce the exact structure each composite type expects.

Buddy is good at this step specifically. Point Buddy at a source and describe the target shape, and it'll propose a Data Sync configuration for you to review and approve.

## Related

<CardGroup cols={2}>
  <Card title="Multi-Channel & Multi-Region" icon="globe" href="/commerce-concepts/multi-channel-region">
    How scopes, regions, currencies, and locales turn one product model into
    a multi-market catalog.
  </Card>

  <Card title="URLs, Redirects & SEO" icon="link" href="/commerce-concepts/urls-seo">
    How product pages get their URLs, how URL history is preserved, and how
    SEO metadata flows from storage to response.
  </Card>

  <Card title="Detail Blocks" icon="cube" href="/api-builder/blocks">
    The full Detail Block configuration reference, including variant list
    and variant field options.
  </Card>

  <Card title="Storage Field Types" icon="gif" href="/reference/storage-field-types">
    Reference for all built-in composites and primitives.
  </Card>
</CardGroup>
