AstroBaaS

Catalogue & product data

Flexible Product Attributes (EAV System)

Free — GPL coresize XLplanned, not built

Generated from docs/plan/core/flexible-product-attributes/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.

The foundational EAV (Entity-Attribute-Value) system that replaces AstroBaaS’s fixed Product TypeScript interface with metadata-driven attributes. Merchants add custom fields (‘lens size’, ‘vintage year’, ‘GPSR warning’) without code redeployment. This is THE core primitive that enables variable products, bundles, attribute sets, faceted search, and all downstream catalog features.

The problem

Product is a fixed TypeScript type. We need ‘manufacturer’ for sunglasses, ‘alcohol content’ for wine, and ‘GPSR safety warnings’ for toys — but adding each one requires a code change and redeploy. We can’t move fast; our merchants are blocked on us for schema changes. We need attributes merchants can create in admin without touching code.

What it does

  • Attribute entity with name, type (string, number, decimal, boolean, select, multiselect, date), is_required, is_filterable, is_sortable, display_order
  • Attribute value assignment per product with version history (old/new value, who changed, when)
  • UI to create, edit, delete attributes (with warning: deleting loses data for all products using it)
  • UI to bulk-assign attributes across products (e.g., set ‘brand = Acme’ for all 20 Acme products at once)
  • Attributes appear in product detail editor alongside standard fields (name, price, images)
  • Attributes are returned in product API response (GET /products/:id includes {attributes: {lens_size: ‘52mm’, uv_protection: true, …}})
  • Attribute filtering in admin product list (show only products where brand=‘Nike’ and size=‘M’)
  • Metadata per attribute: label, help text, placeholder, regex validation for strings, min/max for numbers
  • Attribute grouping: group related attributes (e.g., ‘Dimensions’ group with width, height, depth)
  • Data migration support: import attributes from CSV with mapping (e.g., legacy ‘product_type’ column → new ‘product_family’ attribute)

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.

  • Swatch images for select attributes — media module owns image storage and display. Attributes reference image URLs; they don’t store image files.
  • Localized attribute labels — i18n module owns multi-language labels. Attributes store in one language; i18n wraps display.
  • Attribute inheritance from parent products — that is hierarchical products (bundle/variable parents), a separate feature. This is flat attribute storage.
  • Audit trail per attribute change (who, when, why) — that is the audit log module. Attributes store version history; audit log owns the ‘why’ context.
  • Real-time attribute search indexing — search module owns full-text and facet indexing. Attributes are queryable via filter API; advanced search is search module.
  • Role-based attribute visibility — that is RBAC, a separate feature. Attributes are visible to all admins; RBAC wraps access.

Data model

New ATTRIBUTE table (id, name, type, is_required, is_filterable, is_sortable, group_id, metadata JSON). New PRODUCT_ATTRIBUTE table (product_id, attribute_id, value JSON). New PRODUCT_ATTRIBUTE_VERSION table (product_id, attribute_id, value JSON, version, changed_by, changed_at). No migration required; new tables only. PRODUCT table is unchanged; attributes are stored separately (normalized EAV pattern).

API

  • POST /admin/attributes (create attribute with type, metadata)
  • GET /admin/attributes (list all attributes)
  • PATCH /admin/attributes/:id (update attribute metadata)
  • DELETE /admin/attributes/:id (delete attribute, warn if data exists)
  • POST /admin/products/:id/attributes (set attribute values for product)
  • GET /products/:id (response includes attributes: {lens_size: ‘52mm’, …})
  • GET /admin/products (filter by attributes: ?attributes[brand]=Nike&attributes[size]=M)

Every route added here must also appear in src/pages/openapi.json.ts — a test fails the build if it does not.

Admin

Attributes page with list of all attributes. Create attribute modal with type picker (string, number, select, etc.), metadata editor (label, help, placeholder, regex). Product detail page shows Attributes section below standard fields; edit inline or bulk-assign via dialog. Product list filter: add filter chip ‘Brand = Nike’, see matching products. Bulk edit dialog: select products, assign attribute values to all.

The seam — why this is core

Core owns attribute schema, storage, versioning, and API. Core does not own attribute display swatch images (media module), localized labels (i18n module), or search indexing (search module). Paid modules consume attributes; core owns the interface.

This is THE foundational primitive. Replaces fixed TypeScript interface with metadata-driven attributes. Enables variants, faceting, rules, and sets. Without this, every custom field requires code.

Dependencies

  • Product model (core, existing)
  • Admin UI framework (core, existing)
  • API routing (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.

  • Attribute ‘lens_size’ (type: string) is created; merchant can edit it in product detail
  • Product A gets attribute lens_size = ‘52mm’; Product B gets ‘56mm’; GET /products/A returns attributes: {lens_size: ‘52mm’}
  • Attribute ‘is_polarized’ (type: boolean) is created; merchant can toggle true/false in product detail
  • Bulk assign: merchant selects 20 products, assigns brand=‘Acme’ to all; all 20 update simultaneously
  • Product list filters by attribute: show only products where brand=‘Nike’ — results include only matching products
  • Attribute version history shows lens_size changed from ‘50mm’ → ‘52mm’ on 2024-01-15 by admin@shop.com
  • Deleting attribute ‘lens_size’ shows warning: ‘15 products use this attribute; deleting will remove these values.’
  • Attribute with regex validation (e.g., ’^[A-Z]{3}$’ for SKU prefix) rejects invalid input on save

Risks

If attributes are not versioned, change history is lost (audit and debugging becomes impossible). Mitigate: immutable version history on every value change. If attribute deletion is not warned, merchants accidentally lose data. Mitigate: show product count using attribute, require confirmation. If attribute values are stored as strings without type coercion, numeric filters fail. Mitigate: store type-safe JSON values (numbers as numbers, booleans as booleans). If no regex/validation exists, garbage data corrupts faceted search. Mitigate: per-attribute validation rules applied on save.

Commercial context

Suggested priceFree; core platform primitive
Rival anchorMagento Open Source EAV attribute system; free

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.