@happyvertical/smrt-products

Product catalog โ€” the reference template for triple-consumption: npm library, module federation, and standalone REST API server.

v0.29.34CatalogTriple-Consumption Template

Overview

smrt-products is the SMRT reference template for the triple-consumption pattern. The same codebase is consumed three ways without forking:

  1. NPM library โ€” import models, collections, and helpers directly. The default npm run build emits the published library surface to dist/lib; run npm run build:all only when you also need standalone or federation bundles.
  2. Module federation โ€” runtime component sharing across micro-frontends (still marked experimental โ€” APIs may change).
  3. Standalone API โ€” startServer() (exported from @happyvertical/smrt-products) boots an Express server with auto-generated REST routes and OpenAPI.

Virtual modules generated by the SMRT Vite plugin power the integration: @smrt/client, @smrt/types, @smrt/routes, @smrt/mcp, and @smrt/manifest.

Architecture:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚     smrt-products Module                  โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Models (Decorated with @smrt)            โ”‚
โ”‚  โ€ข Product (STI, specs, tags, price)      โ”‚
โ”‚  โ€ข Category (STI, hierarchical, counts)   โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Virtual Modules (Vite Plugin)            โ”‚
โ”‚  โ€ข @smrt/client (TS client)               โ”‚
โ”‚  โ€ข @smrt/types                            โ”‚
โ”‚  โ€ข @smrt/routes (Express)                 โ”‚
โ”‚  โ€ข @smrt/mcp                              โ”‚
โ”‚  โ€ข @smrt/manifest                         โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  UI (Svelte 5 Runes)                      โ”‚
โ”‚  โ€ข ProductCard, ProductForm               โ”‚
โ”‚  โ€ข product-store.svelte.ts (main store)   โ”‚
โ”‚  โ€ข product-store.client.svelte.ts         โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Triple Consumption                       โ”‚
โ”‚  โ€ข NPM: import classes/components         โ”‚
โ”‚  โ€ข Federation: runtime sharing (exp.)     โ”‚
โ”‚  โ€ข Standalone: startServer()              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Installation

bash
# Using pnpm (recommended)
pnpm add @happyvertical/smrt-products

# Using npm
npm install @happyvertical/smrt-products

The module depends on @happyvertical/smrt-core for base classes, @happyvertical/ai for AI operations, and @happyvertical/sql for database operations.

Quick Start (5 Minutes)

1. Initialize Product Store

typescript
import { ProductStoreClass } from '@happyvertical/smrt-products';

// Create singleton store instance
const productStore = new ProductStoreClass();

// Load products from API
await productStore.loadProducts();

console.log('Products loaded:', productStore.items.length);
console.log('In stock:', productStore.inStockCount);
console.log('Total value:', productStore.totalValue);

2. Display Product Catalog

svelte
<script lang="ts">
  // Compose your own catalog from the exported building blocks:
  import { ProductCard, ProductForm } from '@happyvertical/smrt-products/components';
  import { ProductStoreClass } from '@happyvertical/smrt-products/stores';

  const store = new ProductStoreClass();
  $effect(() => { store.loadProducts(); });
</script>

<!-- Render search/filter UI of your own, then map over store.items -->
{#each store.items as product (product.id)}
  <ProductCard {product} />
{/each}

3. Create Product with Form

svelte
<script lang="ts">
  import { ProductForm } from '@happyvertical/smrt-products';

  let isSaving = false;

  async function handleSubmit(product) {
    isSaving = true;
    const result = await productStore.createProduct(product);
    if (result.success) {
      console.log('Product created:', result.data);
    } else {
      console.error('Error:', result.error);
    }
    isSaving = false;
  }
</script>

<ProductForm
  onSubmit={handleSubmit}
  loading={isSaving}
/>

<!-- Form includes: name, description, price, category, tags, inStock checkbox -->
<!-- Features: client-side validation, error display, loading state -->

4. Search and Filter Products

typescript
// Search by text (name, description, tags)
const results = productStore.searchProducts('laptop');

// Filter by category
const electronics = productStore.filterByCategory('Electronics');

// Filter in-stock only
const available = productStore.filterInStock();

// Access derived state
console.log('Total products:', productStore.items.length);
console.log('In stock:', productStore.inStockCount);
console.log('Total value:', productStore.totalValue);
console.log('Categories:', productStore.categories);

Core Concepts

Product Model Structure

The Product class extends SmrtObject with comprehensive fields for e-commerce and inventory management:

FieldTypePurposeExample
namestringProduct identifier"USB-C Hub"
descriptionstringDetailed info"7-port hub with PD..."
categorystringCategory name"Electronics"
manufacturerstringMaker/brand"TechCorp"
modelstringModel number"TC-HUB-7P"
pricenumberDecimal price39.99
inStockbooleanAvailabilitytrue
specificationsRecordExtensible attributes{ weight: "1kg", ports: 7 }
tagsstring[]Searchable keywords["usb", "hub", "adapter"]

Category System

Categories form a hierarchical structure for organizing products:

typescript
class Category extends SmrtObject {
  name: string
  description: string
  parentId: string | null       // Self-referencing hierarchy
  level: number                 // Depth in hierarchy (0 = root)
  productCount: number          // Cached count for performance
  active: boolean               // Soft delete flag

  // Methods
  async getProducts(): Promise<Product[]>
  async getSubcategories(): Promise<Category[]>
  async updateProductCount(): Promise<void>
  static async getRootCategories(): Promise<Category[]>
}

Specifications System

Products have an extensible specifications field for storing arbitrary attributes:

typescript
// Store specifications
await product.updateSpecification('weight', '1.2kg');
await product.updateSpecification('warranty', 24); // months
await product.updateSpecification('colors', ['black', 'silver', 'gold']);

// Retrieve specifications
const weight = await product.getSpecification('weight');
const warranty = await product.getSpecification('warranty');

// Example specifications by product type:
// Electronics: weight, dimensions, power, warranty
// Clothing: size, material, care_instructions, colors
// Furniture: dimensions, weight_capacity, assembly_required, materials

Reactive Store (Svelte 5 Runes)

The ProductStoreClass uses Svelte 5's runes for reactive state management:

typescript
export class ProductStoreClass {
  // Reactive state with $state rune
  items = $state<ProductData[]>([]);
  loading = $state(false);
  error = $state<string | null>(null);
  selectedProduct = $state<ProductData | null>(null);

  // Derived state with $derived rune
  inStockCount = $derived(
    this.items.filter(p => p.inStock).length
  );

  totalValue = $derived(
    this.items.reduce((sum, p) => sum + (p.price || 0), 0)
  );

  categories = $derived(
    [...new Set(this.items.map(p => p.category).filter(Boolean))]
  );

  // Actions
  async loadProducts(): Promise<void>
  async createProduct(data): Promise<ApiResponse>
  async updateProduct(id, updates): Promise<ApiResponse>
  async deleteProduct(id): Promise<void>

  // Filters (return derived arrays)
  filterByCategory(category): ProductData[]
  filterInStock(): ProductData[]
  searchProducts(query): ProductData[]
}

API Reference

Auto-Generated REST APIs

The @smrt decorator automatically generates REST endpoints:

MethodEndpointPurpose
GET/api/v1/productsList all products
POST/api/v1/productsCreate new product
GET/api/v1/products/:idGet single product
PUT/api/v1/products/:idUpdate product
GET/api/v1/categoriesList all categories

TypeScript Client

typescript
import { createClient } from '@happyvertical/smrt-products';

const client = createClient('/api/v1');

// List products
const products = await client.products.list();

// Get single product
const product = await client.products.get(productId);

// Create product
const newProduct = await client.products.create({
  name: 'New Product',
  price: 29.99,
  inStock: true
});

// Update product
const updated = await client.products.update(productId, {
  price: 24.99,
  inStock: false
});

Components API

ProductCard

svelte
<ProductCard
  product={productData}
  onEdit={handleEdit}
  onDelete={handleDelete}
/>

<!-- Props:
  - product: ProductData (required)
  - onEdit?: (product: ProductData) => void
  - onDelete?: (id: string) => void

  Displays: name, manufacturer, model, category, tags
  Actions: Edit/Delete buttons (if handlers provided)
-->

ProductForm

svelte
<ProductForm
  product={existingProduct}
  onSubmit={handleSubmit}
  onCancel={handleCancel}
  loading={isSaving}
/>

<!-- Props:
  - product?: ProductData (optional, for editing)
  - onSubmit: (product: Partial<ProductData>) => void | Promise<void>
  - onCancel?: () => void
  - loading?: boolean

  Fields: name (required), description, price (required, non-negative),
          category, tags (comma-separated), inStock (checkbox)
  Features: client-side validation, error display, loading state
-->

ProductCatalog not exported in v0.29

svelte
<!-- Internal component (not importable from the package) -->
<ProductCatalog
  readonly={false}
  showCreateForm={true}
/>

<!-- Props:
  - readonly?: boolean (default: false)
  - showCreateForm?: boolean (default: false)

  Features:
  - Search bar (filters name, description, tags)
  - Category dropdown filter
  - Stats display (total, in stock, total value)
  - Product grid with ProductCard components
  - Create/Edit/Delete operations
  - Loading and error states
-->

Tutorials

Tutorial 1: Creating and Managing Your Product Catalog (45-60 min)

Build a complete product catalog from scratch:

  • Initialize ProductStore singleton in your app
  • Create first product using ProductForm component
  • Display the catalog by composing ProductCard/ProductForm with ProductStoreClass (ProductCatalog is no longer a published component)
  • Implement add/edit/delete operations
  • Handle error states gracefully
  • Deploy to production with persistence

Tutorial 2: Advanced Product Organization with Categories (45-60 min)

Organize products with hierarchical categories:

  • Set up hierarchical category structure (Electronics > Accessories > Cables)
  • Assign products to categories
  • Display category-filtered product views
  • Implement category-specific landing pages
  • Calculate and display product counts per category
  • Build category navigation menu

Tutorial 3: Product Search and Filtering (30-45 min)

Implement advanced search and filtering:

  • Full-text search across name, description, and tags
  • Category filters (single and multi-select)
  • Availability filter (in-stock only checkbox)
  • Combine multiple filters for precise results
  • Display result counts and stats
  • Clear all filters button

Tutorial 4: Specifications and Product Variants (45-60 min)

Handle product variants using specifications:

  • Model product variants using specifications field
  • Store variant combinations (size, color, material)
  • Display variant selector UI in ProductCard
  • Update inventory per variant
  • Support dynamic specification schema per category
  • Validate specification values before saving

Real-World Examples

Example 1: E-Commerce Product Catalog

typescript
// Initialize store
const store = new ProductStoreClass();
await store.loadProducts();

// Create category
const electronics = await Category.create({
  name: 'Electronics',
  description: 'Electronic devices and accessories',
  level: 0
});

// Create product in category
const hub = await store.createProduct({
  name: 'USB-C Hub',
  description: '7-port hub with 100W Power Delivery',
  category: 'Electronics',
  manufacturer: 'TechCorp',
  model: 'TC-HUB-7P',
  price: 39.99,
  inStock: true,
  tags: ['usb', 'hub', 'adapter', 'usb-c'],
  specifications: {
    ports: 7,
    power_delivery: '100W',
    weight: '120g',
    warranty: 24
  }
});

// Search and filter
const hubs = store.searchProducts('hub');
const available = store.filterInStock();
const electronicsProducts = store.filterByCategory('Electronics');

console.log('Total products:', store.items.length);
console.log('In stock:', store.inStockCount);
console.log('Catalog value:', store.totalValue);

Example 2: Product Inventory Dashboard

svelte
<script lang="ts">
  // Compose the dashboard from the exported building blocks
  // (ProductCatalog is no longer published โ€” see Components API above).
  import { ProductCard } from '@happyvertical/smrt-products/components';
  import { ProductStoreClass } from '@happyvertical/smrt-products/stores';
  import { onMount } from 'svelte';

  const store = new ProductStoreClass();
  onMount(() => store.loadProducts());
</script>

<div class="dashboard">
  <h1>Inventory Management</h1>

  <!-- Your own stats + search/filter UI, then the product grid -->
  <p>Total: {store.items.length} | In stock: {store.inStockCount} | Value: {store.totalValue}</p>

  {#each store.items as product (product.id)}
    <ProductCard {product} />
  {/each}
</div>

<style>
  .dashboard {
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
  }
</style>

Example 3: Admin Product Editor

svelte
<script lang="ts">
  import { ProductForm } from '@happyvertical/smrt-products';
  import { productStore } from './stores/product-store';

  let selectedProduct = productStore.selectedProduct;
  let isSaving = false;

  async function handleSave(productData) {
    isSaving = true;

    try {
      if (selectedProduct?.id) {
        // Update existing
        await productStore.updateProduct(selectedProduct.id, productData);
      } else {
        // Create new
        await productStore.createProduct(productData);
      }
      handleCancel();
    } catch (error) {
      console.error('Save failed:', error);
    } finally {
      isSaving = false;
    }
  }

  function handleCancel() {
    productStore.selectProduct(null);
  }
</script>

<div class="editor">
  <h2>{selectedProduct ? 'Edit' : 'Create'} Product</h2>

  <ProductForm
    product={selectedProduct}
    onSubmit={handleSave}
    onCancel={handleCancel}
    loading={isSaving}
  />
</div>

Example 4: Manufacturer-Based Search

typescript
import { Product } from '@happyvertical/smrt-products';

// Find all products by manufacturer
const techCorpProducts = await Product.findByManufacturer('TechCorp');

console.log('Found', techCorpProducts.length, 'TechCorp products');

// Compare specifications across products
for (const product of techCorpProducts) {
  const warranty = await product.getSpecification('warranty');
  const weight = await product.getSpecification('weight');

  console.log(product.name);
  console.log('  Price:', product.price);
  console.log('  Warranty:', warranty, 'months');
  console.log('  Weight:', weight);
  console.log('  In stock:', product.inStock);
  console.log('---');
}

// Calculate average price
const avgPrice = techCorpProducts.reduce((sum, p) => sum + p.price, 0) / techCorpProducts.length;
console.log('Average TechCorp product price:', avgPrice.toFixed(2));

Example 5: Product Specifications System

typescript
import { Product, ProductCollection } from '@happyvertical/smrt-products';

// Create product with specifications
const laptop = await Product.create({
  name: 'Pro Laptop 15"',
  manufacturer: 'TechCorp',
  model: 'PL-15-2024',
  price: 1299.99,
  inStock: true,
  category: 'Computers',
  specifications: {
    screen_size: '15.6 inches',
    resolution: '1920x1080',
    processor: 'Intel Core i7',
    ram: '16GB',
    storage: '512GB SSD',
    weight: '1.8kg',
    battery_life: '10 hours',
    warranty: 36
  }
});

// Update specifications
await laptop.updateSpecification('ram', '32GB');
await laptop.updateSpecification('storage', '1TB SSD');

// Retrieve specifications
const ram = await laptop.getSpecification('ram');
const processor = await laptop.getSpecification('processor');

console.log('Upgraded RAM:', ram);
console.log('Processor:', processor);

// Search products with specific specification.
// There is no Product.findByCategory(); query a ProductCollection by category
// (the same where-filter pattern findByManufacturer()/findInStock() use).
const products = new ProductCollection({ db });
const allLaptops = await products.list({ where: { category: 'Computers' } });
const highRamLaptops = [];

for (const product of allLaptops) {
  const ram = await product.getSpecification('ram');
  if (ram && parseInt(ram) >= 32) {
    highRamLaptops.push(product);
  }
}

console.log('Laptops with 32GB+ RAM:', highRamLaptops.length);

Integration Patterns

With smrt-core

  • Product and Category extend SmrtObject for ORM/persistence
  • @smrt decorator auto-generates REST APIs, CLI commands, MCP tools
  • Inherits AI methods: is(), do(), describe()
  • Database schema auto-generated from TypeScript types
  • Built-in validation and lifecycle hooks

With smrt-commerce

  • Products feed into shopping cart and checkout
  • Orders reference products via foreign keys
  • Inventory sync with order transactions
  • Price updates trigger commerce events
  • Product availability affects checkout flow

With smrt-assets

  • Product images/videos managed as assets
  • ALT text auto-generated via AI for accessibility
  • Asset versions for product photo updates
  • Asset tagging aligns with product tags
  • Derivatives (thumbnails, webp) generated automatically

With smrt-ledgers

  • Product prices feed into accounting entries
  • Revenue recognized per product/category
  • Cost of goods sold tracking
  • Inventory valuation for balance sheet
  • Tax calculations based on product category

With smrt-tenancy

  • Tenancy is opt-in: Product/Category/ProductAsset are not tenant-scoped in this reference package
  • Add @TenantScoped in your own fork to isolate catalogs per tenant
  • Once scoped: category hierarchies and inventories become per-tenant
  • Tenant-specific pricing and visibility (after opting in)
  • Shared schema, separate data

Module Federation

  • ProductCard exported for consumption in other micro-frontends
  • ProductForm embeddable in dashboards (compose a catalog shell from ProductCard + ProductStoreClass)
  • Product model definitions shared across services
  • ProductStore accessible in federated apps
  • Type safety maintained across boundaries

Best Practices

DOs

  • Use categories for organization (don't rely on tags alone)
  • Cache derived data (inStockCount, totalValue) via store
  • Validate product data before submission (form handles this)
  • Use tags for searchable keywords and filtering
  • Keep specifications schema consistent per product type
  • Lazy-load products for large catalogs (implement pagination)
  • Use the store for reactive state (not direct API calls)
  • Implement pagination for large product lists (1000+ items)
  • Use STI pattern for product variants/types

DON'Ts

  • Don't store large files in specifications (use smrt-assets)
  • Don't create deep category hierarchies (> 5 levels)
  • Don't hard-code prices (use dynamic fields)
  • Don't skip validation on forms
  • Don't duplicate product data across services (use federation)
  • Don't mutate store state directly (use provided actions)
  • Don't fetch all products on page load without pagination
  • Don't leave error states unhandled in UI
  • Don't rely on Object.assign in constructors โ€” assign each property explicitly so decorators see them

Triple-Consumption Pattern

This package is the canonical reference for SMRT's triple-consumption architecture โ€” the same source consumed in three deployment modes:

typescript
// 1. NPM library โ€” import the model and collection
import { Product, ProductCollection } from '@happyvertical/smrt-products';

const products = new ProductCollection({ db });
const inStock = await products.findInStock();

// 2. Module federation (experimental) โ€” share a Svelte component at runtime
import { ProductCard } from '@happyvertical/smrt-products/components';

// 3. Standalone REST server โ€” the package ships a startServer() convenience that
//    boots an Express app (loopback, /api/v1, CORS allowlist) with auto-generated routes.
import { startServer } from '@happyvertical/smrt-products';

const { shutdown } = await startServer();
// Under the hood this calls startRestServer(objects, context, options) from
// @happyvertical/smrt-core โ€” note the 3-arg signature if you wire it up yourself.

Build commands:

  • npm run build โ€” emits the published library surface to dist/lib. This is what npm consumers read.
  • npm run build:all โ€” additionally produces standalone and federation bundles.

Tenancy

Product, Category, and ProductAsset are intentionally not tenant-scoped in this package โ€” the catalog is the reference template, so it deliberately keeps the model surface minimal and lets consumers add @TenantScoped in their own forks where needed. ProductAsset stays in the product_assets table (not the generic asset_associations) because it is a noun-owned asset relationship โ€” see the asset-ownership convention in docs/content/standards.md ยง7.

Common Issues and Troubleshooting

Issue: Products not appearing in catalog

Cause: Store not initialized or loadProducts() not called

Solution: Call loadProducts() in onMount or component initialization.

typescript
onMount(() => {
  productStore.loadProducts();
});

Issue: Form validation errors

Cause: Required fields empty (name, price) or invalid values

Solution: ProductForm component handles validation. Check console for errors. Ensure name is non-empty and price is a positive number.

Issue: Specifications not saving

Cause: Using wrong method or not awaiting promise

Solution: Always await updateSpecification(), pass correct key/value types.

typescript
// Correct
await product.updateSpecification('weight', '1.2kg');

// Wrong: not awaiting
product.updateSpecification('weight', '1.2kg'); // Promise not resolved

Issue: Search not finding results

Cause: Search only looks at name, description, and tags

Solution: Add relevant keywords to tags array for better searchability.

Issue: Category hierarchy confusion

Cause: Not setting parentId correctly or level incorrect

Solution: Use getRootCategories() for level=0. Set parentId for subcategories.

Issue: Performance with large catalogs

Cause: Loading all products at once without pagination

Solution: Implement API pagination. Filter/search client-side for small datasets (<1000 items), server-side for larger.

Issue: Module federation components not loading

Cause: Expose config not exporting components

Solution: Verify federation/expose.config.ts includes ProductCard, ProductForm exports.

Related Modules