Inkress ADRsarchitecture decisions
Overview / Catalog
ADR 0039

Product options and custom data are schemaless JSON frozen onto order lines

AcceptedCatalog

Context

Products need variants (size, colour) and buyer-supplied customization (engraving text, option choices). The commerce-api catalog is Inkress-original, not a Shopify mirror (the Shopify-style checkout UX and discount model live elsewhere — ADR 0005/0012). Over time more than one way to model options accreted.

Decision

The live model represents product options and buyer customization as schemaless JSON and freezes the buyer's selections onto the order line; a normalized variant catalog exists but is dormant.

  • Schemaless option/custom-data. A product's configurable inputs live in a JSON data (data["customer_inputs"]) rather than a typed option/metafield table; the checkout reads them and snapshots the buyer's chosen values onto the order line (composing with the immutable-order model, ADR 0034).
  • A normalized variant catalog is built but unused. A separate, org-shared option catalog — reusable options, N-dimensional variants keyed by a combination hash, plus a product_variant_exclusions table to subtract impossible combinations — is fully modelled (schema + CRUD) but the checkout path does not read it.

Consequences

  • Positive: the live path is flexible — arbitrary per-product inputs with no schema migration — and the order line is a self-contained frozen record of exactly what the buyer selected.
  • Neutral / to revisit: schemaless options are not schema-validated and not queryable as structured data (hard to report "all orders with option X"); the normalized graph that would fix this is dormant, so the system effectively carries parallel option models with checkout on the least-structured one. Promoting the normalized catalog onto the checkout path is a latent, deliberate option.
  • Neutral: the catalog data model is Inkress-original; only the checkout UX and discount semantics mirror Shopify (ADR 0005/0012).

Alternatives considered

  • A typed variant/metafield store as the live model (the normalized graph): built but not adopted at checkout — the schemaless JSON path shipped; the normalized catalog waits as an upgrade rather than being wired in.
  • Shopify-style per-product option1/2/3: rejected — the (dormant) normalized model is org-scoped/reusable and N-dimensional instead of three fixed per-product slots.
← 0038 Inventory is an advisory checkout-time gate, never reserved or decremented 0040 Migrate commerce-api's core from Elixir to TypeScript, incrementally over a shared database →