# Metadata Control Plane Architecture RFC

**Status:** Draft  
**Date:** 2026-04-14  
**Scope:** Whole-picture architecture for metadata control, managed platform metadata, tenant-authored metadata, policy metadata, derived projections, and bootstrap positioning  
**Audience:** Platform/backend engineers, operators, reviewers of `#822`, `#798`, `#815`, `#826`, `#827`, `#828`

---

## 1. Summary

NexusCRM should stop treating bootstrap as the center of its metadata architecture.

The correct target is a **Metadata Control Plane** with clear boundaries between:

1. metadata substrate
2. persisted runtime metadata
3. metadata authoring
4. managed metadata packs
5. policy metadata
6. derived projections
7. runtime read/enforcement

In that model:

> **bootstrap is not the metadata control plane.**  
> **bootstrap is a small set of initialization operations inside the metadata control plane.**

The system should therefore be refactored around the metadata control plane first, and only then should bootstrap be redefined as:

- substrate bring-up
- initial managed-pack application

This RFC defines the whole-picture architecture.  
The companion execution/sub-RFC is:

- [Bootstrap Execution and Metadata Control Plane Extraction RFC](./bootstrap-metadata-control-plane-rfc.md)

That companion document covers execution classes, extraction mapping from `main.go`, and first-phase implementation boundaries.

---

## 2. Problem Statement

The current system has three overlapping problems:

### 2.1 Bootstrap has become a surrogate control plane

Application startup currently performs:

- schema/materialized metadata initialization
- drift repair
- compatibility backfills
- product seed/config application
- derived-state rebuild

This makes startup carry responsibilities that should belong to an explicit control plane.

### 2.2 The runtime contract is spread across multiple authorities

Today metadata behavior is determined by a combination of:

- source JSON files in `backend/internal/bootstrap/*.json`
- persisted `_System_*` rows
- startup reconcile/update logic
- runtime overlay logic

This is not a clean metadata-driven model. It is a multi-authority model.

### 2.3 The repository already contains half of a metadata plane, but it is incomplete

The system already has real metadata capabilities:

- persisted metadata tables
- `MetadataService`
- object/field/layout/flow/setup CRUD surfaces
- cache/snapshot loading

The missing piece is not “invent metadata control from scratch.”  
The missing piece is to:

- recognize the existing authoring plane
- extend it to managed/platform metadata
- separate policy metadata from derived projections
- stop using startup as the place where the system becomes correct

### 2.4 There are already concrete violations of the target model

Two current implementation gaps matter because they directly contradict the intended direction:

1. **Runtime managed-metadata overlays** currently rewrite in-memory object and relationship semantics during cache construction instead of treating persisted `_System_*` rows as the sole runtime authority.
2. **Permission Set Group muting is incomplete**: system-permission muting exists, but CRUD/FLS effective-permission aggregation does not yet implement PSG-scoped muting semantics.

These are not arguments against the architecture. They are evidence that the control plane still needs to be completed.

---

## 3. Goals

1. Define the complete metadata control-plane architecture.
2. Clarify the role of bootstrap within that architecture.
3. Establish a single runtime metadata authority.
4. Integrate managed/platform metadata into the same overall control-plane model as tenant-authored metadata.
5. Distinguish policy metadata from derived projections.
6. Create a phased path to implementation without rewriting the system from scratch.
7. Preserve tenant/admin customization during managed upgrades.
8. Make read-only startup depend on a genuinely blocking verify gate, not warning-only health logs.

---

## 4. Non-Goals

1. Replacing every metadata implementation detail immediately.
2. Rebuilding `MetadataService` from zero.
3. Solving every security/sharing design problem in this RFC.
4. Defining a full UI/Setup product experience in detail.
5. Eliminating all JSON-based authoring inputs on day one.

---

## 5. Core Architectural Decision

NexusCRM should be organized around the following model:

```text
 Authoring Inputs / Packs / Admin APIs
                 |
                 v
     +-----------------------------+
     |   Metadata Control Plane    |
     |  apply / verify / repair /  |
     |  rebuild / job history      |
     +-----------------------------+
                 |
                 v
     +-----------------------------+
     |  Persisted Runtime Metadata |
     |      (_System_* rows)       |
     +-----------------------------+
                 |
                 v
     +-----------------------------+
     |  Runtime Read / Enforcement |
     |   API / UI API / Query      |
     +-----------------------------+
```

This yields two global rules:

### Rule 1: The control plane mutates metadata

Metadata mutation happens through explicit operations:

- apply
- verify
- repair
- rebuild
- admin/authoring actions

### Rule 2: The runtime reads persisted metadata

Runtime services:

- read persisted metadata
- load snapshots/caches
- enforce policy
- render layout/runtime views

They do not repair, reinterpret, or silently mutate metadata during startup.

This rule only becomes safe once verify-time checks are strong enough to block startup on broken or drifted persisted metadata. A warning-only readiness check is not an adequate substitute.

---

## 6. Whole-Picture Layers

These layers are **conceptual architecture boundaries**, not a claim that Phase 1 must ship seven new services or seven new packages.

Some of them will share implementations for a while. The point of the layering is to preserve authority and lifecycle boundaries that the current system blurs.

Two cross-cutting constraints apply to every layer:

1. **Provisioning and upgrade are different operations.** First creation may fill defaults aggressively; later managed upgrades must preserve tenant/admin customizations unless a field is explicitly immutable.
2. **Legacy repair heuristics must not become permanent runtime behavior.** Value-based escape hatches are acceptable only as narrowly-scoped migrations with a retirement path.

## 6.1 Layer A: Metadata Substrate

This is the irreducible foundation that allows the metadata system itself to exist.

Examples:

- `_System_Object`
- `_System_Field`
- `_System_Relationship`
- `_System_PageLayout`
- job/state tables such as `_System_BootstrapState` or its successor
- any other metadata-registry substrate tables

Properties:

- platform-owned
- minimal
- stable
- cannot depend on higher-level metadata for its own existence

This is the only place where a “true bootstrap substrate” is unavoidable.

---

## 6.2 Layer B: Persisted Runtime Metadata Store

This is the metadata authority that the runtime consumes.

It includes persisted representations of:

- objects
- fields
- relationships
- layouts
- record types
- setup/UI metadata
- policy metadata

Properties:

- runtime source of truth
- queryable
- versionable/drift-checkable
- stable enough to drive runtime enforcement and rendering

The runtime should not simultaneously depend on source JSON, startup reconcile code, and runtime overlays once this layer is authoritative.

---

## 6.3 Layer C: Metadata Authoring Plane

This layer already partially exists in the repository.

Current examples:

- `MetadataService`
- object CRUD
- field CRUD
- layout saves/restores
- setup/UI metadata upserts
- flow metadata CRUD

The correct direction is to recognize this as the **canonical authoring plane** and extend it, rather than building an unrelated second system.

This layer should own:

- tenant-authored metadata
- admin-authored metadata
- future managed/platform metadata application hooks

---

## 6.4 Layer D: Managed Metadata Packs

This is the missing layer in the current system.

Today the repository contains multiple de facto pack-like artifacts:

- `system_tables.json`
- `standard_objects.json`
- `standard_layouts.json`
- `standard_actions.json`
- `themes.json`
- `ui_components.json`
- `setup_pages.json`
- `record_types.json`
- `system_data.json`

They are not yet a coherent control-plane concept.

This RFC proposes formalizing them as **managed metadata packs**.

Phase 1 does not need to operationalize all pack categories independently on day one.

It is acceptable to begin implementation with broader execution families such as:

- platform/schema-oriented managed packs
- config/seed-oriented managed packs

as long as the architecture keeps enough structure to prevent the current concern-mixing from collapsing back into a single opaque bootstrap bucket.

### Current state: split-brain authoring model

**Implementation note (2026-04-14):** Phase 1 implemented the pack execution model. Seed/config JSON files and functions have been moved to `controlplane/packs/`. But the **authoring model** for standard objects remains DDL-manifest-first, not metadata-first. See Section 13 for the full diagnosis and the recommended next RFC.

The original overlap between `system_tables.json` and `standard_objects.json` has been partially addressed — `standard_objects.json` seed data moved to packs. But `system_tables.json` still mixes Layer A substrate (system metadata tables) and Layer D managed metadata (standard business objects) in a single DDL-manifest file with physical SQL types.

The long-term pack model requires:

1. Substrate table definitions (Layer A) separated from managed object pack specs (Layer D)
2. Pack specs expressed in logical metadata types (not physical SQL types)
3. Standard objects created through shared authoring primitives with custom objects
4. A single pack spec as the sole authority for each managed standard object (collapsing `system_tables.json` entries + `standard_objects.json` + `managed_permissions.go` Go maps)

### Managed pack categories

#### 1. Platform Core Pack

Platform-owned metadata required for the CRM platform to behave correctly:

- standard objects
- managed relationship semantics
- standard object access policy
- standard built-in layout defaults
- core record-type baselines

#### 2. Setup/UI Pack

Platform-owned setup/user-interface metadata:

- setup pages
- UI components
- themes
- navigation/setup affordances

#### 3. Automation Pack

Platform-owned automation metadata:

- standard flows
- standard actions
- other automation defaults

#### 4. Security Policy Pack

Platform-owned policy metadata:

- system permission catalog
- baseline profiles
- default object policy
- managed sharing semantics

#### 5. Local/Dev Sample Pack

Strictly non-production convenience pack:

- local sample data
- dev/test-only identities
- local development conveniences

This pack must be explicitly isolated from platform metadata and secrets.

### Required pack capabilities

Managed packs should eventually have:

- typed schema
- version
- desired-state representation
- diff
- verify
- apply
- drift reporting
- standardized idempotent reconciliation semantics rather than today's mix of `INSERT IGNORE`, cleanup-based bootstraps, and ad hoc conditional updates

In the near term, the control plane should assume **idempotent recovery before atomic deployment**. The current system has strong restart-safe patterns, but not a universal transaction model for multi-step managed apply. Job design and operator expectations should reflect that reality.

One immediate implication of this model is that **runtime overlays must eventually disappear**. If a managed pack is authoritative, its desired state should be reconciled into persisted metadata, verified there, and then read back by runtime services without hardcoded in-memory rewrites.

Another immediate implication is that managed-pack apply must distinguish:

- **provisioning**: first materialization of platform-owned metadata/config
- **upgrade/reconcile**: later evolution of already-provisioned metadata/config

The latter must preserve tenant/admin customization by default. Otherwise managed packs would behave unlike Salesforce-style managed upgrades and would keep reintroducing bootstrap overwrite bugs.

One current-state caveat is especially important here:

the existing object-level `IsCustomized` flag is **not yet a general managed-upgrade protection bit**. In current code it is reliably set for some changes such as `SharingModel`, but not for all admin-edited object attributes such as label/description/icon. Managed-pack upgrade protection therefore cannot assume that today's single boolean captures all tenant customization intent.

---

## 6.5 Layer E: Policy Metadata

This layer must be distinguished from derived state.

Authoritative policy metadata includes:

- Profiles
- Permission Sets
- ObjectPermissions
- FieldPermissions
- OWD / sharing settings
- sharing rules
- layout assignments
- record-type assignments

Important clarification:

These are **not merely caches**.  
They are part of the metadata/control-plane source of truth.

This is the correct Salesforce-aligned interpretation:

- policy is declarative metadata
- runtime evaluates effective access from policy
- derived materialization may exist for performance, but policy itself is authoritative

Important current-state note:

The existing `bootstrap.InitializePermissions(...)` implementation should still be understood as **seed/config application of baseline policy rows**, not as a pure derived rebuild. It creates and backfills `ObjectPermissions` and `FieldPermissions` rows because the future declarative policy-pack model does not yet exist.

Only after baseline policy metadata is formalized as a first-class managed pack should any later permission/share materialization be reclassified under Layer F.

Another important current-state note:

The repository currently implements **PSG muting for system permissions only**. CRUD/FLS muting is not yet represented end to end. The architecture should therefore treat complete PSG-scoped muting as unfinished policy-model work rather than assuming it already exists in the effective-permission layer.

Likewise, some current managed-policy repair logic still relies on value-based legacy heuristics. Those heuristics should be treated as migration debt to retire, not as the steady-state policy model.

---

## 6.6 Layer F: Derived Projections

Derived projections are rebuildable artifacts computed from authoritative metadata and policy.

Examples:

- metadata cache snapshots
- formula views
- materialized permission/share projections
- query/runtime indexes
- other computed representations

Properties:

- rebuildable
- not the primary correctness source
- safe to invalidate and recompute
- should have explicit rebuild operations

This layer is where many current startup-time mutations really belong.

---

## 6.7 Layer G: Runtime Read / Enforcement Plane

This is the request-serving plane.

Responsibilities:

- read persisted metadata
- load runtime snapshots/caches
- render describes/layouts/object info
- enforce CRUD/FLS/record access/RLS
- execute runtime business logic

Non-responsibilities:

- drift repair
- pack application
- bootstrap mutation
- historical compatibility migration

---

## 7. The Correct Role of Bootstrap

The term “bootstrap” should be narrowed.

Bootstrap should mean only:

### 7.1 Substrate bootstrap

Bring up the minimal metadata substrate required for the platform to exist.

### 7.2 Initial managed-pack application

Apply the minimum platform-owned managed metadata packs needed for the system to reach a runtime-ready state.

Bootstrap should **not** be the catch-all label for:

- historical repair
- product config packs
- sample data
- cache rebuild
- formula view maintenance
- permission correctness backfills

So the correct statement is:

> bootstrap is a small operation family inside the metadata control plane, not the metadata control plane itself.

---

## 8. The Correct Service Model

The right path is not to replace `MetadataService`.

The right path is:

### 8.1 Keep `MetadataService` as the canonical authoring/runtime metadata service

It already provides:

- CRUD for many metadata types
- metadata caching/snapshots
- layout access and mutation
- schema/field CRUD orchestration

### 8.2 Add a higher-level control-plane orchestration layer

Conceptually, the missing piece is a control-plane orchestration layer that owns:

- pack verify/apply
- repair/migration execution
- derived rebuild
- job/history/state
- environment/mode policy

This orchestration layer should sit **above** `MetadataService`, not beside it as a duplicate metadata subsystem.

In Phase 1, it does not need to start life as a heavyweight `MetadataControlService` struct. A package-first implementation such as `internal/controlplane/...` is acceptable if it cleanly owns the sequencing and authority boundaries.

The important constraint is architectural, not object-oriented:

- one place owns verify/apply/repair/rebuild sequencing
- that layer may construct a **headless control-plane runtime** using the same infrastructure dependencies and service graph that the server already uses
- it must not boot HTTP handlers, background workers, or other request-serving runtime loops
- it should be able to suppress runtime event-driven side effects when executing headless control-plane work

---

## 9. Recommended Control-Plane Operations

The long-term control plane should expose explicit operations such as:

- `bootstrap-substrate`
- `apply-managed-pack`
- `apply-config-pack`
- `verify`
- `run-repair`
- `rebuild-derived`

These can later be implemented as:

- CLI commands
- job runners
- backend services
- admin APIs
- operator UI actions

But the operation model should be explicit regardless of delivery mechanism.

---

## 10. Relationship to the Existing Bootstrap RFC

The companion RFC:

- [Bootstrap and Metadata Control Plane RFC](./bootstrap-metadata-control-plane-rfc.md)

should be treated as an **implementation and extraction sub-RFC**.

It covers:

- execution classes
- extraction mapping from `main.go`
- environment/runtime boundary tightening
- first-phase backend extraction

It does **not** define the whole metadata control-plane architecture by itself.

---

## 11. Implementation Strategy

## Phase 1: Establish the control-plane architecture — IMPLEMENTED

Status: **Implemented** (see `internal/controlplane/`, `cmd/setup/`)

Delivered:

- `internal/controlplane/` package: Runner (headless with NoOpEventBus), operations (ApplyMetadata, ApplyPacks, EnsureIndexes, RebuildDerived, Verify, ApplyAll), pack model (PlatformPolicy, PlatformConfig, PlatformUX, DevSample)
- `cmd/setup` CLI: apply, apply-metadata, apply-packs, ensure-indexes, rebuild, verify
- `cmd/server` is read-only: verify → cache → serve, never mutates metadata
- Runtime overlays removed (4 call sites); persisted metadata is sole runtime authority
- Verify gate hardened: VerifySchemaReadiness returns real errors + 2 managed drift assertions
- EventBus widened to ports.EventPublisher for injectable headless mode
- system_data.json split into profiles.json + bootstrap_identity.json + dev_connected_apps.json
- InitializeThemes/UIComponents/SetupPages accept MetadataService directly (no ServiceManager bridge)

## Phase 2: ~~Extract execution boundaries~~ (merged into Phase 1)

Phase 2 deliverables were achieved as part of Phase 1 implementation:

- startup mutation work moved behind controlplane operations
- runtime startup is read-only
- `MetadataService` retained as authoring/runtime core

## Phase 3: ~~Formalize managed metadata packs~~ — IMPLEMENTED (merged into Phase 1)

- ✅ 4 pack categories: PlatformPolicy, PlatformConfig, PlatformUX, DevSample
- ✅ JSON data and seed logic owned by respective packs (not bootstrap/)
- ✅ verify/apply per pack via Pack interface
- ✅ Sample data (DevSample) separated from platform metadata and excluded from Verify

## Phase 4: ~~Clarify policy metadata vs derived projections~~ — PARTIALLY IMPLEMENTED

- ✅ Policy metadata (profiles, permissions, sharing) owned by PlatformPolicy pack
- ✅ Derived projections (formula views, cache) owned by RebuildDerived operation
- ⚠️ Full reclassification of InitializePermissions (seed vs derived) deferred
- ⚠️ PSG muting model completion deferred

## Phase 5: Operator surface — NOT STARTED

Deliverables (future):

- HTTP API endpoints for control-plane operations
- job/drift model with persistence
- thin operator/admin UI

---

## 12. Immediate Consequences

If this RFC is accepted, several earlier instincts need to be corrected:

1. **We should not treat bootstrap cleanup as a sufficient end state.**
2. **We should not describe the current bootstrap RFC as the full metadata control-plane architecture.**
3. **We should not create a new parallel metadata subsystem when `MetadataService` already exists.**
4. **We should not demote policy metadata to “just caches.”**
5. **We should not keep runtime startup as the hidden place where the system becomes correct.**

---

## 13. Identified Gap: Split-Brain Authoring Model

The control plane extraction (Phase 1) resolved execution boundaries — `cmd/server` is read-only, `cmd/setup` is the sole write path, packs own their data.

But Phase 1 did **not** resolve the fundamental authoring model split-brain that exists in the codebase:

### 13.1 The two authoring paths

**Custom objects (metadata-first):**

```
API call → MetadataService.CreateSchema()
  → receives ObjectMetadata (logical types: text, currency, reference, ...)
  → PrepareTableDefinition() maps logical → physical types
  → CreateTableWithStrictMetadata() executes DDL
  → registers _System_Object + _System_Field rows
```

Code path: `metadata_schema.go:15` → `metadata_schema_helpers.go:17` → `schema_ddl_ops.go:189`

**Standard/managed objects (DDL-manifest-first):**

```
system_tables.json (physical SQL types: VARCHAR(255), DECIMAL(18,2), ...)
  → GetManagedTableDefinitions() merges system_tables.json + standard_objects.json
  → InitializeSchema() creates physical tables from DDL manifest
  → backfills _System_Object + _System_Field rows afterward
```

Code path: `system_tables.go:14` → `standard_objects.go:17` → `schema.go:895`

These two paths produce the same result (physical table + metadata rows) but from opposite directions. The custom path starts from logical metadata. The managed path starts from a physical DDL manifest.

### 13.2 Why this is a problem

1. **Pack specs express physical SQL types instead of logical metadata.** `system_tables.json` and `standard_objects.json` declare `VARCHAR(255)`, `DECIMAL(18,2)`, `TEXT`. These are DDL-manifest artifacts, not logical metadata specifications. A metadata-driven pack spec should declare `type: “text”`, `type: “currency”`, `type: “reference”` — the same vocabulary the custom object path already uses.

2. **Three overlapping authorities for the same metadata.** Standard object sharing models are declared in `standard_objects.json`, hardcoded in `managed_permissions.go` Go maps, and reconciled by `schema.go`. A single pack spec should be the sole authority.

3. **`system_tables.json` is a cross-layer mixed file.** It contains both substrate definitions (`_System_Object`, `_System_Field` — Layer A tables that must exist before any metadata) and standard business objects (`Account`, `Contact` — Layer D managed metadata). These are different architectural layers with different lifecycles.

### 13.3 The correct direction

The core principle:

> **Metadata-first means the authoring truth is a logical metadata specification, not a physical DDL manifest.**

“Metadata-first” does **not** mean “write committed `_System_Object` rows before DDL.” In a non-transactional DDL environment (TiDB/MySQL), writing runtime-visible metadata before physical materialization creates phantom metadata on DDL failure.

The correct materialization model has three stages:

1. **Authoring spec** — pack definition / managed definition / admin-edited metadata (logical, not physical)
2. **Apply state** — control-plane job tracking: pending → in-progress → applied / failed
3. **Committed runtime metadata** — only metadata that has been successfully materialized (physical table exists, columns exist, metadata rows consistent)

Runtime only sees stage 3. Stage 1 and 2 are control-plane internal.

### 13.4 Target state

**Substrate (Layer A):** Small, stable set of `_System_*` table definitions. Can be Go code or a dedicated substrate JSON — either way, this is pure DDL for the metadata container itself. Not business metadata.

**Managed metadata packs (Layer D):** Platform-owned object/field/policy/relationship/layout pack specs expressed in **logical metadata vocabulary** (not physical SQL types). Applied through the control plane, which:
- reads the pack spec (logical)
- derives physical DDL from logical types (same mapping the custom object path already uses)
- materializes tables + columns
- commits metadata rows to `_System_*` store
- all within a tracked apply operation that can succeed or fail cleanly

**Standard objects and custom objects converge:** Both should be authored in the same logical metadata vocabulary. The only difference is the trigger — platform pack apply vs. admin API call. The underlying authoring primitives (logical type → physical DDL, metadata registration, layout generation) should be shared.

### 13.5 Current `CreateSchema()` is not directly reusable for managed objects

The existing `MetadataService.CreateSchema()` is custom-object-oriented:
- `metadata_schema_helpers.go:29` hardcodes `tableType` to `custom_object`
- `metadata_schema_helpers.go:35` auto-injects custom-style system fields
- `metadata_schema.go:57` auto-generates a default layout
- `metadata_schema.go:69` fires `ObjectCreated` event

The correct approach: extract the **authoring primitives** (logical type mapping, DDL generation, metadata registration) into shared functions that both the custom-object API path and the managed-pack apply path can use, with appropriate parameterization for tableType, system field injection, layout generation, and event firing.

### 13.6 What needs to change

| Current | Target | Scope |
|---------|--------|-------|
| `system_tables.json` mixes substrate + business objects | Split: substrate definitions (Layer A) separate from managed object pack specs (Layer D) | File restructuring + schema.go refactoring |
| `standard_objects.json` declares physical SQL types | Pack spec declares logical metadata types | Pack spec format redesign |
| `managed_permissions.go` hardcodes sharing model Go maps | Sharing model / policy defaults part of the pack spec | Pack spec enrichment |
| `InitializeSchema()` reads DDL manifest, creates tables, backfills metadata | Pack applier reads logical spec, derives DDL, materializes, commits metadata | New apply pipeline |
| Custom and managed object creation use different code paths | Shared authoring primitives, different trigger sources | MetadataService refactoring |

### 13.7 Recommended next step

This should become a dedicated RFC:

> **Metadata Authority and Authoring Model RFC**

It must answer:

1. What is the authoring truth for managed standard objects? (logical pack spec, not DDL manifest)
2. What does the pack spec logical schema look like? (field types, policies, relationships — no VARCHAR/DECIMAL)
3. How does apply avoid pending/failed metadata leaking to runtime? (apply state tracking, rollback)
4. How do MetadataService / SchemaManager / control plane divide responsibility for the shared authoring primitives?

This RFC is the bridge between “control plane extraction” (done — Phase 1) and “true metadata-driven platform” (not done — requires resolving the split-brain authoring model).

### 13.8 Implementation reference: Terraform state model

The Phase 1 follow-up items — multi-pod locking, apply-state persistence, pack-signature verify, dry-run (plan), and idempotency testing — are not five independent problems. They are five symptoms of one missing architectural primitive: a **persisted apply-state layer**.

The Terraform + Atlantis model provides a concrete reference:

| Terraform Concept | NexusCRM Equivalent | What It Closes |
|---|---|---|
| State backend locking (DynamoDB/Consul) | `GET_LOCK('nexuscrm_controlplane_apply', 600)` (Phase 1) → persistent state-based lock (Phase 2) | Multi-pod concurrency control |
| State file (terraform.tfstate) | `_System_ControlPlaneState` table: pack versions, apply timestamps, result, checksums | Apply-state persistence + pack-signature verify |
| `terraform plan` on clean state | `cmd/setup plan` — compare desired pack spec against persisted state, show diff without applying | Dry-run / plan operation |
| `plan` output is empty after successful apply | Running `plan` immediately after `apply` produces no changes | Idempotency verification |

The key insight: adopting this shape (state backend + state file + plan) would close five follow-ups with one architectural move rather than five separate patches.

**Recommended Phase 2 primitive:** A `_System_ControlPlaneState` table that records:
- pack name + version (content hash of pack spec JSON)
- last apply timestamp + result (success/failure)
- applied-by (operator / CI job ID)
- state checksum (hash of the committed metadata rows for drift detection)

With this primitive:
- `cmd/setup apply` writes state after successful apply
- `cmd/setup plan` compares desired spec against recorded state
- `cmd/setup verify` checks state checksums against actual DB rows
- Multi-pod locking upgrades from advisory lock to state-based locking
- Idempotency test = `plan` after `apply` produces empty diff

---

## 14. Recommendation

Adopt this document as the top-level architecture RFC for the work currently tracked under `#822` and related issues.

Phase 1 (control plane extraction) is implemented:
- [Bootstrap Execution and Metadata Control Plane Extraction RFC](./bootstrap-metadata-control-plane-rfc.md)

The next phase should address the authoring model split-brain identified in Section 13:
- Metadata Authority and Authoring Model RFC (to be written)

The most important conceptual shifts, in order:

> **Phase 1 (done): “The real project is not 'fix bootstrap.' It is 'complete the metadata control plane.'”**

> **Phase 2 (next): “The real project is not 'reorganize JSON files.' It is 'unify managed and custom object authoring into a single metadata-first model.'”**
