Happy House - Ecommerce Docs
Developer ResourcesCatalog

Catalog Backend Documentation

Backend architecture, data model, services, cache, queues, runtime rules, and operational behavior for the Catalog module.

Catalog - Backend Documentation

1. Documentation Evidence

AreaFiles InspectedVerified Details
Module wiringcatalog.module.ts, catalog-{admin,shared,worker}.module.ts, customer/catalog-customer.module.tsAggregate + leaf composition, shared services
Controllersadmin/{category,brand,brand-series,job}/*.controller.ts, bulk controllers, customer/**/*.controller.tsRoute ownership, guards, status codes, registration order
Servicescatalog-slug.service.ts, catalog-search.service.ts, catalog-category-{write,admin,bulk,tree}.service.ts, catalog-brand*.service.ts, catalog-import-*.ts, catalog-export.service.tsTransactions, locks, slug ordering, all-or-nothing import
DTOsdto/*.ts under each leafValidation and defaults
Schemapackages/db/src/schema/catalog/*.tsCHECKs, partial indexes, slug tables
Jobspackages/jobs/src/index.ts, workers/*Queue contracts, lease claim, sweep
Cachecache-invalidation.tags.tsDomains, patterns, TTL

2. Backend Scope and Boundaries

Owns

  • Category tree (adjacency list + depth, max 6), brand and brand series CRUD, bulk and reorder.
  • Slug ownership for all three entities (current + retired under one unique).
  • Storefront reads under /api/mobile/catalog/* and search over all three entities.
  • Catalog import/export jobs with lease-based claims and cooperative cancellation.
  • The tag entity (flat facet taxonomy).

Does Not Own

  • Product membership (product_tag_link lives in products; tag never references products).
  • The outbox (generic infra), the money representation, the product domain.
  • The movement of tag-related filtering — products consumes tags, catalog does not know products exist.

Source of Truth

ConcernSource of TruthNotes
Runtime statePostgreSQL (category, brand, brand_series, *_slug, catalog_job)
Slug historyOwnership tables — unique across current and retired
Tree legalityDB CHECKs (depth, root consistency) + advisory lock
Job statecatalog_job row + lease claim

3. Module Composition

ModuleTypePathControllersProvidersExportsResponsibility
CatalogModuleAggregatecatalog.module.tsNoneLeaf modulesComposes admin/customer/worker
CatalogAdminModuleAggregatecatalog-admin.module.tsNoneLeaf modulesComposes the four admin leaves
CatalogCategoryAdminModuleLeafadmin/category/CatalogCategoryAdminController, CatalogCategoryBulkControllerServicesServicesCategory surface
CatalogBrandAdminModuleLeafadmin/brand/CatalogBrandAdminController, CatalogBrandBulkControllerServicesServicesBrand surface
CatalogBrandSeriesAdminModuleLeafadmin/brand-series/+ bulk controllerServicesServicesSeries surface
CatalogJobAdminModuleLeafadmin/job/CatalogJobAdminControllerServiceJobs surface
CatalogCustomerModuleAggregatecustomer/NoneLeavesComposes the storefront leaves
CatalogCategoryTreeCustomerModuleLeafcustomer/category-tree/CatalogCategoryTreeCustomerControllerCatalogCategoryTreeCustomerServiceServiceStorefront navigation tree, one statement
CatalogSharedModuleLeafshared/NoneCatalogSlugService, CatalogSearchServiceBothShared domain services
CatalogWorkerModuleLeafcatalog-worker.module.tsNoneProcessors/handlersImport/export + sweep

Bulk controllers are registered before their :publicId siblings — both mount the same base path and Nest matches in registration order, so bulk/reorder never get swallowed as UUIDs (verified live: unauthenticated bulk → 401, not 400).

4. File and Directory Map

apps/api/src/modules/catalog/
  catalog.module.ts
  catalog-admin.module.ts
  catalog-shared.module.ts
  catalog-worker.module.ts
  admin/
    category/   catalog-category-{admin,bulk,write,tree}.service.ts + controllers + dto/
    brand/      catalog-brand-{admin,bulk}.service.ts + controllers + dto/
    brand-series/ catalog-brand-series-{admin,bulk}.service.ts + controllers + dto/
    job/        catalog-job-admin.{controller,service}.ts + dto/
  customer/
    category/  category-tree/  brand/  brand-series/  search/  tag/
  shared/
    catalog-slug.service.ts          # slug generation, ownership, rename, resolve
    catalog-search.service.ts        # pg_trgm search
    catalog.constants.ts             # lock key, caps, TTLs
    catalog-bulk.util.ts             # dedupeAndCapBulkIds
    catalog-activity-actions.ts      # typed activity literals
    catalog-db-executor.type.ts
  import-export/
    catalog-import-{parser,row,service}.ts
    catalog-export.service.ts
  workers/
    catalog-import.processor.ts   catalog-export.processor.ts
    catalog-job-state.ts          catalog-job-sweep.{processor,scheduler}.ts

Key files:

FilePurposeKey ExportsNotes
shared/catalog-slug.service.tsSlug ownership for all three entitiesCatalogSlugServiceRename is demote-then-promote, one transaction
shared/catalog-search.service.tsShared trigram searchCatalogSearchService% bounds, similarity() ranks
shared/catalog.constants.tsTunables with why-commentsCATALOG_TREE_LOCK_KEY, caps
admin/category/catalog-category-tree.service.tsTree mechanicsCatalogCategoryTreeServiceLock, cycle checks, one-statement move
customer/category-tree/catalog-category-tree-customer.service.tsStorefront navigation treeCatalogCategoryTreeCustomerServiceONE UNION ALL statement so roots and children come from one snapshot; caps enforced in SQL, never a silent truncation
import-export/catalog-import.service.tsImport orchestrationCatalogImportServiceAll-or-nothing, terminal write in same tx
workers/catalog-job-state.tsJob transitionsclaimCatalogJob, completeImportJob, …Zero-row guard throws

5. Data Model

5.1 Schema Source

packages/db/src/schema/catalog/
  category.ts  brand.ts  brand-series.ts  catalog-job.ts  enums.ts  tag.ts

5.2 Tables

TablePurposeKey points
categoryTree nodeparent_id + depth; 6 CHECKs; 8 indexes incl. trgm
category_slugSlug ownershipUNIQUE(slug) across current+retired; partial UNIQUE WHERE is_current; RESTRICT
brandFlat listCHECKs: order non-negative, alt-requires-url; trgm indexes
brand_slugSlug ownershipsame shape as category_slug
brand_seriesSeries under brandbrand_id FK RESTRICT; trgm indexes
brand_series_slugSlug ownershipsame shape
catalog_jobJob rows6 CHECKs; status enum; lease claim
tagFlat facetslug unique among live rows only; no history table

5.3 Constraint Table

ConstraintTableWhat it prevents
chk_category_depth_within_capcategorydepth outside 0..6
chk_category_depth_matches_parentcategoryparent row claiming root depth
chk_category_display_order_non_negativecategorynegative order
chk_category_no_self_parentcategoryself-parenting
chk_category_image_alt_requires_urlcategoryalt without url
chk_*_slug_retired_consistentslug tablesis_current=false without retired_at
chk_*_slug_formatslug tablesnon-route-safe slug
uq_*_slug_current (partial unique)slug tablestwo current slugs per owner
chk_catalog_job_row_counts_sanecatalog_jobfailed ≤ processed ≤ total
chk_catalog_job_total_rows_knowncatalog_jobcompleted without total_rows
chk_catalog_job_terminal_has_finished_atcatalog_jobterminal status without finished_at
chk_catalog_job_timestamps_orderedcatalog_jobfinished_at < started_at
chk_catalog_job_source_file_matches_kindcatalog_jobimport without source file
chk_catalog_job_result_matches_statuscatalog_jobresult URL except on completed export
all FKsallON DELETE RESTRICT — hard delete unsupported

5.4 Relationship Diagram

6. Services and Responsibilities

6.1 CatalogSlugService

MethodCalled ByReadsWritesSide EffectsErrors
generateUniqueSlug()create/update pathsownership tableCATALOG_SLUG_GENERATION_FAILED
claimSlug()create pathsslug row
rename()update pathscurrent slugdemote + promote*_SLUG_ALREADY_EXISTS
resolve()storefrontownership tablenull → 404 by caller

Ordering note: demote-then-promote is not negotiable — uq_*_slug_current is a partial unique index and can never be DEFERRABLE.

6.2 CatalogCategoryTreeService

Owns lock acquisition (pg_advisory_xact_lock, key 4_812_001, first statement in tx), cycle detection (recursive CTEs with CYCLE clauses), depth validation for subtree moves, and the one-statement subtree move (UPDATE ... FROM rewriting parent_id + depth together — two statements fail 100% at root boundaries because the CHECK is immediate).

6.3 CatalogSearchService

UNION ALL over three branches; % operator in WHERE (index-accelerated), similarity() only in ORDER BY; ORDER BY rank DESC, display_order ASC, id ASC for stable pagination; visibility enforced in SQL (ancestor CTE for categories, brand join for series); cached under catalog:search: with CACHE_TTL.STANDARD (300s); terms > 64 chars skip the cache.

6.4 CatalogImportService / CatalogExportService

Import: parse (CSV/XLSX streaming), validate every row, detect file-internal category cycles (Kahn's algorithm), then one transaction: inserts + terminal job write (completeImportJob last). Cancellation check every 500 rows inside the transaction. Export: count-guard at 50,000 rows, CSV with formula-injection escaping, result_file_url + completed in one UPDATE.

7. Runtime Flows

7.1 Move a category subtree

7.2 Import job

8. Cache

DomainRevalidation tagsRedis patterns
catalog_categorycatalog:categories, page:catalogcatalog:category:*, catalog:search:*
catalog_brandcatalog:brands, page:catalogcatalog:brand:*, catalog:series:*, catalog:search:*
catalog_brand_seriescatalog:series, page:catalogcatalog:series:*, catalog:brand:*, catalog:search:*

A brand write clears series keys (series visibility depends on the brand) and vice versa; every domain clears search. Activity logging and cache invalidation run after commit.

9. Jobs

QueueJobsContract
CATALOGcatalog.import_entities, catalog.export_entities{ jobPublicId, entity, sourceFileUrl } / { jobPublicId, entity, filters }
  • Lease-based claim (queued OR processing past 15-min TTL); retries are real (release to queued before final attempt).
  • Cooperative cancellation: import re-checks its job status every 500 rows; completeImportJob throws CATALOG_JOB_NOT_PROCESSING on a zero-row match.
  • Sweep (maintenance.sweep_stalled_catalog_jobs, every 5 min) fails stalled processing jobs; isStalled derived flag on detail.
  • CATALOG is registered by its own module — not in REGISTERED_QUEUES; job options set explicitly at .add() (attempts 3, exponential backoff 5s).

10. Security and Authorization

  • Admin: JwtAuthGuard + RoleGuard; permissions Categories_*, Brands_*, BrandSeries_*, Catalog_*, Tags_*; superadmin bypasses.
  • Import submit re-checks the entity-specific create permission (route-level Catalog_CREATE is a superset).
  • Rate limits: ADMIN_READ 30/min, ADMIN_WRITE 10/min, ADMIN_BULK_WRITE 5/min, ADMIN_REORDER 30/min, ADMIN_ASYNC_JOB_SUBMIT 10/hour, PUBLIC_READ 60/min, PUBLIC_SEARCH 60/min, PUBLIC_SUGGEST 300/min (guest-facing).
  • Storefront never exposes integer PKs; slugs are ownership-table resolved.