Shop It Docs
Developer ResourcesContent

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, optional seoId)
  • 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:

  • ContentAdminModule
  • ContentCustomerModule

Shared service wiring:

  • ContentSharedModule provides and exports ContentService

Leaf ownership model:

  • ContentAdminModule: admin HTTP surface + guards + permissions
  • ContentCustomerModule: 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:

  • id serial PK
  • page_key varchar(80), unique
  • title varchar(255)
  • seo_id uuid nullable FK -> seo.id (ON DELETE SET NULL)
  • created_at timestamptz
  • updated_at timestamptz

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:

  • id serial PK
  • page_id integer FK -> content_page.id (ON DELETE CASCADE)
  • section_key varchar(80)
  • position integer default 0
  • is_enabled boolean default true
  • payload_json jsonb
  • created_at timestamptz
  • updated_at timestamptz

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 Content to permission module list.

Generated permission codes:

  • Content_CREATE
  • Content_READ
  • Content_UPDATE
  • Content_DELETE

Controller use:

  • GET /admin/content/pages/:pageKey -> Content_READ
  • PATCH /admin/content/pages/:pageKey/meta -> Content_UPDATE
  • PUT /admin/content/pages/:pageKey/sections/:sectionKey -> Content_UPDATE
  • DELETE /admin/content/pages/:pageKey/sections/:sectionKey -> Content_DELETE

Guards:

  • JwtAuthGuard
  • RoleGuard
  • 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, and updatedAt

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:

  1. Param DTO validation:
  • pageKey must be one of CONTENT_PAGE_KEYS
  • sectionKey must be one of CONTENT_SECTION_KEYS
  1. Service-level registry validation:
  • section must be allowed for page (isAllowedSectionForPage)
  • payload must satisfy section schema (validateContentSectionPayload)

7.1 Validation constants

  • MAX_LABEL_CHARS = 60
  • MAX_HEADING_CHARS = 110
  • MAX_DESCRIPTION_CHARS = 320
  • MAX_CARD_TITLE_CHARS = 60
  • MAX_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

  • home
  • about
  • faq
  • privacy_policy
  • terms_of_service
  • products_thangkas
  • products_singing_bowls
  • products_statues
  • products_jewellery
  • global_footer

8.2 Section keys

  • hero
  • top_picks
  • categories
  • heritage
  • why_us
  • faq_intro
  • blogs
  • quote
  • why_choose
  • story_blocks
  • products_header
  • blogs_header
  • brand_block
  • quick_links
  • collections_links
  • support_links
  • featured_collections_cards
  • copyright
  • document

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 seo object if seoId exists
  • sections[]

Sorting strategy:

  • position ASC
  • id ASC

Public projection filter:

  • isEnabled = true

10. Error Handling Contract

10.1 Content-specific error codes used

  • CONTENT_NOT_FOUND
  • CONTENT_SECTION_KEY_INVALID
  • CONTENT_SECTION_PAYLOAD_INVALID
  • CONTENT_SEO_NOT_FOUND

10.2 Error scenarios

ScenarioHTTPerrorCode
Missing page on public read404CONTENT_NOT_FOUND
Invalid section for page400CONTENT_SECTION_KEY_INVALID
Invalid payload shape/Tiptap/URL400CONTENT_SECTION_PAYLOAD_INVALID
Invalid SEO id on meta update400CONTENT_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_trgm before 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 updated
  • seoId: null clears 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.ts
  • content-admin.controller.spec.ts
  • content-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_SECONDS set (or default accepted).
  • CONTENT_PAGE_ADMIN_CACHE_TTL_SECONDS set (or default accepted).
  • Frontend mapped page keys correctly for route usage.
  • Frontend handles CONTENT_NOT_FOUND fallback path.
  • Tiptap JSON emitted by admin is valid doc root 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.ts
  • apps/api/src/modules/content/content-shared.module.ts
  • apps/api/src/modules/content/content.service.ts

HTTP surfaces:

  • apps/api/src/modules/content/admin/content-admin.controller.ts
  • apps/api/src/modules/content/admin/content-admin.module.ts
  • apps/api/src/modules/content/customer/content-customer.controller.ts
  • apps/api/src/modules/content/customer/content-customer.module.ts

Schema and contracts:

  • apps/api/src/modules/content/content.contract.ts
  • apps/api/src/modules/content/content-schema.registry.ts
  • apps/api/src/modules/content/dto/*

Database:

  • packages/db/src/schema/content/content-page.schema.ts
  • packages/db/src/schema/content/content-section.schema.ts
  • packages/db/src/migrations/0000_fresh_baseline_reset.sql

Permissions/error wiring:

  • apps/api/src/common/authorization/permissions.types.ts
  • packages/db/src/seed/seed-auth.ts
  • apps/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