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
| Area | Where it lives | Routes |
|---|---|---|
| Product admin surface | apps/api/src/modules/products/admin/ | 17 (/api/admin/products + bulk) |
| Product storefront surface | apps/api/src/modules/products/customer/ | 5 (/api/mobile/products, discovery) |
| Product import/export jobs | apps/api/src/modules/products/admin/job/ + workers/ | 5 (/api/admin/products/jobs) |
| Tags — admin + storefront | apps/api/src/modules/catalog/{admin,customer}/tag/ | 7 + 2 (/api/admin/catalog/tags, /api/mobile/catalog/tags) |
| Blog ↔ product linking | apps/api/src/modules/blog/admin/post/blog-post-products.service.ts | part 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.ts | infra |
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
- Every BullMQ enqueue goes through the outbox — an
outbox_eventsrow written in the same transaction as the business change. Directqueue.add()in a request handler is a defect. See outbox. - Money is integer minor units, and there is no tax field.
1000means NPR 10.00. Tax is already included in MRP and selling price. See backend §8. - Storefront reads inline the
publishedstatus literal. Every storefront index is partial onstatus = '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
| Page | Read it for |
|---|---|
| Features and flows | Actor journeys, capability matrix, lifecycle, business rules and diagrams |
| Backend | Architecture, data model, lifecycle, filtering, money, jobs, cache, security |
| API | Every endpoint, DTO, error code and integration note |
| Outbox | The enqueue rule every future module author must follow |