Planning corpus: this is the real design-discussion doc for epic cms-admin-crud, synced verbatim from this repo’s own internal planning process.
Design Discussion — Epic 28: cms-admin-crud
0. Prelude
Source: a direct finding from a fresh gap audit (2026-09-08): CMS’s own subsystem
(packages/cms) is fully capable of creating and publishing pages — CmsService.createPage/
updatePage/publishPage/createMarketingPage all already exist and work — but nobody
ever built an admin UI to call them. /admin/cms is a read-only table. Worse: the AI/MCP
tool manage_cms_page only supports action: "update"|"publish", no "create". Today a
custom marketing page can only be authored by hand-editing seed code — neither a human admin
nor an agent can create one.
1. Goal
An admin can create, edit, and publish a CMS page (including a marketing/campaign page) from
/admin/cms. An agent can do the same through the MCP interface.
2. Why this needs no new package or subsystem
Unlike every epic in the 20s wave, no new domain model or adapter contract is needed here.
CmsService’s public surface already has everything: createPage(input), updatePage(id, patch), publishPage(id), createMarketingPage(input), components: ComponentRegistry
(the list of valid componentTypes a section can reference). This epic is pure
app-composition-layer work (an admin UI + an MCP tool extension), the same shape epic 25
(admin-adapter-visibility-settings) took when it found no new subsystem was warranted either.
3. Design decisions
- Section (component) editing uses a fixed-slot, raw-JSON-config form — mirroring
bundles’/advertising’s own “fixed slot count, plain HTML, no client JS” convention for
nested/variable-shaped data. A
ComponentInstance’sconfigisRecord<string, unknown>by design (opaque to CMS itself, perdocs/subsystems/05-cms-pages.md) — there is no single schema to build a typed form against acrosshero-banner/category-spot/product-grid/ad-slot/service-area-info, each with different config shapes. A fixed number of section slots, each with acomponentType<select>(populated fromcms.components.list()) and a raw-JSON<textarea>forconfig(parsed server-side, clear error on invalid JSON) is the honest, simplest form that doesn’t require building five separate per-component-type editors. - Marketing pages get their own create form, separate from the generic page-create form,
since
createMarketingPage’s input shape (campaignName,startDate,endDate,productIds) is genuinely different from a plain page’s. - The MCP tool gains a
"create"action, matching the existingactionenum pattern already used for"update"/"publish"on the same tool, rather than a brand-new tool definition — keeps one tool per capability area, consistent with how the other 7 tools in this interface are scoped. - Every new mutation (admin UI and MCP handler alike) is gated by the admin-auth epic’s
requireAdminPermission("mutate"), the same guard every other admin mutation in this repo now carries — this epic ships strictly after admin-auth-clerk for that reason.
4. Scale assessment
Small. No new package. Proceeding with 3 stories: admin UI, MCP tool extension, closeout.
5. Version bump
patch — additive UI and one new MCP tool action; no change to any package’s own public
contract shape (CmsService’s methods are unchanged, just finally called from somewhere).