Happy House - Ecommerce Docs
Developer ResourcesProducts

Products Module Overview

What a product is, where it sits in the domain, and the module map for the product domain.

Audience: Everyone — product owners, QA, frontend and backend developers Scope: The product domain, its place beside catalog and blog, and where each concern lives

Products Module - Overview

1. What a product is

A product is the sellable item of the store: a Grey Linen Sofa with a SKU, prices, a lifecycle status, media, SEO metadata, specifications and classification. Every product belongs to exactly one category, may carry a brand and a brand series, and may carry any number of tags.

Products sit on top of the catalog taxonomy built by the catalog module:

  • Category — the tree a product must belong to (one, required).
  • Brand / brand series — optional. A series is verified against the product's own brand, at the database level: a product carrying a series that belongs to a different brand cannot exist.
  • Tag — a flat facet. Tags themselves are owned by catalog (/api/admin/catalog/tags); the membership table lives in the products domain.

A product can also be featured on blog posts.

2. The module map

AreaWhere it livesRoutes
Product admin surfaceapps/api/src/modules/products/admin/17 (/api/admin/products + bulk)
Product storefront surfaceapps/api/src/modules/products/customer/5 (/api/mobile/products, discovery)
Product import/export jobsapps/api/src/modules/products/admin/job/ + workers/5 (/api/admin/products/jobs)
Tags — admin + storefrontapps/api/src/modules/catalog/{admin,customer}/tag/7 + 2 (/api/admin/catalog/tags, /api/mobile/catalog/tags)
Blog ↔ product linkingapps/api/src/modules/blog/admin/post/blog-post-products.service.tspart of blog create/update/detail
Transactional outbox (generic)apps/api/src/modules/outbox/infra — no routes
Money utility (first in repo)apps/api/src/utils/money/money.util.tsinfra

The blog_post_product link table lives in the blog schema (packages/db/src/schema/blog/blog-post-product.schema.ts), so the dependency runs blog → products — the same ownership rule as tags: the entity doing the featuring owns the link. The delete behaviour is asymmetric by design: CASCADE when the post is hard-deleted (blog has no soft delete), RESTRICT on the product side (products are soft-deleted, so their membership rows must survive restore).

The route surface went from 444 to 516 endpoints with zero removals; the authoritative route list is apps/api/test/structure/structure.baseline.json.

3. The three rules that govern everything

  1. Every BullMQ enqueue goes through the outbox — an outbox_events row written in the same transaction as the business change. Direct queue.add() in a request handler is a defect. See outbox.
  2. Money is integer minor units, and there is no tax field. 1000 means NPR 10.00. Tax is already included in MRP and selling price. See backend §8.
  3. Storefront reads inline the published status literal. Every storefront index is partial on status = 'published' AND deleted_at IS NULL, and a bound parameter is proven to make the planner fall back to a sequential scan. Storefront queries never bind the status. See backend §9.

4. Page guide

PageRead it for
Features and flowsActor journeys, capability matrix, lifecycle, business rules and diagrams
BackendArchitecture, data model, lifecycle, filtering, money, jobs, cache, security
APIEvery endpoint, DTO, error code and integration note
OutboxThe enqueue rule every future module author must follow