Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ency07/B2B/llms.txt

Use this file to discover all available pages before exploring further.

The industrial product catalog is publicly accessible and organized in a five-level hierarchy. Products carry technical specifications stored as key-value pairs, plus separate collections for images, downloadable documents, CAD files, and SEO metadata. The entire hierarchy is resolved in two database queries and cached at the Next.js layer for 60 seconds.

Category hierarchy

Category
└── Subcategory
    └── Family
        └── Series
            └── Product (specs, images, documents, CAD files, SEO)
Each level has a unique *Code identifier (e.g. categoryCode, seriesCode) suitable for use in URL slugs and external integrations.

Fetching the catalog

getIndustrialCatalog(tenantCode?)

Location: src/web/actions/catalog.ts Returns the full CatalogCategory[] tree for the requested tenant. Internally it:
  1. Runs a single deeply-nested Supabase PostgREST query that joins all five levels together with product_specifications, product_images, product_documents, and product_files.
  2. Runs a second query against seo_metadata for entity_type = 'PRODUCT' and builds an in-memory map keyed by entity_id.
  3. Maps the raw DB rows to the typed CatalogCategory[] structure and attaches SEO data per product.
This approach fetches the entire catalog in 2 DB round-trips regardless of catalog size, avoiding the N+1 query problem common with level-by-level fetches.

TypeScript type hierarchy

interface CatalogCategory {
  id: string;
  categoryCode: string;
  name: string;
  description: string;
  subcategories: CatalogSubcategory[];
}

interface CatalogSubcategory {
  id: string;
  subcategoryCode: string;
  name: string;
  description: string;
  families: CatalogFamily[];
}

interface CatalogFamily {
  id: string;
  familyCode: string;
  name: string;
  description: string;
  series: CatalogSeries[];
}

interface CatalogSeries {
  id: string;
  seriesCode: string;
  name: string;
  description: string;
  products: ProductDetail[];
}

interface ProductDetail {
  id: string;
  productCode: string;
  name: string;
  description: string;
  status: string;
  specifications: Record<string, string>;
  images: ProductMedia[];
  documents: ProductMedia[];
  cadFiles: ProductMedia[];
  seo?: { metaTitle: string; metaDescription: string; metaKeywords: string; slug: string };
}

Media assets

All binary assets (images, documents, CAD files) reference rows in the media_assets table via junction tables (product_images, product_documents, product_files). Each asset is represented by ProductMedia:
interface ProductMedia {
  id: string;
  fileName: string;
  filePath: string;
  fileSize: number;
  mimeType: string;
  altText: string;
}
The filePath is a Supabase Storage path. Construct the public URL with:
supabase.storage.from("product-assets").getPublicUrl(media.filePath).data.publicUrl

Caching

getIndustrialCatalog() wraps fetchRawCatalogFromDB() with Next.js unstable_cache:
unstable_cache(
  async (tenantId: string) => fetchRawCatalogFromDB(tenantId),
  ["industrial-catalog-key"],
  {
    revalidate: 60,          // 60-second TTL
    tags: ["catalog-all"],   // invalidation tag
  }
)
  • TTL: 60 seconds. After expiry, the next request triggers a background revalidation (stale-while-revalidate semantics).
  • Invalidation tag: catalog-all. Call invalidateCatalogCache(tenantCode?) from src/web/actions/catalog-cache.ts to force immediate invalidation via revalidateTag('catalog-all').
After any product or category modification, the catalog cache is automatically invalidated — every admin Server Action (saveProduct, deleteProduct, saveCategory, deleteCategory, addProductImage) calls invalidateCatalogCache() as its final step. Manual invalidation is only needed if you modify the database directly.

Admin operations

All write operations require the items.manage permission, enforced by requireAction("items.manage") before any database mutation. The authenticated user’s tenant access is then verified with validateTenantAccess().

Products

ActionSignatureNotes
Create / update productsaveProduct(tenantCode, product)Replaces all product_specifications rows on update
Soft-delete productdeleteProduct(tenantCode, productId)Sets deleted_at = NOW()
// saveProduct — product shape
{
  id?: string;           // omit to create; provide to update
  productCode: string;
  name: string;
  description: string;
  status: string;        // e.g. "ACTIVO"
  seriesId: string;      // parent series UUID
  specifications: Record<string, string>;
}
saveProduct performs a full replace of specifications: it deletes all existing product_specifications rows for the product and re-inserts the new map. This is a destructive operation — pass the complete specification object every time.

Categories

ActionSignatureNotes
Create / update categorysaveCategory(tenantCode, category)Upserts by id
Soft-delete categorydeleteCategory(tenantCode, categoryId)Sets deleted_at = NOW()
// saveCategory — category shape
{
  id?: string;           // omit to create; provide to update
  categoryCode: string;
  name: string;
  description: string;
}

Media assets

addProductImage(
  tenantCode: string | null,
  productId: string,
  image: {
    fileName: string;
    filePath: string;   // Supabase Storage path (already uploaded)
    fileSize: number;
    mimeType: string;
    altText?: string;
  }
)
addProductImage inserts a row into media_assets and a corresponding row into product_images with a default sort_order of 10. Upload the binary to Supabase Storage first, then call this action to register and link the asset.

Public access

The catalog read path uses supabaseAdmin (service-role key), which bypasses RLS. Tenant isolation is enforced at the query level via .eq("tenant_id", tenantId) — only records belonging to the requested tenant are returned. All write operations use authenticated admin requests and require the items.manage permission.
Anonymous user → getIndustrialCatalog() → supabaseAdmin (read)

                           Tenant isolation: .eq("tenant_id", tenantId)
                           (scoped in query; supabaseAdmin bypasses RLS)

Soft-delete behavior

Deleted products and categories are not physically removed. They receive a deleted_at timestamp and are excluded from all queries via .is("deleted_at", null) filters. This preserves referential integrity for historical orders, leads, and diagnostic reports that reference catalog items. To permanently purge soft-deleted records, run a manual SQL cleanup against the relevant Supabase project — there is no automated hard-delete mechanism.

Database tables

TableDescription
product_categoriesTop-level categories
product_subcategoriesSubcategories, FK → product_categories
product_familiesFamilies, FK → product_subcategories
product_seriesSeries, FK → product_families
productsIndividual products, FK → product_series
product_specificationsKey-value specs, FK → products
product_imagesImage links, FK → products + media_assets
product_documentsDocument links, FK → products + media_assets
product_filesCAD file links, FK → products + media_assets
media_assetsCentral asset registry (path, size, MIME type)
seo_metadataSEO fields keyed by (entity_type, entity_id)

Build docs developers (and LLMs) love