Today's authoring path for a doctrine update:
Doctrine.md in a text editor.APPLY confirm.This is appropriate for code changes. It is over-ceremonial for content. A typo in doctrine takes 20 minutes including the apply wait. An updated example in a walkthrough is a PR. A clarification to the agent skill is a PR. The repository becomes the bottleneck for content velocity.
Three things move with a CMS:
Standard cartridge shape: API Gateway HTTP API + Lambda + DDB + CloudFront-fronted S3 frontend.
The MCP server (Step 6.r) is the only consumer of the CMS read API in v2.0. Future consumers (a public docs site, a Slack bot, etc.) read the same DDB but get their own access path.
Table: forge-sdk-cms-content
pk: content_id // e.g. "doctrine", "docs/cartridge-walkthrough",
// ".claude/skills/forge-sdk/SKILL",
// "shared/modules/jwt-verify.js"
sk: version // semver string: "1.0.0", "1.1.0", "2.0.0-rc1"
Attributes:
type: // "markdown" | "javascript" | "terraform-snippet" | ...
title: // human-readable display name
content: // raw bytes (markdown source, source file)
metadata: { // JSON
owner: "platform-team",
audience: "cartridge-authors",
tags: ["foundation", "rules"],
summary: "...",
}
status: // "draft" | "submitted" | "approved" | "published" | "deprecated" | "revoked"
submitted_by: // cognito username (during draft phase)
submitted_at: // ISO timestamp
approved_by: // cognito username (during approval phase)
approved_at: // ISO timestamp
published_by: // cognito username (final publish)
published_at: // ISO timestamp
deprecation: { // optional
sunset_at: "2027-06-01T00:00:00Z",
replacement: "2.0.0",
message: "..."
}
content_hash: // SHA-256 of content bytes (for git-mirror diff)
GSI by status: status → content_id, version (for the approval queue UI)
| Type | Examples | Rendered as |
|---|---|---|
markdown | Doctrine, docs/*, README files, SKILL.md | Rendered to HTML by the editor preview; served raw by MCP |
javascript | shared/modules/*.js, oauth adapters | Served raw by MCP; syntax-highlighted in editor preview |
terraform-snippet | cartridges/_template/backend.tf.snippet | Served raw; syntax-highlighted |
json | forge-sdk-manifest.json | Validated as JSON on submit; served as JSON |
Each content item versions independently. jwt-verify.js at 1.0.0 plus Doctrine.md at 1.0.0 = the SDK at 1.0.0. Bumping one helper to 1.1.0 doesn't automatically bump everything else; the "SDK release" is a curated set of versions.
Built on the SDK v1 frontend components (table, modal, design tokens). The CMS frontend extends with:
[ in the editor offers a list of other CMS documents to link to. Updates if the target document is renamed (keeps cross-refs stable).For agent skills (SKILL.md), the editor adds:
Per-document semver. Each publish bumps a tier:
| Bump | What it means for that document |
|---|---|
| major | Breaking change to the document's contract. For shared/modules JS: incompatible API change. For doctrine: a non-negotiable changes meaning. Consumers pinned to the previous major need to read and adapt. |
| minor | Additive change. For shared/modules JS: new exported function. For doctrine: a new entry added. Existing consumers are unaffected. |
| patch | Bug fix or clarification. No semantic change. |
The SDK as a whole has its own version that operators bump at release time. The "SDK 2.1.0 release" is a curated snapshot of (jwt-verify.js@a.b.c, hmac.js@d.e.f, Doctrine.md@g.h.i, ...). Operators using the CMS choose which document versions are in the next SDK release; publish creates a release manifest.
The release manifest lives in DDB:
Table: forge-sdk-cms-releases
pk: "release"
sk: sdk_version // e.g. "2.0.0"
Attributes:
contents: {
"shared/modules/jwt-verify.js": "2.0.0",
"shared/modules/hmac.js": "1.3.2",
"Doctrine.md": "2.0.0",
...
}
released_at: // ISO timestamp
released_by: // cognito username (system admin)
notes: // human-readable release notes (rendered in pull-sdk.sh output)
The MCP server resolves a consumer's sdk_version: "2.0.0" pin by looking up the release row and serving each requested resource at its release-pinned version.
The MCP server (Step 6.r) reads from the CMS. The CMS does NOT serve content directly to external consumers in v2.0; everything goes through MCP.
This separation is intentional:
If we later add a public docs site or a Slack-bot consumer, they read from the CMS via a different access pattern (probably DDB direct via IAM, or a separate REST cartridge). MCP is the cartridge-and-agent path; alternative consumers get alternative paths.
v1.x SDK ships content as files in the repo. The CMS bootstrap is a one-time import that turns those files into CMS rows.
sdk-v1.1.0). For each file, creates a CMS row with status: published, version equal to the SDK's version (1.1.0), published_by: system-admin, published_at: now.sdk_version: "1.1.0", contents map every imported document to 1.1.0.The import script lives in the CMS cartridge under lambda/import.js. Runs idempotently (re-running against the same git tag is a no-op).
Every CMS publish triggers a job that:
cms-sync/2.1.0).main with the diff: "CMS publish: doctrine 2.7 update + cartridge-walkthrough patch + skill clarification."The git mirror is the disaster-recovery cache. If the CMS or MCP server is down, consumers fall back to ./pull-sdk.sh --fallback-git which clones the mirror at the latest sdk-v*.*.* tag. The content is byte-equivalent to the CMS's published rows because the sync job keeps it in step.
Quarterly verification: a job diffs the git mirror's current main against the CMS's published rows. Divergence triggers an investigation; convergence is logged.
An alternative design: the CMS writes directly to git via the GitHub API, and "publish" is "merge to main." Rejected because it couples the CMS's availability to GitHub's, and because the approval UX wants to live in the Console (not in a GitHub PR comment thread). The sync-job pattern keeps the CMS authoritative for content, with git as a mirror.
| Pro | Con |
|---|---|
| Doctrine edits ship in minutes via the Console, no Terraform apply. | Loss of git-PR review tooling for content. Mitigated by the approval-queue UI being functionally equivalent. |
| Per-document version history is queryable. "What did Doctrine 2.7 say on May 1st?" is one DDB query. | DDB versioning is less battle-tested than git for content workflows. Mitigated by the git mirror staying byte-equivalent. |
| Approval workflow lands the platform's existing Request Roles shape. | More cartridges to operate (CMS plus MCP both become critical). |
| Per-document deprecation, sunset dates, replacement pointers are first-class. | Reviewers lose the ability to comment on specific lines (git PR comments). Mitigated by adding inline-comment support in the approval-queue UI as a future iteration. |
| External cartridge authors never need git access to read doctrine. | Authoring on a plane (no internet, broken Cognito) is impossible. Mitigated by the git mirror being readable offline. |
Three PRs within the v2 build plan from Step 6.r.
PR 2 + 3 + 4 can ship while the MCP server work is in flight on a parallel track. The handoff in PR 5 is when both halves meet.