Catalogue & product data
Variable Products (First-Class)
Generated from docs/plan/core/variable-products/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.
A first-class product model where variants (sizes, colors) are stored as independent products with their own SKU, inventory count, URL, and pricing. Currently variants are embedded sub-records; this elevates them to top-level products so each variant can have its own page, stock tracking, and analytics. Essential for ecommerce: when size L sells out, size M should remain discoverable and purchasable.
The problem
When I sell out of size L, the whole product disappears from search results because I don’t have separate SKUs per size. I need each size to be a standalone product with its own stock, URL, and availability — but they should show as one product to customers when browsing, and link together on the detail page.
What it does
- Product.variant_type field: ‘none’ (simple product), ‘configurable’ (has variants), or ‘child’ (is a variant of a parent)
- Configurable products have a ‘variant_attributes’ array: which attributes define variants (e.g., size, color)
- Child products (variants) have parent_product_id, inherit brand/category/description from parent, have own SKU/price/inventory
- Inventory is tracked per variant (child product): when size L is out of stock, size M is still buyable
- Product detail page shows variant selector (size picker, color picker) and updates price/inventory/SKU as user selects variant
- URL structure: /products/parent-id (shows parent + variant picker) or /products/variant-sku (SEO-friendly variant-specific URL)
- Admin product form: ‘Create Variants’ button generates child products from attribute combinations (5 sizes × 3 colors = 15 variants auto-created)
- Bulk variant operations: bulk-assign price premium per size (size XL = price + €5), bulk-update inventory for all sizes
- Variant can be hidden individually (e.g., hide L even though parent is live) without hiding parent product
- Analytics: report variant popularity (which sizes sell best), variant-level revenue attribution
What it deliberately does NOT do
Each boundary carries its reason. A boundary without a reason gets crossed by the next person who reads this.
- Variant-specific images or swatches — media module owns image management; variants reference images by URL, do not store images.
- Automatic price/inventory sync from supplier APIs — that is inventory sync module. Variants store SKU; sync module populates inventory.
- Rules for auto-generating variants — that is data enrichment module. Admin manually creates variants or uses bulk import.
- Variant grouping beyond one parent — this is one-level hierarchy only (parent ← variants). Multi-level product hierarchies are a separate feature.
- Variant pre-orders or backorder logic — that is fulfillment module. Variants store inventory; fulfillment owns backorder state.
- Variant-specific tax or shipping rules — that is tax/shipping module. Variants are products; tax/shipping module applies standard rules per product type.
Data model
New field on PRODUCT: variant_type (enum: ‘none’ | ‘configurable’ | ‘child’), parent_product_id (nullable), variant_attributes (JSON array). New PRODUCT_VARIANT_COMBINATION table (parent_id, child_id, attribute_values JSON, display_order). Migration required: backfill variant_type=‘none’ for all existing products.
API
- POST /admin/products/:id/variants/create (generate variants from attribute combinations)
- GET /admin/products/:id/variants (list variants of a parent, with inventory + price)
- PATCH /admin/products/:id/variants/:variantId (update variant price, SKU, inventory, hide status)
- POST /admin/products/:id/variants/bulk-update (apply price or inventory delta to all variants)
- GET /products/:id (variant_type, parent info if child, variant_attributes)
- GET /products/:id/variants (if configurable: list available variants with prices and inventory)
Every route added here must also appear in src/pages/openapi.json.ts — a test
fails the build if it does not.
Admin
Product detail shows ‘Variants’ section. If product is configurable, ‘Generate Variants’ button creates variants from selected attributes (5 sizes × 3 colors = 15). Variants list shows variant ID, selected attribute values (size=L, color=Red), SKU, price, inventory, and hide toggle. Bulk update dialog: apply price offset (size XL = +€5) or inventory adjustment (all variants -10 units) to all at once.
The seam — why this is core
Core owns variant schema, parent-child relationships, inventory tracking per variant, and URL routing. Core does not own automatic variant generation from supplier data (that is inventory module) or variant images/swatches (that is media module). Core owns the interface; paid modules consume it.
Currently variants are embedded sub-records. Magento makes them first-class products with own SKU, inventory, URLs, and reporting. This is table stakes for modern ecommerce.
Dependencies
- flexible-product-attributes (must exist first; variants use attributes)
- Inventory management (core, existing)
- Product routing/URLs (core, existing)
Acceptance checks
Each of these must be able to fail. Before claiming this is done, break the code deliberately and watch each one go red.
- Configurable product ‘T-Shirt’ with variant_attributes [size, color] is created; ‘Generate Variants’ creates 3×2=6 variants auto-generated
- Each variant has own SKU (e.g., TSH-S-BLU, TSH-M-BLU, TSH-L-BLU) assigned automatically or manually
- Variant size L is out of stock (inventory_count=0); size M is in stock (inventory_count=50). Product detail page shows ‘Size L Sold Out’ but Size M is selectable.
- Variant-specific URL /products/TSH-L-RED redirects to parent /products/t-shirt with size=L&color=RED pre-selected
- Bulk update ‘all variants of T-Shirt get -5 inventory’ reduces all variants by 5 simultaneously
- Variant is hidden (hide=true) but parent product is still visible; variant doesn’t appear in variant picker
- Analytics report shows variant popularity: ‘50 units of size L, 100 units of size M, 25 units of size XL sold this month’
- Child product (variant) inherits parent’s description, category, brand; child can override price and inventory only
Risks
If variant inventory is not tracked separately, overselling occurs (L and M share inventory pool). Mitigate: enforce inventory per variant, not parent. If parent is deleted, orphaned variants remain (dangling references). Mitigate: cascade delete or auto-reassign to ‘discontinued’ parent. If variant URLs are not SEO-friendly, ranking is lost. Mitigate: use variant SKU in URL slug, ensure variant URLs are canonical. If variant generation is not atomic, half-created variants corrupt data. Mitigate: transaction wrapper, rollback on error.
Commercial context
| Suggested price | Free; core storefront feature |
| Rival anchor | Magento Open Source configurable products; Shopify variants |
The anchor is what the nearest equivalent charges on Shopify or Magento today. It is context for a pricing decision, not the decision.
Generated from the commerce plan. See docs/COMMERCE-PLAN.md for the full
catalogue and ../../AI-GUIDE.md for how to work on this repository.