Content Module Backend Documentation
Data model, validation architecture, and module composition for page-section CMS.
Content Module - Backend Documentation
1. Backend Scope and Boundaries
Content backend owns:
- page-level CMS metadata (
pageKey, title, optionalseoId) - section-level payload CRUD
- strict page/section compatibility enforcement
- strict payload validation per section key
- public read projection (enabled, ordered sections)
Content backend does not own:
- product listing source data
- blog listing source data
- FAQ listing source data
Those data sources remain in their existing modules and are composed by frontend runtime.
2. Module Composition (Aggregate + Leaf)
ContentModule composes:
ContentAdminModuleContentCustomerModule
Shared service wiring:
ContentSharedModuleprovides and exportsContentService
Leaf ownership model:
ContentAdminModule: admin HTTP surface + guards + permissionsContentCustomerModule: public/mobile HTTP read surface
3. Routing and Composition
3.1 Direct routes
- Admin base:
admin/content/pages - Public base:
content/pages
3.2 Mobile composition
MobileModule imports ContentCustomerModule and includes it in RouterModule.register([{ path: "mobile", children: [...] }]).
Resulting mobile route:
GET /api/mobile/content/pages/:pageKey
4. Database Model (Drizzle / PostgreSQL)
4.1 content_page
Columns:
idserial PKpage_keyvarchar(80), uniquetitlevarchar(255)seo_iduuid nullable FK ->seo.id(ON DELETE SET NULL)created_attimestamptzupdated_attimestamptz
Indexes and constraints:
- unique index on
page_key - index on
seo_id - partial unique index on
seo_id(IS NOT NULL)
4.2 content_section
Columns:
idserial PKpage_idinteger FK ->content_page.id(ON DELETE CASCADE)section_keyvarchar(80)positioninteger default0is_enabledboolean defaulttruepayload_jsonjsonbcreated_attimestamptzupdated_attimestamptz
Indexes and constraints:
- unique
(page_id, section_key) - index on
page_id - index on
(page_id, position)
5. Permission and Authorization Model
Permission module registration:
- Added
Contentto permission module list.
Generated permission codes:
Content_CREATEContent_READContent_UPDATEContent_DELETE
Controller use:
GET /admin/content/pages/:pageKey->Content_READPATCH /admin/content/pages/:pageKey/meta->Content_UPDATEPUT /admin/content/pages/:pageKey/sections/:sectionKey->Content_UPDATEDELETE /admin/content/pages/:pageKey/sections/:sectionKey->Content_DELETE
Guards:
JwtAuthGuardRoleGuard- per-route throttling with
IpThrottlerGuard
6. Service Architecture and Read/Write Semantics
ContentService is the single business service and handles:
- page existence policy
- section allowlist checks
- payload validation
- SEO foreign-key pre-validation
- admin/public response shaping
6.1 Page existence policy
Admin paths (getPageForAdmin, updatePageMeta, upsertSection) use ensurePage:
- if page row does not exist, create it with default title from
CONTENT_PAGE_DEFAULT_TITLES
Public path (getPageForPublic) uses getPageByKey only:
- missing page throws
404 CONTENT_NOT_FOUND
6.2 Section upsert policy
Section writes are idempotent by (page_id, section_key):
- implemented with
onConflictDoUpdate - updates
position,isEnabled,payloadJson, andupdatedAt
6.3 Section delete policy
Delete requires existing page + section row:
- missing page =>
404 CONTENT_NOT_FOUND - missing section =>
404 CONTENT_NOT_FOUND
7. Validation Architecture
Validation happens at two levels:
- Param DTO validation:
pageKeymust be one ofCONTENT_PAGE_KEYSsectionKeymust be one ofCONTENT_SECTION_KEYS
- Service-level registry validation:
- section must be allowed for page (
isAllowedSectionForPage) - payload must satisfy section schema (
validateContentSectionPayload)
7.1 Validation constants
MAX_LABEL_CHARS = 60MAX_HEADING_CHARS = 110MAX_DESCRIPTION_CHARS = 320MAX_CARD_TITLE_CHARS = 60MAX_CARD_DESCRIPTION_CHARS = 220
7.2 URL safety rule
safeUrlSchema accepts:
- absolute HTTP(S) URLs
- site-relative URLs that start with
/
7.3 Tiptap doc rule
tiptapDocSchema requires:
type: "doc"- non-empty
content[]
Size guard:
- JSON stringified length capped by
maxChars * 12
8. Page and Section Registries
8.1 Page keys
homeaboutfaqprivacy_policyterms_of_serviceproducts_thangkasproducts_singing_bowlsproducts_statuesproducts_jewelleryglobal_footer
8.2 Section keys
herotop_pickscategoriesheritagewhy_usfaq_introblogsquotewhy_choosestory_blocksproducts_headerblogs_headerbrand_blockquick_linkscollections_linkssupport_linksfeatured_collections_cardscopyrightdocument
8.3 Allowlist mapping
CONTENT_SECTIONS_BY_PAGE enforces exact allowed section sets for each page key.
9. Response Construction and Projection
Response DTO root:
ContentPageResponseDto
Fields returned:
- page metadata (
id,pageKey,title,seoId, timestamps) - resolved
seoobject ifseoIdexists sections[]
Sorting strategy:
position ASCid ASC
Public projection filter:
isEnabled = true
10. Error Handling Contract
10.1 Content-specific error codes used
CONTENT_NOT_FOUNDCONTENT_SECTION_KEY_INVALIDCONTENT_SECTION_PAYLOAD_INVALIDCONTENT_SEO_NOT_FOUND
10.2 Error scenarios
| Scenario | HTTP | errorCode |
|---|---|---|
| Missing page on public read | 404 | CONTENT_NOT_FOUND |
| Invalid section for page | 400 | CONTENT_SECTION_KEY_INVALID |
| Invalid payload shape/Tiptap/URL | 400 | CONTENT_SECTION_PAYLOAD_INVALID |
| Invalid SEO id on meta update | 400 | CONTENT_SEO_NOT_FOUND |
11. Data Integrity and Migration Notes
Migration reset model:
- single baseline migration file generated for full schema
- baseline includes
CREATE EXTENSION IF NOT EXISTS pg_trgm;
Why extension is required:
- existing catalog/product/tag trigram indexes use
gin_trgm_ops - baseline migration must always provision
pg_trgmbefore those indexes
12. Performance and Query Notes
Current query model is simple and bounded:
- single page row query with left join to
seo - section query filtered by page id (+ enabled flag for public)
- deterministic sort with indexed columns
Current write model:
- one upsert per section write
- one update for metadata write
- one delete for section removal
No N+1 behavior exists in current implementation.
13. Concurrency and Consistency
13.1 Concurrent section edits
Because section writes use upsert with unique (page_id, section_key), concurrent writes converge to last-write-wins.
13.2 Partial update semantics
PATCH meta: only provided fields are updatedseoId: nullclears SEO relation
13.3 Read consistency with cache
Reads are cache-aside with Redis:
- public/mobile reads use
content:page:public:key space - admin reads use
content:page:admin:key space
Every successful write (PATCH meta, PUT section, DELETE section) invalidates both key spaces for that page key, so post-write reads see fresh DB state.
14. Test Coverage
Added unit tests:
content.service.spec.tscontent-admin.controller.spec.tscontent-customer.controller.spec.ts
Covered scenarios:
- invalid page/section pair rejection
- invalid payload rejection before DB write
- missing public page returns
CONTENT_NOT_FOUND - deterministic public/admin cache key + TTL usage
- cache read fallback to DB on Redis failure
- write-path cache invalidation for meta/section write/delete
- controller response wrapping and service delegation
15. Operational Checklist
- DB migrate applied successfully in target env.
- Role permissions seeded include
Content_*codes. - Admin roles updated to include required content permissions.
- Redis configured and reachable in runtime env.
-
CONTENT_PAGE_PUBLIC_CACHE_TTL_SECONDSset (or default accepted). -
CONTENT_PAGE_ADMIN_CACHE_TTL_SECONDSset (or default accepted). - Frontend mapped page keys correctly for route usage.
- Frontend handles
CONTENT_NOT_FOUNDfallback path. - Tiptap JSON emitted by admin is valid
docroot shape.
16. Future Extension Points
Planned-compatible extension options:
- draft/publish workflow (
content_page_version) - locale support (
locale, localized section rows) - scheduled publish windows
- admin list/search endpoint for pages
Current implementation intentionally stays immediate-live for v1.
17. File Map
Core implementation:
apps/api/src/modules/content/content.module.tsapps/api/src/modules/content/content-shared.module.tsapps/api/src/modules/content/content.service.ts
HTTP surfaces:
apps/api/src/modules/content/admin/content-admin.controller.tsapps/api/src/modules/content/admin/content-admin.module.tsapps/api/src/modules/content/customer/content-customer.controller.tsapps/api/src/modules/content/customer/content-customer.module.ts
Schema and contracts:
apps/api/src/modules/content/content.contract.tsapps/api/src/modules/content/content-schema.registry.tsapps/api/src/modules/content/dto/*
Database:
packages/db/src/schema/content/content-page.schema.tspackages/db/src/schema/content/content-section.schema.tspackages/db/src/migrations/0000_fresh_baseline_reset.sql
Permissions/error wiring:
apps/api/src/common/authorization/permissions.types.tspackages/db/src/seed/seed-auth.tsapps/api/src/common/types/error-codes.ts
Time fields in this module are stored as timezone-aware values and should be handled as ISO-8601 instants by API consumers.
See Also
- Feature Guide: See Content Module - Feature List for page/section capability matrix.
- API Guide: See Content Module - API & Integration Guide for endpoint payload contracts.