Composable Content (CMS)
Relevant source files
The following files were used as context for generating this wiki page:
- packages/cms/README.md
- packages/cms/src/types/content-types.ts
- packages/convex/convex/tables/cms.ts
- packages/cms/scripts/codegen/outputs.ts
- CONTEXT.md
- apps/storefront/src/components/cms/cms-content.tsx
- apps/admin/src/app/(app)/(dashboard)/[domain]/content/page.tsx/(dashboard)/[domain]/content/page.tsx)
The Composable Content system (CMS) is a multi-tenant, Convex-native content layer designed to provide structured data for marketing pages, articles, and metadata overlays. It replaces the legacy Payload-based implementation with a descriptor-driven architecture where content shapes are defined once and propagated through the entire stack via automated codegen.
Sources: packages/cms/README.md:1-15, CONTEXT.md:88-93
Architecture and Data Flow
The CMS follows a "Single Source of Truth" model using field descriptors to define content shapes. These descriptors drive the storage schema in Convex, the authoring interface in the Admin app, and the read-contract types in the Storefront.
The Codegen Pipeline
The pnpm cms:gen command processes field descriptors and editor manifests to produce several critical artifacts:
| Artifact | Purpose | Target Path |
|---|---|---|
| Action Wrappers | Server actions for Admin editor operations | apps/admin/src/lib/cms-actions/_generated/ |
| Read-Contract Types | TypeScript types for Storefront getters | packages/cms/src/types/content-types.ts |
| Table Validators | Convex schema definitions for content tables | packages/convex/convex/tables/cms.ts |
| Localization Manifest | Registry for i18n bucket detection | packages/convex/convex/cms/localized_paths.ts |
Sources: packages/cms/scripts/codegen/outputs.ts:14-25, packages/cms/README.md:30-40
System Workflow Diagram
The following diagram illustrates how descriptors flow from definition to consumption across the platform.
The diagram shows the descriptor-driven codegen process and the resulting interaction between the Admin editor, Convex storage, and Storefront rendering.
Sources: packages/cms/README.md:20-50, packages/cms/scripts/codegen/outputs.ts:114-143
Core Data Structures
Content Tables
CMS content is stored in Convex tables, each keyed by a shop foreign key to ensure strict multi-tenancy. Tables are indexed by by_shop to allow efficient tenant-scoped lookups.
| Table | Purpose | Key Fields |
|---|---|---|
pages | Marketing/Landing pages | shop, slug, blocks |
articles | Blog posts/News | shop, slug, author, body |
productMetadata | CMS overlays for Shopify products | shop, shopifyHandle, blocks |
collectionMetadata | CMS overlays for Shopify collections | shop, shopifyHandle, blocks |
header / footer | Global navigation singletons | shop, items, cta |
Sources: packages/convex/convex/tables/cms.ts:20-200, apps/admin/src/app/(app)/(dashboard)/[domain]/content/page.tsx:142-178
Block Model
Content within pages is structured into Blocks, which are discriminated-union units. Blocks carry per-instance fields (authored in the page editor) and optional settings (store-wide defaults).
// Block rendering in the storefront
<BlockRenderer
blocks={page.blocks}
context={{
shop,
locale,
loaders: {
loadCollection,
loadVendors,
loadOverview,
},
}}
/>
Sources: packages/cms/README.md:68-85, CONTEXT.md:101-115
Multitenancy and Security
Tenant Isolation
Every CMS content row carries a shop id. Tenancy is never implicit; all requests must provide a { shop, locale } context. In the Admin app, the getAuthedCmsCtx(domain) helper ensures that operators only access content for their authorized shops.
Sources: CONTEXT.md:95-100, apps/admin/src/app/(app)/(dashboard)/[domain]/content/page.tsx:115-116
Access Control Sequence
The following sequence diagram represents the authenticated request flow for accessing CMS content in the Admin dashboard.
This diagram depicts how the Admin app validates tenant authorization before fetching CMS documents from the backend.
Sources: apps/admin/src/app/(app)/(dashboard)/[domain]/content/page.tsx:115-180, CONTEXT.md:218-221
Storefront Integration
The Storefront reads published content directly from Convex cms/read functions. While it uses the generated types for safety, it often implements its own rendering logic (<CMSContent />) to ensure styling consistency with the storefront theme.
CMS Content Dispatcher
The CMSContent component handles the fetching and rendering of supplemental content for Shopify entities:
- Fetch: Calls
PageApiwith shop, locale, and handle. - Draft Mode: Checks
isDraftModeEnabled()to determine if it should render admin-specific preview hints (data-cms-field). - Extensions: Resolves
ResolvedExtensionsApito apply per-shop settings/defaults. - Render: Passes blocks to the
<Blocks />dispatcher.
Sources: apps/storefront/src/components/cms/cms-content.tsx:28-48, apps/storefront/README.md:120-135
Conclusion
The CMS module provides a type-safe, multi-tenant framework for content management. By using a descriptor-driven architecture, it ensures that changes to content shapes are automatically reflected in the database schema, administrative tools, and the storefront's reading contract. This centralization eliminates drift between storage and UI, while the multi-tenant design allows a single deployment to serve independent content sets for various shops.
Sources: packages/cms/README.md:20-25, README.md:15-25