This is the canonical reference. Non-negotiable doctrines are guardrails (PRs violating them are rejected). Default doctrines can be overridden with a written rationale. Each entry has three parts: the rule (imperative sentence), the why (failure mode it prevents), and how to apply (where in the codebase it shows up).
The safety floor every cartridge inherits.
Why. Egress allowlist is the only practical defense against data exfiltration from a compromised cartridge. Four systems of record (Salesforce, Monday, GitHub, GitBook) are the only outside destinations a Lambda can reach. Anything else is a foundation PR with explicit rationale.
How to apply. New external destinations require updates to substrate/modules/platform/network/. Cartridges do not add their own egress rules.
Why. The bootstrap permission boundary prevents categories of escalation no matter what a cartridge role asks for. Layering app-level checks on top is redundant; bypassing is impossible.
How to apply. Every aws_iam_role gets permissions_boundary = var.permission_boundary_arn.
Why. AuthType = NONE exposes Lambdas without authentication, breaking the three-layer defense. SCP-denied at the org level so the resource type cannot be created even by an admin.
How to apply. Every Lambda invocation goes through API Gateway HTTP API + X-Origin-Secret + JWT, fronted by CloudFront.
Why. Compromise of an agent role must not leak PII; compromise of an audit role must not allow tampering. Three keys structurally enforce three trust boundaries.
How to apply. Agent CMK, Lockbox CMK, Audit CMK declared in substrate/modules/platform/identity/. Cartridges declare which cell they consume (see Step 4.5.a); they do not pick keys.
Why. Auditors and incident responders need to answer "who did what" without trusting application code. CloudTrail data events + immutable audit bucket + GuardDuty give that answer.
How to apply. CloudTrail data events on for S3, KMS, Secrets Manager. Audit bucket: Object Lock, no delete.
Why. AWS-managed keys are fine for a hobby project. PayCargo needs the audit trail of every decrypt call and the ability to revoke decrypt without redeploying.
How to apply. Every CMK is aws_kms_key with an explicit key policy. No aws/...-prefixed AWS-managed key paths.
Why. Observe what production traffic looks like before declaring what is an anomaly. Skipping the alert phase produces false positives and operator distrust of the firewall.
How to apply. New egress rules ship in alert mode first. After clean alert logs, the rule flips to drop. See Step 5.
Why. AES-256 at rest is quantum-resistant per current consensus. TLS-in-transit is on AWS's hybrid post-quantum rollout. We do not roll our own crypto, so we inherit the migration.
How to apply. Symmetric encryption only for our own choices (AES-256-GCM). Asymmetric stays on AWS surfaces. See Step 4.5.a section 12.
The installation hub. The standard that cartridges plug into.
Why. If each cartridge implements its own access control, retention, or budget logic, drift is inevitable. Centralized enforcement lets us change policy once.
How to apply. Cartridge manifests declare data classification, retention, OAuth providers, owner, budget. Platform applies the constraints. Step 6.i.
Why. Data governance is a substrate concern, not a per-app concern. Without this layer every app invents its own classification.
How to apply. Cartridges declare a governance_cell. Substrate maps cells to keys.
Why. CloudFront OAC + Lambda Function URL silently fails on POST/PUT body-hash signing. We hit this on the obs budget-save bug and lost an afternoon. Never again.
How to apply. Every cartridge backend is API Gateway HTTP API + Lambda. CloudFront origin sends X-Origin-Secret; Lambda rejects without it. Step 6.j.
Why. Three-layer defense: CloudFront fronts, Cognito authenticates, the Cognito group authorizes. Any single layer failing does not collapse the others.
How to apply. Admin routes: read JWT from X-Auth-Token, verify, check cognito:groups contains the required group. Step 4.5.b.
Why. A single SDK at one URL is auditable and updatable in one place. Per-cartridge SDK copies drift apart within weeks.
How to apply. SDK lives at console/sdk/v1/. Cartridges link from the Console origin via the consoleOrigin config returned from /api/config.
Why. External cartridges (and increasingly internal ones) need a reproducible SDK version they can pin. The browser SDK URL implies v1 but is not a real version. Without a manifest declaring what counts as "the SDK", consumers cannot reason about compatibility.
How to apply. Semver in VERSION at the repo root. forge-sdk-manifest.json declares the four bundle paths (shared/modules/, console/sdk/v1/, cartridges/_template/, Doctrine.md + .claude/skills/forge-sdk/). Release tags are sdk-vX.Y.Z. Consumers pull with console/sdk/pull-sdk.sh [version] into ./forge-sdk/. See Step 6.o.
Why. PayCargo's Azure AD is the identity source of truth. Federation eliminates the two-password problem. Zero default permissions on first sign-in prevents accidental access; permissions are explicitly granted via the in-platform Request Roles cartridge.
How to apply. Cognito User Pool IdP type = SAML. SAML assertion does NOT carry group claims. JIT provisioning creates the Cognito user with no groups; user sees only the Request Roles cartridge. System admins approve requests via AdminAddUserToGroup. Cartridge JWT verify code unchanged. See Step 6.p.
Why. Forge access tracks Forge ownership, not Azure HR conventions. Coupling Cognito groups to Azure groups ties our access model to a shape we don't control and makes the audit story go through HR's group state from N weeks ago.
How to apply. SAML assertion configured to omit group claims. No pre-token-generation Lambda. Group management surfaces: obs Admin tab (ongoing) and Request Roles cartridge (new-user onramp). Each cartridge declares which Cognito groups it requires; the platform enforces. See Step 6.p.
Why. Bulk-download distribution lets consumers drift for months between pulls and gives the platform no visibility into who is on what version. A live MCP server makes every fetch authenticated, audited, and version-specific. Real-time means consumers always build against an answered question, not a stale lockfile. Protected means a leaver loses SDK access automatically when Cognito access is revoked. Agents (Claude Code, custom MCP clients) become first-class consumers alongside humans.
How to apply. A forge-sdk-mcp cartridge serves SDK content as MCP resources at URIs like forge-sdk://shared/modules/jwt-verify.js, forge-sdk://Doctrine.md#2.7. Every request is Cognito-authenticated; every fetch logs {requester, resource, version, timestamp, prompt_context_hash}. The pull-sdk.sh CLI wraps MCP under the hood; agents speak MCP natively. v1 git-pull is the bootstrap layer; v2 MCP ships once Request Roles + SSO are live. See Step 6.r.
Why. Doctrine typo fixes should not require a git PR and a Terraform apply. Versioning of content as DDB time-series gives operators rollback, diff, and per-document publish history that git tag-based versioning cannot match for content workflows. Authoring in the Console UI puts the doctrine update flow inside the same approval surface (Request Roles, role assignment) that already governs the platform.
How to apply. A forge-sdk-cms cartridge owns the markdown content layer: editor UI in the Console, draft / submit / approve / publish workflow, DDB-backed content rows with per-document version history. The MCP server cartridge (Doctrine 2.9) consumes from the CMS. The repo continues to hold cartridge code; the CMS holds the content cartridges and agents read. Initial bootstrap imports the existing Doctrine.md + docs/*.md + SKILL.md as v1.x.x CMS rows; subsequent edits happen via the Console UI. See Step 6.s.
How rides are built.
Why. Every npm dependency is a supply-chain surface, a version-pin drift risk, and an audit story we have to defend. JWT verify is ~70 lines of WebCrypto. HMAC is ~10 lines of node:crypto. None justifies npm.
How to apply. Import AWS SDK clients from the Node 20 runtime. Get JWT, HMAC, log, OAuth helpers from shared/modules/ staged into lambda/_shared/ at apply. Any new npm dep requires a written justification.
Why. Department-led, platform-supported. The folder is the home; the department owns the cartridge and its budget.
How to apply. New cartridges start from cartridges/_template/ and land under the owning department. global/ only for cross-department.
Why. Every cartridge backend looks the same: API Gateway HTTP API + Lambda + CloudFront. Every operator who has read one cartridge's backend.tf can read every cartridge's backend.tf.
How to apply. Copy the shape from cartridges/global/chatbot/ (small) or cartridges/administrative/obs/ (full admin). Step 6.j.
Why. Cartridges that skip the manifest are invisible to the platform's cost attribution, retention enforcement, and access policy.
How to apply. Required fields: owner, data classification, retention, OAuth providers, budget. Step 6.i.
*-complete log lines for every Bedrock invocation. Non-negotiableWhy. The obs Cartridges tab attributes cost to users by joining on this schema. A missing line means unattributed cost.
How to apply. logComplete({ msg: '<agent>-complete', user, model, inputTokens, outputTokens, durationMs }) from shared/modules/log.js.
local_file staging. DefaultWhy. Cartridges should not vendor their own copy of shared helpers; drift is the result. One source of truth at shared/modules/.
How to apply. local_file reads from shared/modules/, writes into lambda/_shared/. archive_file picks it up with depends_on. lambda/_shared/ is gitignored. See cartridges/_template/backend.tf.snippet.
Why. A cartridge is mostly business logic. If a cartridge has more plumbing than business logic, the platform has failed to provide the plumbing.
How to apply. When you find yourself writing JWT, refresh, or OAuth flow code inside a cartridge, lift it into shared/modules/. The first cartridge to need a primitive contributes it.
How information moves through the platform.
Why. Without a centralized classification ENUM, every cartridge invents its own.
How to apply. Mutable ENUM today: public, internal, confidential, pii. Default internal. Classification happens at platform ingress boundaries. Step 4.5.a.
Why. Encryption boundary aligns with trust boundary. Compromise of one zone does not leak the others. See P1.5 Locks.
How to apply. Agent, Lockbox, Audit. Cartridges declare cells; substrate maps cell to CMK.
Why. Agents are smart enough to do useful work on tokens. They are also smart enough to leak PII if given the plaintext. The lockbox pattern makes the leak structurally impossible.
How to apply. Agents work on tokens. Reveal requires an explicit authorized call to the lockbox service which logs the reveal.
Why. The lockbox is specced but not built. Until it is, the platform structurally prevents PII handling rather than trusting cartridge code not to leak.
How to apply. Any cartridge declaring pii is gated until lockbox lands.
Why. An auditable trail that operators can modify is not auditable. Object Lock + Audit CMK + separate IAM zone is the design that survives an insider threat.
How to apply. Audit bucket: aws_s3_bucket_object_lock_configuration with COMPLIANCE mode. Audit CMK key policy denies kms:Decrypt to anything except the audit-reader role.
How cartridges talk to outside systems.
Why. If every cartridge implements its own OAuth, the auth surface multiplies. The Console owns the flow; cartridges consume a clean adapter. Step 6.n.
How to apply. OAuth flow lives in shared/modules/oauth/ and the Console Lambda. Cartridges call getOAuthClient(providerId, username).
Why. Mixing the three is the root cause of every OAuth abstraction that ages badly. Distinct on purpose.
How to apply. Provider = SSM manifest. Connection = DDB + Secrets Manager, owned by one user. Consumer = cartridge calling adapter verbs.
Why. Per-cartridge connections multiply the auth surface. Platform-shared connections defeat the provider's audit log. Per-user keeps identity end-to-end aligned.
How to apply. Connection ID = <provider_id>:<username>. Each user manages their own in the Setup tab.
Why. Adding a new provider should be one SSM parameter plus one adapter file. The OAuth flow code never changes.
How to apply. Provider manifest at /forge/oauth/providers/<id>.
Why. A token alone is not an abstraction. Cartridges need typed verbs. The scope gate prevents calling a verb that needs a scope the connection did not get.
How to apply. Per-provider adapter at shared/modules/oauth/adapters/<name>.js. buildAdapter attaches only methods whose scope_required is granted; ungranted methods throw a clear "reconnect from Setup" error.
Why. Lazy refresh hits user-facing latency and races on the expiry boundary. The scheduler isolates refresh from the request path.
How to apply. OAuth refresh sweeper runs every 10 minutes via EventBridge Scheduler. Cartridges read the current valid token; never refresh themselves.
Why. Different attack surfaces, different mechanisms.
How to apply. verifyGithubHmac for webhooks. checkOriginSecret for CloudFront → API Gateway. Do not reuse one for the other.
How AI work is decomposed and routed.
Why. Every AI task has cheap routine steps and expensive high-stakes steps. Paying frontier prices for routine steps is the most common cost mistake.
How to apply. Scout passes use Haiku or local LLM. Writer passes use Sonnet or Opus. The model router substrate makes the choice swappable. Step 6.l, 6.m.
Why. Hard-coding vendor model IDs locks the platform into one vendor's pricing trajectory. Routing as data lets us slide compute from cloud to local without touching cartridge code.
How to apply. Logical route names (scout-local, scout-cloud, writer-opus) map to backend endpoints in substrate/modules/platform/model-router/.
Why. Frontier models matter today; open-source will matter as it catches up. The router absorbs every quality improvement at zero refactor cost.
How to apply. When an open-source model crosses a quality threshold for a route, change the SSM manifest entry. Cartridges do not change.
Why. Claude 4.5 cannot be invoked on-demand against the bare foundation-model ID. The us.anthropic.claude-*-v1:0 inference profile is required.
How to apply. expandModelId() maps short IDs to the inference profile prefix. New cartridges use the same map.
Why. Single-turn AI work either pays for context the writer does not need or skips context the writer does need.
How to apply. Scout narrows context. Fetcher loads it. Writer produces. Optional reviewer adversarially verifies. Step 6.l.
Why. Per-user, per-model, per-cartridge cost visibility is the difference between "this is working" and "this is too expensive to keep."
How to apply. Every Bedrock call emits a *-complete log line via logComplete().
Why. Bedrock guardrails enforce policy at the inference boundary: no PII output, no off-policy topics, content moderation. Cheaper than per-cartridge prompt engineering.
How to apply. guardrailIdentifier and guardrailVersion set on every InvokeModel call. KMS Decrypt grant on the caller role for the guardrail CMK. Step 6.j.
How changes get from PR to production.
Why. Auto-apply on merge is how shops accidentally deploy unreviewed Terraform on a Friday night.
How to apply. GitHub Actions OIDC role; workflow_dispatch for Terraform Apply; required input confirm = "APPLY". Step 3a.
Why. The boundary is the trap door. Changes cross trust zones.
How to apply. Permission boundary policy in substrate/bootstrap/. Changes route to platform-team CODEOWNERS.
Why. Cron jobs that silently miss runs are how operational visibility erodes.
How to apply. Every scheduled Lambda is wrapped with wrapScheduledHandler. Watchdog runs every 30 minutes against the SSM manifest of expected jobs. Step 6.k.
Why. Cost Explorer charges per query and is slow.
How to apply. 48h hard TTL for daily-rollup data (narrative); per-query freshness for the Cartridges tab "Calculate now" button.
Why. Mixing "declare the cert" with "wait for DNS to validate it" with "attach the cert" produces an apply that hangs forever.
How to apply. PR 1 declares (apply succeeds, DNS records emitted). DNS admin adds records. PR 2 attaches the cert and wires SES into Cognito.
Why. If a multi-agent workflow bounds coverage with top-N, sampling, or no-retry, the operator must know. Otherwise a "complete" review reads as "we covered everything" when it did not.
How to apply. log() what was dropped. Reviewers see the cap explicitly.
Why. External cartridges depend on stable references. A clear release event lets consumers upgrade deliberately rather than chase main. A clean tag is also what pull-sdk.sh resolves against.
How to apply. ./console/sdk/release-sdk.sh {major|minor|patch|X.Y.Z} validates clean working tree, bumps VERSION and forge-sdk-manifest.json, commits, tags sdk-vX.Y.Z. Does NOT auto-push. Operator pushes main and the tag separately so the release goes through review. See Step 6.o.
How decisions get made.
Why. Cartridges built without a design sheet end up costing three times what was budgeted, touching data classifications they did not declare, and integrating with systems whose OAuth nobody has set up.
How to apply. Before the Forge runs, an Anvil sheet exists with: purpose, audience, external systems, OAuth providers, data classification, capabilities, cost estimate, risk callouts. P4 Anvil.
Why. AI produces code fast; that does not make it correct.
How to apply. Branch protection requires CODEOWNERS review. Apply requires literal APPLY typed by a human. AI scales the writing; humans gate the landing.
Why. Chasing functional outcomes with shippers and branches is what hardens the substrate. Building the Enclave first wastes time on theater.
How to apply. Cartridges target real users and real workflows. Substrate hardening follows from production use.
Why. Competitors who match feature velocity inherit one vendor's pricing power as a permanent dependency. Substrate-first looks slower for two quarters and then compounds. In financial compliance the damage is irreversible.
How to apply. When tempted to skip substrate to chase a feature, ask whether the cartridge ships without it. If yes, ship; substrate work compounds in parallel. If no, substrate was load-bearing all along.
Why. A bug fix does not need surrounding cleanup. Three similar lines is better than a premature abstraction. Half-finished features ship as full features and rot.
How to apply. Scope to the task. File follow-ups for cleanup.
Why. Defensive error handling for impossible scenarios is code that will never run and will distract a reader.
How to apply. Validate at user input, external APIs, and provider responses. Inside the platform, trust the framework.
Why. Strategic narrative is a different discipline from engineering.
How to apply. Memos to the CEO or board go through Mike Lewis first. Engineering-side docs go through CODEOWNERS.
Why. SSO failure modes (Azure outage, expired SAML signing certificate, IdP misconfiguration, federation incident) must not lock the platform team out. A Cognito-native admin track that exists outside the federation is the recovery path. System admins are not break-glass-only; they are the platform's administrative track that also covers break-glass.
How to apply. At least two Cognito-native accounts in the admins Cognito group. TOTP MFA enforced. Used routinely (approve Request Roles, debug cartridges, incident response). Verified quarterly. SSO bypass is the design feature, not a workaround. See Step 6.p.
Why. Once doctrine + skill + docs live in the CMS, the CMS becomes critical-path infrastructure. A CMS outage cannot leave system admins without doctrine to reference, agents without skills to load, or consumers without docs to read. The git copy stays canonical-equivalent at every published version and serves as both the disaster-recovery cache and the cold-start bootstrap for the CMS itself.
How to apply. Every CMS publish triggers a git mirror update (system admin reviews and merges). System admins maintain at least one current local checkout. During a CMS incident, the MCP server falls back to the git mirror for read paths. Quarterly verification: diff CMS published content against the git mirror; divergence triggers investigation. Same parallel-track shape as Doctrine 8.8: when the primary path is down, the recovery path is alive. See Step 6.s.
The shape of things that get rejected. Each one has a one-line failure mode and the doctrine that replaces it.
Why it fails. SigV4 body-hash mismatch silently 403s POST/PUT. Lost an afternoon on the obs budget-save bug.
Replace with. API Gateway HTTP API + X-Origin-Secret. Console 2.3.
Why it fails. Multiplies the auth surface. Operators connect Salesforce N times. The IAM permission boundary already enforces scope; app-level repeat is theater.
Replace with. Shared OAuth in the Console, per-user connections. OAuth 5.1, 5.3.
Why it fails. Supply chain surface; version drift; audit story we have to defend.
Replace with. Lift the primitive into shared/modules/. Cartridge 3.1.
aws/...).Why it fails. Cannot answer "who decrypted what." Cannot revoke decrypt without redeploying.
Replace with. Customer-managed CMK. Foundation 1.6.
Why it fails. Compromise of any role leaks everything. Audit story fails.
Replace with. Three CMKs per trust zone. Foundation 1.4, P1.5.
Why it fails. Vendor lock-in; cannot swap to a cheaper or local model without changing every cartridge.
Replace with. Model router substrate; logical route names. AI 6.2.
Why it fails. Operators read a "complete" report and assume coverage. The cap hides the gap.
Replace with. Log the cap. Operational 7.6.
Why it fails. Department ownership is structural. A sales cartridge under engineering/ signals confusion about whose budget it lives in.
Replace with. Cartridge under the owning department. Cartridge 3.2.
Pre-PR by Claude Code via the agent skill at .claude/skills/forge-sdk/SKILL.md. At PR time via CODEOWNERS review. At apply time via the literal APPLY confirm. At runtime via the Foundation guardrails (IAM permission boundary, KMS key policies, Bedrock guardrails, Network Firewall, GuardDuty).
Markdown source: Doctrine.md at the repo root.