# Bootstrap Execution and Metadata Control Plane Extraction RFC

**Status:** Draft  
**Date:** 2026-04-14  
**Scope:** Execution-boundary extraction from current startup/bootstrap into explicit control-plane operation classes  
**Audience:** Platform/backend engineers, operators, reviewers of `#822`, `#798`, `#815`, `#826`, `#827`, `#828`

---

## 1. Summary

This RFC is a **subordinate implementation RFC**.

The top-level architecture document is:

- [Metadata Control Plane Architecture RFC](./metadata-control-plane-architecture-rfc.md)

That document defines the whole-picture target architecture.

This RFC focuses on a narrower problem:

> how to extract the current `main.go` startup/bootstrap sequence into explicit execution classes inside the future metadata control plane.

NexusCRM currently overloads application startup with five different responsibilities:

1. runtime service initialization
2. authoritative metadata materialization
3. seed/config application
4. historical repair and migration
5. derived-state rebuild

This RFC proposes an execution model that separates those responsibilities into explicit classes and makes request-serving startup read-only with respect to metadata.

The core principle is:

> **Setup mutates metadata. Runtime reads metadata.**

The system should stop using request-serving startup as an implicit metadata repair and seed engine.

This RFC does **not** fully define:

- metadata authority model
- managed-pack architecture
- policy metadata model
- the complete metadata control-plane service model

Those belong to the top-level architecture RFC.

---

## 2. Problem Statement

Today the startup path in [`backend/cmd/server/main.go`](../../backend/cmd/server/main.go) performs a large amount of heterogeneous work:

- schema initialization and reconciliation
- managed metadata repairs
- physical-table discovery
- field and lookup backfills
- layout migration and related-list backfill
- product seed data creation
- permission initialization
- system-permission seeding
- folder/theme/UI/setup-page/flow initialization
- assertion checks

This creates several architectural problems:

### 2.1 Startup is both runtime and control plane

The request-serving process is currently responsible for:

- verifying readiness
- mutating metadata
- repairing drift
- running historical compatibility work
- seeding product-owned configuration

That makes startup behavior difficult to reason about and difficult to operate safely.

### 2.2 Multiple authorities compete at runtime

Today the live contract is spread across:

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

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

### 2.3 Metadata, configuration, data, and secrets are mixed

Files like [`backend/internal/bootstrap/system_data.json`](../../backend/internal/bootstrap/system_data.json) currently mix:

- system identities
- user/bootstrap data
- environment-dependent behavior
- default credential behavior
- connected-app setup
- permission-related side effects

These concerns should not share the same lifecycle.

### 2.4 Historical repair work is treated as normal bootstrap

Backfills and compatibility cleanups currently live in the same startup sequence as authoritative metadata initialization. This makes “bootstrap” a catch-all bucket instead of a well-defined control-plane surface.

### 2.5 Derived state is mixed with authoritative state

Formula views, caches, and materialized permission/share state are conceptually rebuildable artifacts. They should not be treated as the same class of operation as authoritative metadata apply.

### 2.6 There are already concrete runtime contradictions to account for

This RFC is not starting from a clean baseline. Two current implementation gaps matter because they directly contradict the intended target model:

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

These should be handled explicitly during implementation rather than assumed away.

---

## 3. Goals

1. Make request-serving startup read-only with respect to metadata.
2. Define explicit control-plane execution classes instead of a single opaque “bootstrap”.
3. Separate authoritative metadata apply from seed/config apply, repair/migration, and derived rebuild.
4. Establish one runtime metadata authority.
5. Make drift visible and explicit.
6. Preserve developer velocity in local/dev without embedding repair behavior in the runtime server.
7. Provide a migration path that can be implemented incrementally.
8. Make verify mode a genuinely blocking gate before relying on read-only startup.
9. Preserve tenant/admin customization during managed config and policy upgrades.

---

## 4. Non-Goals

1. Replacing every existing bootstrap JSON file immediately.
2. Rewriting the full permission/sharing engine in the first phase.
3. Building a full Salesforce-Setup-equivalent UI before backend boundaries are defined.
4. Eliminating all materialized caches or derived rows.
5. Removing all bootstrap logic from the repository.

---

## 5. Salesforce-Aligned Design Principles

The proposed model is based on the following principles:

### Principle 1: Setup mutates metadata; runtime reads metadata

Metadata changes belong to an explicit control plane, not to request-serving startup.

Corollary: a read-only runtime startup is only safe when verify-time checks are strong enough to block startup on broken or drifted persisted metadata.

### Principle 2: One runtime metadata authority

Runtime should read one persisted metadata authority. Authoring sources may exist elsewhere, but runtime should not require agreement across source JSON, startup reconcile, and runtime overlays.

### Principle 3: Metadata, configuration metadata, seed data, and secrets are different things

They should have different storage, review, deployment, and operational models.

### Principle 4: Declarative policy first; derived state second

This applies to:

- security policy
- sharing semantics
- layout structure
- assignment/defaulting rules

The policy is the source of truth. Derived state is rebuildable.

### Principle 5: Derived state must be explicitly rebuildable

Formula views, caches, snapshots, and materialized permission/share rows are derived artifacts, not the primary correctness source.

### Principle 6: Repair/migration is not normal bootstrap

Historical backfills and compatibility fixes should be explicit repair/migration jobs.

### Principle 6a: Provisioning and upgrade are different

The first creation of platform-owned metadata/config is not the same thing as a later managed upgrade.

Provisioning may create defaults. Upgrade/reconcile must preserve tenant/admin customization by default, unless a field is explicitly immutable.

### Principle 7: Structure over labels

Display labels are presentation, not semantic identity. This is especially important for layout sections such as “System Information”.

### Principle 8: Platform-owned built-ins and tenant-defined metadata must remain distinct

Standard/system metadata and custom/tenant metadata can share a runtime representation, but they do not have the same authoring lifecycle.

---

## 6. Proposed Execution Model

Instead of a single overloaded “bootstrap”, the system should expose five execution classes.

These are **conceptual execution distinctions**. Phase 1 does not need to expose five separate top-level commands on day one. A smaller initial CLI surface is acceptable as long as these behaviors are not collapsed back into one opaque bootstrap path internally.

### 6.1 Runtime Startup

Purpose:

- initialize services
- load metadata/cache snapshots
- verify readiness
- refuse startup when configured drift/policy violations are detected

Rules:

- read-only with respect to metadata
- no repair
- no backfill
- no seed/config mutations

### 6.2 Metadata Apply

Purpose:

- authoritative schema/materialized metadata initialization
- managed metadata reconciliation
- controlled metadata deployment/apply

Rules:

- explicit control-plane entrypoint
- resumable if needed
- produces drift-free persisted metadata

### 6.3 Seed/Config Apply

Purpose:

- apply product-owned configuration packs
- initialize non-authoritative but deployable product configuration

Examples:

- themes
- setup pages
- UI components
- record types
- actions
- flow seed packs
- standard list-field defaults
- baseline permission rows and system-permission catalog while policy packs remain incomplete

Rules:

- explicit control-plane entrypoint
- separate from metadata apply
- separate from business/sample data import

### 6.4 Repair/Migration

Purpose:

- historical compatibility fixes
- one-time or resumable backfills
- cleanup of legacy bootstrap mistakes

Rules:

- explicit operation
- separately invokable
- separately observable
- not part of normal startup
- **must be idempotent**: a repair step that runs to completion on a healthy DB must produce zero writes and exit fast (single SELECT, no UPDATEs)
- **must pair with a fatal verify check**: a one-time migration that needs to run on existing tenants is paired with a verify check that fails on the pre-migration state, so the existing `infra/entrypoint.sh` verify→apply fallback heals the tenant on next container restart with no manual operator action

Concrete instance landed in Phase 1 (PR #1065): managed-metadata Id rewrite. `_System_Object.Id` was generated by two competing schemes (`disc_<lowercase-table>` from discovery, `obj_<ApiName>_<sha256>` from pack apply) which silently produced orphan `_System_Field` rows whenever a logical object was registered by both paths. Resolved by:

1. Lowercasing `schema.GenerateObjectID` so all write paths converge on one canonical Id.
2. Switching discovery to call `GenerateObjectID` instead of building its own format.
3. `bootstrap.NormalizeObjectIds` (one-shot, in `RunRepair`) rewriting every `_System_Object` row whose `Id ≠ GenerateObjectID(ApiName)` to canonical, cascading through `_System_Field.ObjectId` and `_System_RecordType.ObjectId` under `FOREIGN_KEY_CHECKS=0`.
4. `bootstrap.ReconcileOrphanFieldObjectIds` (in `RunRepair`, after step 3) cleaning up legacy orphan field rows.
5. `controlplane.VerifyOrphanSystemFields` (fatal, in `Verify`) so any future regression that creates orphans causes verify to fail and the entrypoint to re-run apply.

This is the canonical pattern for repair work going forward: pair an idempotent reconcile in `RunRepair` with a fatal verify check, and don't touch `infra/entrypoint.sh`.

### 6.5 Derived Rebuild

Purpose:

- rebuild derived artifacts from authoritative metadata/policy

Examples:

- formula views
- cache snapshots
- future share/materialization projections derived from authoritative policy metadata
- other rebuildable derivatives

Rules:

- may be invoked after apply or repair
- conceptually separate from authoritative metadata mutation

---

## 7. Environment Strategy

The same control-plane model should behave differently by environment, but the **runtime server** should remain read-only in all cases.

### 7.1 Proposed Modes

| Environment | Runtime Server | Control Plane Trigger |
|---|---|---|
| local/dev | `verify` only | external dev launcher may auto-run `metadata apply` / `seed-config apply` before server start |
| staging | `verify-strict` | CI/CD or explicit operator action |
| production | `verify-strict` | explicit operator or deployment action |

### 7.2 Why not let the dev server mutate?

Developer ergonomics matter, but the correct place for convenience is:

- `make dev`
- `scripts/dev-up`
- compose/launcher tooling

not the request-serving server process itself.

This keeps the architectural boundary honest while preserving local velocity.

### 7.3 Immediate blocker: today's verify mode is not yet a safe gate

The current codebase does **not** yet have a strong enough verify path to justify the target model by itself.

Today:

- `VerifySchemaReadiness(...)` logs registry/critical-column problems and still returns `nil`
- `RunAssertions(...)` only runs inside the mutate path
- verify mode does not currently assert seed/config invariants such as connected apps, permission seeding, or managed sharing-policy correctness

So read-only startup should be treated as a **target state gated by verify hardening**, not as a property already guaranteed by the current implementation.

At minimum, verify hardening needs to cover failure modes such as:

- missing or unhealthy physical-table registry state
- missing system/admin bootstrap identities
- missing required connected apps used by login flows
- missing or half-seeded permission policy rows
- managed sharing-policy drift once runtime overlays are removed
- missing formula views or other derived artifacts that the runtime assumes exist

### 7.3.1 Phase 1a verify-gate acceptance spec

Phase 1a should begin by codifying the verify contract as a read-only acceptance specification before implementation work starts removing runtime overlays or promising read-only startup safety.

For `verify` / `verify-strict`, the gate should be considered **passing** only when all of the following are true:

1. schema registry health is `healthy`
2. required managed columns exist
3. read-only assertions run in the verify path rather than only in mutate mode
4. required bootstrap identities and login-critical connected apps exist
5. permission-policy seeding is complete enough that bootstrap profiles are not left half-seeded
6. managed access-policy values that runtime depends on are persisted correctly
7. required derived artifacts that the runtime assumes to exist are either present or explicitly downgraded to non-blocking warnings by policy

The gate should be considered **failing** when any configured blocking invariant is violated, including:

- missing physical tables or unhealthy registry state
- assertion failures with error severity
- missing system admin/profile bootstrap identities
- missing required connected apps
- missing or half-seeded bootstrap permission policy rows
- managed sharing/access-policy drift once runtime overlays are no longer relied upon

Phase 1a should therefore produce both:

- the spec itself
- the read-only assertion/verification implementation that enforces it

Overlay removal and "startup is read-only" claims should remain blocked on that acceptance spec being satisfied.

The remaining design choice is therefore **not** which checks are required. Section 7.3.1 defines that baseline. The remaining policy question is only whether any listed check is downgraded from blocking to warning for a specific environment mode, and that should be decided explicitly rather than by leaving the baseline spec open.

### 7.3.2 Closed current-state answer: existing assertions are read-only but post-mutation-assuming

This RFC no longer treats the assertion question as open.

The current startup assertions are **read-only SQL checks**, but they implicitly assume that the mutate path has already provisioned required state. The implementation problem is therefore:

- not hidden writes inside `RunAssertions(...)`
- but insufficient verify-path coverage and sequencing

That means assertions belong in the verify/readiness model, but they must be expanded and re-sequenced before verify can serve as a real safety gate.

### 7.4 Immediate blocker: current bootstrap recovery is idempotent, not atomic

The current bootstrap path is intentionally written to be restart-safe through idempotent SQL patterns such as:

- `INSERT IGNORE`
- `ON DUPLICATE KEY UPDATE`
- `WHERE col != desired`
- cleanup/rebuild helpers that can be retried

That is a strength, but it is **not** the same thing as atomic apply.

Today many multi-step bootstrap operations in `backend/internal/bootstrap/` are not wrapped in SQL transactions. Phase 1 should therefore treat partial-failure recovery as an explicit design concern:

- first-class job state and step/progress tracking
- resumable operations where appropriate
- clear distinction between "idempotent retryable" and "transactionally atomic"

The control plane should not promise Salesforce-style atomic deployment semantics until it actually has the primitives to provide them.

### 7.5 Immediate blocker: repeated cache refreshes expose hidden inter-step dependencies

The current startup path refreshes metadata cache more than once because later bootstrap steps read snapshots that earlier bootstrap steps mutate.

This is evidence of real sequencing boundaries, not just untidy code.

Phase 1 should therefore avoid assuming that all mutations can immediately collapse into "one big apply, one final cache load". The safer first step is:

- extract the operations honestly
- preserve required intermediate snapshot boundaries
- then reduce extra refreshes only after those dependencies are explicitly mapped and removed

---

## 8. Phase 1 Backend Control Plane

Before any meaningful UI exists, Phase 1 should establish a backend control-plane package and a thin non-HTTP runner.

### 8.1 Control-Plane Operations

The control plane should distinguish at least:

- `verify`
- `apply-metadata`
- `apply-seed-config`
- `run-repair`
- `rebuild-derived`

### 8.2 Phase 1 Delivery Surface: package first, CLI first

Phase 1 should begin with two surfaces:

1. an internal Go package, for example:

```text
backend/internal/controlplane/
  verify.go
  metadata_apply.go
  seed_config.go
  repair.go
  rebuild.go
```

2. one thin non-HTTP runner, for example a `cmd/setup` or similar CLI entrypoint

This is the smallest increment that:

- removes sequencing logic from request-serving startup
- gives tests a first-class provisioning surface
- avoids re-encoding control-plane behavior in ad hoc scripts
- does not prematurely commit to an operator HTTP API before the package/runtime boundaries are stable

HTTP endpoints may come later, but they should wrap the same internal control-plane package rather than define the first execution model.

### 8.3 Control-Plane Process Model

Phase 1 cannot assume every control-plane operation runs against raw SQL alone.

The current startup path has a real dependency ladder:

1. substrate/schema work populates `_System_*`
2. a service graph is created via `NewServiceManager(...)`
3. metadata cache is loaded via `RefreshMetadataCache(...)`
4. several seed/config steps then call through `svcMgr.*`

That means the control plane needs a **headless process model**, not just extracted helper functions.

#### Recommended dependency tiers

**Tier 0: Raw DB / repository operations**

These can run without constructing the application service graph:

- `InitializeSchema`
- `VerifySchemaReadiness`
- `DiscoverAndRegisterPhysicalTables`
- `BackfillFieldCapabilities`
- `BackfillLookupReferences`
- managed metadata reconcile helpers
- `InitializeRoles`
- `InitializeRecordTypes`
- `InitializeStandardListFields`
- `InitializeFolders`
- `InitializeAutoNumbers`
- `SeedSystemPermissions`
- `EnsureFolderOrganizationShares`

**Tier 1: Headless control-plane runtime**

These depend on existing application services and should run through a lightweight control-plane runtime that:

- builds `InfrastructureDeps`
- constructs `NewServiceManager(...)`
- refreshes metadata/permission caches
- does **not** start the HTTP server
- does **not** start background workers
- does **not** start session cleanup

Current examples:

- `EnsureViews`
- `InitializeSystemData`
- `InitializePermissions`
- `InitializeStandardActions`
- `InitializeThemes`
- `InitializeUIComponents`
- `InitializeSetupPages`
- `InitializeFlows`

This is where the existing `MetadataService`, `PermissionService`, `FormulaViewService`, and `SystemManager` should be reused rather than duplicated.

`cmd/setup_db` is only a partial precedent: it demonstrates substrate bootstrap, but it does not yet provide the headless service stack needed for full control-plane execution.

**Tier 2: Request-serving runtime only**

These are runtime lifecycle actions, not control-plane operations:

- background worker startup
- session cleanup startup
- HTTP server startup

The key rule is:

> control-plane execution may build a headless service graph, but it must not boot the request-serving runtime.

One more practical constraint follows from the current implementation:

- the headless control-plane runtime should be able to suppress runtime event/listener side effects when running bootstrap/control-plane work, because `NewServiceManager(...)` wires event subscribers during construction.

### 8.4 Internal Go API for CLI, tests, and jobs

Phase 1 should expose an internal Go API as the source of truth for control-plane operations.

The important property is not the exact package path. It is that:

- tests can call control-plane operations directly
- a thin CLI can wrap the same API
- any future admin HTTP endpoints can wrap the same API
- control-plane behavior does not get reimplemented three different ways

This matters because the current integration-test setup relies heavily on inline bootstrap helpers such as `testhelpers.BootstrapSchema(...)`. Once request-serving startup becomes read-only, tests need a first-class internal API to provision substrate, metadata, and seed/config state intentionally.

Phase 1 should therefore include:

- extracted internal Go APIs for control-plane operations
- at least one thin non-HTTP entrypoint (CLI or equivalent runner) that exercises those APIs
- explicit verify hardening work before any runtime-overlay removal depends on it

### 8.5 Optional later HTTP/API surface

An admin HTTP API is still compatible with this design, but it should be a **later** delivery surface, not the first one.

If or when it is added, it should wrap the same internal control-plane package and expose concepts such as:

- `verify`
- `apply-metadata`
- `apply-seed-config`
- `run-repair`
- `rebuild-derived`
- job history
- drift reports

### 8.6 Job Model

The control plane should include:

- job type
- state
- phase/progress
- finer-grained step or checkpoint data where recovery inside a phase matters
- start/end timestamps
- environment mode
- drift summary / result payload
- failure details

Because current bootstrap behavior relies heavily on idempotent retries rather than SQL transactions, the job model should explicitly support partial-failure recovery and resumability. It should not assume that "phase started" and "phase completed" are enough to reconstruct safe recovery points.

This RFC also no longer treats `_System_BootstrapState` reuse as an open architectural question.

The current `_System_BootstrapState` table is useful as proof that persisted run/phase/status tracking is valuable, but its present schema is too:

- startup-specific
- single-keyed
- coarse-grained
- phase-oriented rather than operation/checkpoint-oriented

to survive as-is as the long-term control-plane job model.

So the answer is:

- retain the idea of persisted run state
- do **not** retain the current schema unchanged
- redesign the table/model around explicit operations, richer status, and finer-grained recovery

---

## 9. Minimal UI Direction

The first UI should be a thin operator/admin surface, not a full Setup clone.

### 9.1 First Page

One initial page is sufficient:

**Bootstrap Status / Metadata Control**

It should show:

- last verify result
- last apply result
- drift summary
- running/completed jobs
- explicit action buttons or links

This is a UI MVP, not a system MVP. The backend contract comes first.

---

## 10. Extraction Map

This section classifies the current startup/bootstrap work in [`backend/cmd/server/main.go`](../../backend/cmd/server/main.go) into the proposed execution classes.

### 10.1 Pre-Service Blocking Bootstrap Sequence

| Current Step | Current Location | Proposed Class | Keep in Runtime Startup? | Notes |
|---|---|---:|---:|---|
| `InitializeSchema` | `main.go` | Metadata Apply | No | Core authoritative schema/materialized metadata init |
| `DiscoverAndRegisterPhysicalTables` | `main.go` | Repair/Migration | No | Discovery/compatibility job, not normal startup |
| `BackfillFieldCapabilities` | `main.go` | Repair/Migration | No | Historical metadata/data backfill |
| `BackfillLookupReferences` | `main.go` | Repair/Migration | No | Explicit compatibility/data repair |
| `VerifySchemaReadiness` | `main.go` | Runtime Startup | Yes | Read-only readiness verification |
| `ReconcileManagedStandardObjectAccessPolicies` | `main.go` | Metadata Apply | No | Managed metadata mutation |
| `ReconcileSystemObjectTableTypes` | `main.go` | Metadata Apply | No | Managed metadata mutation |
| `EnsureFolderSharingModels` | `main.go` | Metadata Apply | No | Managed metadata mutation |
| `ReconcileManagedRelationshipSemantics` | `main.go` | Metadata Apply | No | Managed metadata mutation |

### 10.2 Service Initialization and Cache Load

| Current Step | Current Location | Proposed Class | Keep in Runtime Startup? | Notes |
|---|---|---:|---:|---|
| `NewServiceManager` | `main.go` | Runtime Startup | Yes | Pure service composition |
| `RefreshMetadataCache` | `main.go` | Runtime Startup | Yes | Runtime snapshot load |
| `RefreshViewCache` | `main.go` | Runtime Startup | Yes, if read-only | Cache detection only; no mutation |

### 10.3 Background and Async Runtime Processes

| Current Step | Current Location | Proposed Class | Keep in Runtime Startup? | Notes |
|---|---|---:|---:|---|
| `go EnsureStandardIndexes(...)` | `main.go` | Repair/Migration | No | Heavy index creation already runs async today; should become an explicit control-plane job, potentially async, but not co-resident with request-serving startup |
| `StartBackgroundWorkers` | `main.go` | Runtime Startup | Yes | Starts Outbox/Scheduler/BulkProcessor runtime workers; not a control-plane operation |
| `SessionCleaner.Start` | `main.go` | Runtime Startup | Yes | Runtime maintenance loop; not metadata apply/repair |
| `ListenAndServe` goroutine | `main.go` | Runtime Startup | Yes | Request-serving process start, not control-plane work |

### 10.4 Post-Service Blocking Mutation Sequence

| Current Step | Current Location | Proposed Class | Keep in Runtime Startup? | Notes |
|---|---|---:|---:|---|
| `EnsureViews` | `main.go` | Derived Rebuild | No | Rebuilds formula views from authoritative state |
| `InitializeRoles` | `main.go` | Seed/Config Apply | No | Product/system identity seed |
| `InitializeSystemData` | `main.go` | Seed/Config Apply | No | Must later be split internally by concern |
| `InitializeRecordTypes` | `main.go` | Seed/Config Apply | No | Product configuration pack |
| `InitializeStandardActions` | `main.go` | Seed/Config Apply | No | Product configuration pack |
| `MigrateLayoutSystem` | `main.go` | Repair/Migration | No | Historical layout migration |
| `BackfillLayoutRelatedLists` | `main.go` | Repair/Migration | No | Compatibility backfill |
| `RefreshMetadataCache` after backfill | `main.go` | Derived Rebuild | No | Rebuild snapshot after mutations |
| `CleanupSystemListFields` | `main.go` | Repair/Migration | No | Cleanup of previous layout/list-field bug |
| `InitializeStandardListFields` | `main.go` | Seed/Config Apply | No | Product configuration pack |
| `RefreshMetadataCache` after list fields | `main.go` | Derived Rebuild | No | Rebuild snapshot after mutations |
| `InitializePermissions` | `main.go` | Seed/Config Apply | No | Current implementation creates/backfills baseline `ObjectPermissions` and `FieldPermissions` rows; it should only move toward derived rebuild after declarative policy packs exist |
| `SeedSystemPermissions` | `main.go` | Seed/Config Apply | No | Product/platform permission catalog seed |
| `EnsureFolderOrganizationShares` | `main.go` | Seed/Config Apply | No | Product config/share seed |
| `InitializeThemes` | `main.go` | Seed/Config Apply | No | Product configuration pack |
| `InitializeUIComponents` | `main.go` | Seed/Config Apply | No | Product configuration pack |
| `InitializeSetupPages` | `main.go` | Seed/Config Apply | No | Product configuration pack |
| `InitializeFlows` | `main.go` | Seed/Config Apply | No | Product configuration pack |
| `InitializeFolders` | `main.go` | Seed/Config Apply | No | Product configuration pack |
| `InitializeAutoNumbers` | `main.go` | Seed/Config Apply | No | Product configuration pack |
| `RunAssertions` | `main.go` | Runtime Startup | Yes, if read-only | Must move out of mutate-only gating if verify is expected to protect read-only startup |

### 10.5 Immediate First-PR Extraction Candidates

The first PR should extract the highest-value, least-ambiguous groups:

#### Group A: Move out of runtime startup first

- `InitializeSchema`
- all four managed metadata reconcile helpers
- `MigrateLayoutSystem`
- `BackfillLayoutRelatedLists`
- `InitializeSystemData`
- `InitializeRoles`
- `InitializeRecordTypes`
- `InitializeStandardActions`
- `InitializeStandardListFields`
- `InitializeThemes`
- `InitializeUIComponents`
- `InitializeSetupPages`
- `InitializeFlows`
- `InitializeFolders`
- `InitializeAutoNumbers`
- prepare managed-metadata runtime-overlay removal, but only delete the overlays once verify/apply paths can guarantee persisted metadata is authoritative
- separate provisioning semantics from upgrade/reconcile semantics for managed config objects before moving `InitializeThemes`-style logic into steady-state apply flows

#### Group B: Keep in startup for now

- service manager initialization
- metadata cache refresh
- `VerifySchemaReadiness`
- read-only assertions
- background workers
- session cleanup
- HTTP server startup

#### Group C: Explicitly reclassify for later follow-up

- `InitializePermissions`
- `SeedSystemPermissions`
- `EnsureViews`
- `EnsureStandardIndexes`
- `DiscoverAndRegisterPhysicalTables`
- `BackfillFieldCapabilities`
- `BackfillLookupReferences`
- `CleanupSystemListFields`
- `EnsureFolderOrganizationShares`

These have the highest chance of needing a second-pass design split between seed/config, repair, policy-pack apply, and derived rebuild.

#### Group D: Current semantic gaps that extraction must not hide

- runtime managed-metadata overlays (`applyManagedStandardObjectAccessPolicies`, `overlayManagedRelationshipSemantics`)
- incomplete PSG muting for CRUD/FLS effective-permission aggregation
- verify mode is currently warning-oriented rather than a strict startup gate
- several managed apply helpers currently overwrite admin customization on restart/upgrade
- legacy value-based sharing-model repair heuristics can bypass customization intent

These are not reasons to delay extraction. They are reasons to make the extracted control-plane/runtime boundary honest.

---

## 11. Proposed Phasing

### Phase 1: Classification and backend extraction — IMPLEMENTED

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

Delivered:

- `internal/controlplane/` package with Runner (headless, NoOpEventBus), operations, pack model
- `cmd/setup` CLI with 6 subcommands (apply, apply-metadata, apply-packs, ensure-indexes, rebuild, verify)
- `cmd/server` is fully read-only: verify → cache → serve
- Runtime overlays removed (not just planned); persisted metadata is the sole runtime authority
- Verify gate hardened: returns errors + managed metadata drift assertions
- system_data.json split by concern (profiles, bootstrap identity, dev connected apps)
- EventBus widened to ports.EventPublisher interface for headless mode
- Inter-pack cache refresh boundaries made explicit in ApplyPacks ordering
- **PR #1065** (2026-04-17): retired the legacy `disc_<table>` Id scheme; `schema.GenerateObjectID(apiName)` is now lowercase-normalized and used by every write path (discovery, pack apply, admin UI). One-shot `NormalizeObjectIds` in `RunRepair` rewrites legacy rows; fatal `VerifyOrphanSystemFields` in `Verify` ensures the entrypoint heals any future regression.

### Phase 2: ~~Separate seed/config and repair paths~~ — IMPLEMENTED (merged into Phase 1)

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

- ✅ `apply-seed-config` → `controlplane.ApplyPacks()` with 4 typed packs
- ✅ `run-repair` → `controlplane.RunRepair()` (discovery + backfills, separately invokable)
- ✅ initial job model → `controlplane.JobResult` with step-level tracking
- ✅ `system_data.json` split → profiles.json, bootstrap_identity.json, dev_connected_apps.json (old file deleted)
- ⚠️ PSG muting, upgrade-protection model, legacy repair retirement → deferred to Phase 3+

### Phase 3: ~~Derived rebuild path~~ — IMPLEMENTED (merged into Phase 1)

- ✅ `rebuild-derived` → `controlplane.RebuildDerived()` (formula views + cache refresh)
- ✅ formula view rebuild owned by RebuildDerived operation
- ⚠️ permission/share derived-state reclassification → deferred

### Phase 4: Thin operator/admin UI — NOT STARTED

Deliverables (future):

- Bootstrap Status / Metadata Control page
- verify/apply status visibility
- job history and drift visibility
- HTTP API endpoints for control-plane operations

---

## 12. Open Questions

Questions resolved by implementation:

4. ~~Should the first headless control-plane runner be a dedicated CLI, an existing setup command extension, or both?~~ **Resolved:** Dedicated CLI (`cmd/setup`) with `internal/controlplane` package. Runner uses NoOpEventBus for headless mode.
5. ~~At what point can runtime managed-metadata overlays be deleted?~~ **Resolved:** Overlays deleted. Reconcilers persist correct values; drift assertions detect divergence. Runtime reads persisted metadata as-is.
7. ~~Which checks from Section 7.3.1 are downgraded from blocking to warning?~~ **Resolved:** All checks run in strict mode via `controlplane.Verify()`. No environment-based downgrading.

Questions still open (Phase 2+):

1. Which current seed/config packs should eventually become authoritative metadata instead of bootstrap-applied data rows?
2. How should today's baseline permission-row seeding evolve once policy metadata is formalized as a managed pack?
3. What is the smallest acceptable operator UI after the backend contract lands? (Phase 4)
6. What is the intended end-state for CRUD/FLS muting inside Permission Set Groups: full Salesforce-style muting, or a narrower documented capability?
8. Which managed seed/config operations are one-time provisioning only, and which are intended to participate in later upgrades?
9. How should legacy sharing-model repair be retired without losing the ability to fix already-drifted installations once?

---

## 13. Recommendation

Adopt this RFC as the design direction for `#822` and related issues.

The most important immediate move is not merely “split startup vs apply.”

It is:

> **separate authoritative metadata apply from seed/config apply, repair/migration, and derived rebuild.**

Without that distinction, the control plane will remain an opaque bootstrap bucket under a new name.
