2026-10-07_core-product-database-buildup

Wine2You — Core Build Plan: The Product Database (PIM) as the Central Piece

Date: 2026-10-07 (prepared 2026-10-06) Status: PLAN ONLY — no build started Author: Hermes (from the Google Notebook: Wine2You — 31 sources, all ready) Sources: Wine2You Architecture (PaaS, Medusa v2 & Temporal) · Wine2You E-Commerce Tech Stack Architecture · API Schema and Taxonomy Research · Programmatic MedusaJS Instance Initialization · Avelyn core concept specification · order-flow-schematic · Wine2You Identity & API-Key Architecture (deployed) Related: docs/plans/identity-auth-architecture-deployed.md, infra/identity/, docs/diagrams/w2y-identity-architecture.png


0. One-paragraph thesis

Wine2You's core is not a storefront — it is the Product Database: a single, aggregated, multi-supplier wine catalogue where each wine carries both commercial facts (who owns it, where it physically sits, what price, how much stock) and descriptive/compliance facts (vintage, ABV, terroir, HS/TARIC code, certifications). The chosen split is Payload CMS v3 = master Wine PIM (rich, technical, compliance, storytelling data) + Medusa v2 = commerce/inventory/pricing ledger (stock, prices, channels, orders), joined by a single medusaProductId back-reference on the Payload wines document. Everything else in the E2E flow — estate onboarding, price lists, curation into shops, flash-trade orders, landed-cost, excise/VAT — is a consumer of this one data model. So the model is mapped first, in one place, before any flow code is written.


1. What "the core" is (scope discipline)

Layer System Owns Not this
Product Database (the core) Payload CMS v3 (apps/payload) Wine master data: identity, terroir, compliance, media, tasting notes Stock numbers, prices, orders
Commerce spine Medusa v2 (apps/medusa) Product/Variant, InventoryItem, StockLocation, PriceList, SalesChannel, Cart, Order Rich wine metadata
Duty/pricing brain packages/duty-engine Landed-cost: customs + excise(ABV,V) + VAT + freight Storage of any master data
Macro-orchestrator Temporal.io Multi-day processes (onboarding, escrow, filings) Millisecond checkout
Storefront Next.js 15 multi-tenant (apps/storefront) Tenant routing, curation display Source of truth
Identity/API keys ZITADEL + OpenFGA + Unkey (deployed) Who may read/write catalogue, per entity Catalogue content

Rule: the Product Database is the only place a wine's facts live. Every other system holds a reference (medusaProductId, sku, entity_id) plus derived/transactional state. If a fact exists in two places, Payload wins for descriptive data, Medusa wins for commercial data.


2. The canonical Product Database model

2.1 Payload CMS wines collection (master PIM)

Source: apps/payload/src/collections/Wines.ts. Field names verbatim:

Group Field Type Notes
Identity title text, required
slug text, required, unique, indexed URL key
estate relationship → estates required; owner entity
medusaProductId text, indexed the join to Medusa
Regulatory vintage number (1900–2099)
isNonVintage checkbox (default false) NV wines
abv number (0–22) drives excise band + LPA
volumeInLiters select 0.75 | 0.375 | 1.5 drives excise + HS line
category select still | sparkling | fortified excise class
hsCode text (default 2204.21.79) TARIC/HS customs code
Terroir (group) country select ISO (FR,IT,ES,DE…)
region text
appellation text AOC/DOCG…
grapeVarieties[] array(variety text, percentage number) blends
Attributes certifications select hasMany organic|biodynamic|natural|vegan|sustainable
Storytelling description richText
aromaProfile / palateProfile text
foodPairings[] array(pairing text)
mainImage / gallery[] upload → media DAM assets
Workflow status select draft | published | archived publish gate

Plus supporting collections: estates (the supplier entity), media (DAM), CuratedShops (curator config, markup, layout — the tenant record).

2.2 The Wine ↔ Medusa link (one field, one direction of truth)

2.3 Commercial model in Medusa v2

Medusa entity Wine2You meaning
Product / ProductVariant one wine (variant = SKU incl. bottle format); carries the mirrored SKU
PriceList the Estate ex-cellars B2B export price P_estate (per currency/region)
SalesChannel one storefront/curator shop; controls which variants are visible
StockLocation one physical cellar/warehouse (per estate or 3PL)
InventoryItem the stock-keeping record, decoupled from the product
InventoryLevel (InventoryItem × StockLocation × Owner/Estate) → quantities

2.4 Distributed inventory & multi-supplier catalogue

Design note: these two schemas (LocationDataSchema, TaxDutiesContextSchema) are the API contract (OpenAPI 3.2), not the DB. Store them as explicit DTOs in packages/types and map to Payload/Medusa tables in application code — this is what lets us refactor storage without breaking partner integrations.


3. Mapping the End-to-End Flow against PIM features

The 7-stage lifecycle (Avelyn core concept + order-flow-schematic) vs. what the PIM must supply at each step:

# E2E stage What the Product Database must provide Tool feature used
1 Order capture & payment Which variants are sellable in this channel; price basis Medusa SalesChannel + PriceList
2 Flash sell-buy-sell (millisecond back-to-back) Atomic lock of stock at the fulfilling StockLocation; buy at P_estate, sell at landed Medusa flash-trade workflow + available_to_promise
3 Document generation ABV, volume, HS code, origin, excise class per line item Payload wines compliance fields + duty-engine
4 Estate pick & pack Physical source location + capability flags StockLocation + LocationDataSchema.capabilities
5 Direct transport Origin geo + destination geo for rate/shipment geo_coordinates, fulfillment_route
6 Parallel compliance LPA, excise band, VAT rate, EPR levy per destination TaxDutiesContextSchema + duty-engine
7 Settlement & payout Settlement split: supplier payout, affiliate commission, platform fee, escrowed taxes quote-calculate settlement_split

Landed cost (the pricing brain):

Landed = P_estate + D_customs + D_excise(ABV, V)
       + VAT_dest × (P_estate + D_customs + D_excise) + Freight + Fees

Implemented once in packages/duty-engine (eu-excise.ts, eu-vat-oss.ts, norway-ddp.ts) and called from Medusa workflows (compute-landed-cost.ts) — never duplicated per storefront.

API contracts the core exposes/consumes:


4. Tooling scorecard — chosen PIM vs. alternatives

Layer Chosen License Why (from notebook research) Rejected
PIM / wine metadata Payload CMS v3 MIT Node/TS-native; complex wine schema in one place; shares stack + @wine2you/types with Medusa; self-hostable Akeneo (OSL restrictions on hosted multi-tenant), Pimcore (GPLv3, PHP → stack split)
Commerce / inventory / pricing Medusa v2 MIT Decouples inventory from products; multi-StockLocation; SalesChannel curation; workflows + compensation Saleor (BSD, webhook-heavy), Vendure (thinner marketplace recipe)
Search Meilisearch / Typesense MIT/Apache-2.0 Sub-50ms faceted wine search —
Orchestration Temporal.io + Medusa sagas MIT Multi-day processes vs ms checkout LangChain (non-deterministic — rejected)
Storefront Next.js 15 + Puck MIT Multi-tenant host routing + visual curation —
Identity / API keys ZITADEL + OpenFGA + Unkey AGPL/Apache/SaaS Already deployed & E2E-proven —

Payload vs. "just use Medusa's product module"? The notebook is explicit: Payload is the optimal PIM because the wine schema (grain blends, appellations, TARIC, awards, bilingual storytelling, DAM) is far richer than commerce product fields, and Payload's draft/published workflow + versioning give a proper publish gate for catalogue changes. Medusa stays the fast query layer (SKU/price/stock); Payload stays the rich layer.


5. Proposed build sequence (for approval — nothing started)

Phase A — Schema & contracts (the "core" proper). ~1 increment

  1. packages/types: author the OpenAPI 3.2 / JSON-Schema contracts for Product, ProductVariant, InventoryItem, LocationDataSchema, TaxDutiesContextSchema, plus GET /v1/inventory/network-snapshot + POST /v1/orders/quote-calculate DTOs.
  2. Payload: stand up apps/payload, implement Wines.ts, Estates.ts, Media.ts, CuratedShops.ts with the field names above; enable drafts/versioning.
  3. Medusa: define the mirrored product/inventory/price/channel schema; declare the medusaProductId link contract.
  4. DB migrations as code (Atlas or Flyway/Liquibase) + expand–contract policy; schema-registry policy for async events.

Phase B — Ingest (single front door). ~1 increment 5. create-estate-product-workflow: one endpoint, atomic split to Payload + Medusa. 6. Three intake paths: Merchant Dashboard (UI), Unified API key (w2y_estate_…), Bulk CSV/Excel importer — all funnel to the one endpoint. 7. Estates can never see two systems / two keys.

Phase C — Commercial wiring. ~1 increment 8. PriceList = P_estate per estate; SalesChannel per curator shop; publishable API keys per channel. 9. duty-engine package + compute-landed-cost.ts; unit-tested tax matrices (EU excise, OSS VAT, Norway DDP).

Phase D — Read & trade paths. ~1 increment 10. Search sync (Medusa → Meilisearch/Typesense) with faceted filters (region, vintage, cert, price). 11. Next.js multi-tenant storefront + curator Puck editing + [tenant] routing via SalesChannel. 12. flash-trade.ts back-to-back order workflow; then Temporal macro-workflows (onboarding, escrow, filings).

Phase E — Integrity & ops (runs across all). 13. Nightly PIM↔Medusa reconciliation report; event schema registry; contract tests in CI.

Sequencing rationale: the model and its contracts are the core — B–D are consumers. Getting Phase A's field names and the medusaProductId join right is what makes curation, flash-trade and compliance cheap later.


6. Hosting (already decided, for context)

Piece Host State
Wine2You core app (Medusa + Payload + storefront + TD engine) Oracle free box wine2you-dev-zrh (2 OCPU/12 GB, dev.wine2you.shop) reserved, nothing deployed
Identity/auth (ZITADEL + OpenFGA + user-mgmt) contabo1 (shared service) LIVE & E2E-verified
API keys Unkey Cloud free tier LIVE (api_wCXvOV7tt)

Temporal server and Meilisearch will need a host decision at Phase D (candidate: contabo1, or scale Oracle).

7. Open decisions for Kris

  1. Payload vs. Medusa-only product module — confirm Payload as master PIM (rec: yes, per notebook).
  2. Build sequence — approve Phase A first (schema/contracts) before any flow code (rec: yes).
  3. Temporal + Meilisearch host — contabo1 now vs. defer to Phase D (rec: decide at Phase D).
  4. Migration tool — Atlas vs. Flyway/Liquibase for schema-as-code (rec: pick one in Phase A).

8. AMENDMENT — Account primitives change (2026-10-07, from Kris)

Change: ZITADEL is now a shared, multi-org instance — Wine2You is a virtual organisation (org) inside it, not a dedicated per-project ZITADEL instance. Per-tenant/per-project isolation is expressed as ZITADEL organisations on one instance, not separate instances.

Consequence for the core build: the "Identity/API keys" spine in §1/§3/§6 now multi-tenants at the org layer. Every entity that maps to a "customer" or "integrator" resolves to a ZITADEL org; OpenFGA relationship tuples must carry the org/tenant scope, and API keys stay entity-scoped (Unkey), gated per org membership.

Live state observed while amending (must be reconciled before wiring):

Pending action (blocked on host confirmation): re-point w2y-user-mgmt from the stale v2.71.0 :18080 instance to the v4.19.4 :18090 instance, targeting the Wine2You org; re-verify the OpenFGA-gated key E2E live; retire the stale w2y-identity stack once clear. §8 supersedes the host/identity rows in §6 for the ZITADEL line.