Files
stack/docs/native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md
2026-07-15 07:04:38 -05:00

67 KiB
Raw Blame History

KBN-101 — Database Runtime/Migration Role Split

Status: frozen implementation contract; rc.8 exact residual remediation complete, awaiting independent exact-head re-review for #771

Version: 1.0.0-rc.8

Dependency: KBN-010 → KBN-101 foundation → KBN-100 → KBN-101 deployed-role certification → KBN-105 Scope: PostgreSQL standalone/federated runtime identity, the sole application DDL runner, TLS bootstrap, readiness, deployment handoff, and evidence. This documentation card changes no database, secret, deployment, CI, runtime, migration, or compose artifact.

1. Decision, modes, and non-negotiable boundaries

Current main has one DATABASE_URL path in packages/db/src/client.ts, migrate.ts, drizzle.config.ts, storage adapters/CLI, Gateway startup, fleet-backlog, CI, installer output, compose, and Portainer. It also has PostgreSQL DDL outside a controlled migration phase: direct Drizzle scripts, CREATE EXTENSION probes, a direct-DDL federated integration test, and platform init SQL. None of those current paths is certified by this contract; every implementation card below must close its listed path.

Item Exact name / shape Rule
Runtime URL DATABASE_URL PostgreSQL non-owner runtime connection. Required only by runtime services in standalone/federated; those services never receive DATABASE_MIGRATION_URL.
Migration URL DATABASE_MIGRATION_URL Required only by mosaic-db-migrator, the dedicated one-shot runner/Job. It is forbidden in Gateway, storage runtime, fleet, and ordinary CLI environments.
Runtime DTO DatabaseRuntimeConnectionConfigDto tier, databaseUrl, runtimeMode; maps only DATABASE_URL. It is not constructible from migration configuration.
Migration DTO DatabaseMigrationConnectionConfigDto tier, migrationDatabaseUrl, migrationMode; maps only DATABASE_MIGRATION_URL, is accepted only by mosaic-db-migrator, and has no runtime-bootstrap import path.
TLS DTO DatabaseTlsConfigDto caCertificatePath, rejectUnauthorized: true, serverName; validates the mounted CA without serializing its bytes. serverName is derived only from the validated connection host.
Readiness DTO DatabaseSchemaReadinessDto state, expectedSchemaVersion, observedSchemaVersion, migrationRequired, roleCheck, checkedAt; no URL, user, host, database name, or secret.
Modes local, standalone, federated local is the explicit PGlite-only exception. standalone and federated are production-like PostgreSQL modes.

No fallback or plaintext: in production-like modes, missing DATABASE_URL, DATABASE_MIGRATION_URL, or DATABASE_TLS_CA_CERT_PATH is a phase-specific error. No command may substitute a runtime URL, config-file URL, default URL, inferred URL, or hard-coded URL. PostgreSQL URLs must use sslmode=verify-full with rejectUnauthorized: true; disable, allow, prefer, require, no-verify, an absent CA, a wrong CA, an unverified certificate, or host/SAN mismatch fails before readiness. PGlite uses only its configured local data directory and explicit PGlite routine; it neither reads nor interprets PostgreSQL URL/TLS variables.

ASSUMPTION K101-A1: standalone and federated are the complete current PostgreSQL production-like modes. A future PostgreSQL tier inherits this contract until a versioned amendment names its DNS, secrets, bootstrap, and evidence.

2. The sole PostgreSQL DDL control plane

mosaic-db-migrator is the only application/CI/test command that may connect with DDL authority to PostgreSQL. It owns, in one max: 1 PostgreSQL session: migration-DTO parse, TLS/identity/search-path preflight, advisory-lock acquisition, manifest/ledger reconciliation, migration execution, postflight/readiness verification, lock release, and session close. It rejects DATABASE_URL-only invocation before opening a connection or emitting DDL. The only exception is the external privileged platform/IaC bootstrap actor (§4/§7), which is not an application command, never receives either application URL, and executes only the fixed bootstrap artifact.

Executable contract (KBN-101-03 exclusive): packages/db/package.json publishes bin.mosaic-db-migrator; its executable target is packages/db/src/cli.ts. KBN-101-03 also exclusively owns the immutable runner image recipe docker/db-migrator.Dockerfile. The CLI imports only private implementation modules packages/db/src/migrator/runner.ts, packages/db/src/migrator/config.dto.ts, packages/db/src/migrator/manifest.ts, packages/db/src/migrator/identity.ts, and packages/db/src/migrator/tls.ts, plus their tests. Package root exports do not expose runMigrations; no other programmatic migration API is public. The sole typed programmatic runner is private to the package/CLI boundary and accepts the typed migration DTO, never a URL/string/SQL/schema/role argument.

The exact user-facing interface is mosaic-db-migrator --run and mosaic-db-migrator --verify. --help is required and is the only discovery mode. Both commands use the fixed packaged migration folder and accept only DATABASE_MIGRATION_URL, DATABASE_TLS_CA_CERT_PATH, and non-secret MOSAIC_DATABASE_TIER/correlation input from the environment. They reject DATABASE_URL as fallback and reject URL, SQL, schema, role, database, migration-folder, and identifier arguments in argv. Stable sanitized process exits are: 0 success; 64 configuration; 65 TLS; 66 unsafe identity; 67 ledger/schema; 68 lock contention; 69 migration failure. CI, Compose, Swarm, and each two-gateway migration Job invoke the immutable db-migrator image with exactly mosaic-db-migrator --run; their readiness verification invokes exactly mosaic-db-migrator --verify. Required tests cover --help, argv rejection, runtime-only refusal before connect, migration-only success, each sanitized error/lock exit, and ordered postgres-a → mosaic-db-migrator-a → gateway-a plus postgres-b → mosaic-db-migrator-b → gateway-b execution.

The implementation must inventory and close every present and future entrypoint as follows:

Current entrypoint Required rc.8 disposition
packages/db/src/migrate.ts:runMigrations() Remove its optional URL, DATABASE_URL, and default fallback API from public/runtime exports. Its PostgreSQL behavior moves behind mosaic-db-migrator; callers cannot invoke it with an arbitrary URL.
packages/db/src/index.ts KBN-101-03 removes the public runMigrations re-export; only the explicit PGlite-local API remains as separately typed local behavior. A compile/import-negative proves @mosaicstack/db cannot directly import runMigrations; a DATABASE_URL-only direct-library attempt fails before connect.
packages/db/drizzle.config.ts and direct drizzle-kit migrate A database-connecting Drizzle configuration reads only DATABASE_MIGRATION_URL through the migration DTO and rejects its absence before connection. Replace direct drizzle-kit migrate exposure with mosaic-db-migrator. db:generate is an offline schema artifact command and must neither resolve nor connect to a URL.
pnpm --filter @mosaicstack/db db:migrate and CI invocation Make it a thin mosaic-db-migrator wrapper; no direct Drizzle migrator invocation remains. CI supplies an isolated disposable migration URL only to that job.
db:push / direct drizzle-kit push Forbidden for standalone, federated, CI production-like, Portainer, and any URL outside a disposable developer database. If retained for local experimentation, a wrapper requires MOSAIC_DISPOSABLE_DEVELOPER_DB=1, a locally allowlisted disposable target, DATABASE_MIGRATION_URL, verified TLS when PostgreSQL is used, and rejects DATABASE_URL, any production-like tier, and every non-allowlisted host/database before connection. It is never a release, repair, or migration procedure.
mosaic storage migrate --run Delegates only to mosaic-db-migrator; it rejects DATABASE_URL-only execution before spawning or connecting. Its PGlite form remains explicitly local-only.
PostgresAdapter.migrate(), Gateway DatabaseModule/startup, and PostgreSQL adapter factories Runtime PostgreSQL migration is removed: no runMigrations, DDL, CREATE EXTENSION, or migration-compatible handle is reachable from startup. Gateway performs read-only identity, TLS, search_path, and manifest-ledger readiness checks only.
packages/mosaic/src/commands/fleet-backlog.ts PostgreSQL fleet backlog never migrates or creates tables. It consumes a ready runtime connection; PGlite may use only its explicit local migration routine.
packages/storage/src/{adapters/postgres,tier-detection}.ts extension work and infra/pg-init/01-extensions.sql Runtime probes become read-only catalog/extension-presence checks. Extension provisioning is a fixed external bootstrap prerequisite or a reviewed runner migration where the migrator has the required scoped authority; it is never a probe side effect or a reason to grant runtime database CREATE.
packages/db/src/federation.integration.test.ts direct type/table/index DDL Replace with a pre-migrated disposable database created by mosaic-db-migrator, or make the test invoke that runner. The test itself has runtime credentials and no direct DDL.
apps/gateway/src/__tests__/integration/federated-pgvector.integration.test.ts CREATE TEMP TABLE KBN-101-06 replaces the temporary runtime DDL with a runner-prepared disposable persistent mosaic.federated_pgvector_fixture database/table. The test receives only its runtime URL/CA and performs read/query-only qualified-vector assertions (including the selected vector operator); it has no setup hook, temporary privilege, or DDL. Runtime TEMPORARY remains denied.
docker/init-db.sql KBN-101-00 deletes this duplicate tracked init artifact. infra/pg-init/01-extensions.sql is also retired from image-init ownership; neither may remain as a hidden extension authority. The sole extension action is the fixed external bootstrap artifact described in §5/§7, or the runner only when its reviewed implementation card explicitly grants that authority.
packages/storage/src/migrate-tier.ts operator remediation KBN-101-07 replaces the raw SQL recommendation with the sanitized instruction mosaic-db-migrator --run (secret injection and target selection are deployment-owned; no URL, SQL, or credential is accepted on the command line), or directs the operator to the approved external-bootstrap runbook when extension eligibility has not been met. It never recommends CREATE EXTENSION.
tools/federation-harness/docker-compose.two-gateways.yml Active topology; KBN-101-05 migrates it rather than retires it. It must contain postgres-a, postgres-b, mosaic-db-migrator-a, mosaic-db-migrator-b, gateway-a, and gateway-b; each database has separate runtime/migrator URL and CA consumers, TLS server material, verified-TLS readiness, and runner-before-Gateway ordering. Its existing plaintext URLs/init mount are forbidden.
docs/fleet/backlog-conventions.md KBN-101-07 removes the current automatic first-use PostgreSQL runMigrations() claim. It points PostgreSQL operators to the sole runner/readiness sequence and labels only the PGlite routine as local-only. A documentation-route DATABASE_URL-only test asserts no first-use/migration instruction is executable before connect.
docs/PERFORMANCE.md KBN-101-07 removes direct drizzle-kit migrate and Gateway-startup runMigrations() instructions. It points to the sole runner/readiness sequence. A documentation-route DATABASE_URL-only test asserts neither direct command nor Gateway fallback remains.
README.md, CLAUDE.md, docs/guides/{dev-guide,deployment}.md, and docs/federation/SETUP.md operator instructions KBN-101-07 replaces direct db:migrate, db:push, and CREATE EXTENSION production-like instructions with the runner/bootstrap procedure, and labels any remaining disposable-local command as non-production with the §2 guard. Documentation is a DDL entrypoint and cannot advertise a bypass.
New package scripts, test helpers, setup hooks, CLI commands, installers, CI steps, adapters, or operator docs A repository check rejects any new PostgreSQL DDL-capable entrypoint or instruction unless it is the dedicated runner or the named external bootstrap artifact. No future script may accept a URL parameter or DATABASE_URL as a DDL escape hatch.

The runner must run the original migration bytes, including shipped 0009; no migration command may repair a ledger by manual insertion, adoption, db:push, or schema diff. Tests requiring PostgreSQL schema consume a pre-migrated disposable database or invoke this exact runner.

KBN-101-06 owns a finite scanner and DATABASE_URL-only-before-connect matrix. It inventories by exact path every executable current source/script/package bin, operator document, and deploy manifest—including every §2 row, packages/db/src/index.ts, docs/fleet/backlog-conventions.md, docs/PERFORMANCE.md, infra/pg-init/01-extensions.sql, .woodpecker/ci.yml, packages/db/{src/migrate.ts,drizzle.config.ts,package.json}, packages/db/src/migrate.test.ts, packages/storage/src/cli.ts, CLAUDE.md, and docs/federation/SETUP.md. The matrix assigns each row an owner, disposition, and test; packages/db/src/index.ts is KBN-101-03/import-compile-negative, the two current documents are KBN-101-07/operator-route negatives, and both gateway pairs are KBN-101-05/06 ordered-Job negatives. It rejects every unclassified hit in scanner classes executable current source/scripts/package bins, operator docs, and deploy manifests.

The only reviewed allowlists are exact path/category pairs: byte-immutable historical SQL under packages/db/drizzle/** (reviewed solely as runner input); PGlite-only routines; negative-test literals; vendored/generated artifacts; and clearly labeled historical review reports. Each allowlist entry records path, category, rationale, reviewer, and expiry/review revision; it cannot mask a current executable or current instruction. An unknown token hit fails. The scanner is a classifier plus path inventory and review—not a naive token scan alone—and therefore never by itself proves DDL authority. For every non-allowlisted inventory row and gateway-a/gateway-b, the matrix proves DATABASE_URL-only, missing migration URL, direct library import, direct Drizzle, db:push, init artifact, operator route, runtime-only fixture/test, and each harness migration Job/Gateway fail before connection/DDL as applicable. The db:push negatives also cover production-like tier and production-like URL rejection.

3. Exact migration manifest, ledger, and lock

3.1 Manifest v1

The runner generates and verifies a source-controlled migration manifest v1 from the shipped Drizzle journal and SQL files. A record has exactly:

logicalIndex: non-negative integer from journal array position
journalTag: exact journal `tag` string
migrationSha256: lowercase SHA-256 of the exact migration `.sql` file bytes

The canonical SQL bytes are the raw Git blob bytes at the signed source-release commit, not workstation checkout bytes. KBN-101-03 adds/validates an LF-pinning .gitattributes rule for packages/db/drizzle/**/*.sql, generates the source-controlled manifest from those canonical blobs in CI, and makes the runner verify deployed file bytes against the manifest before DDL. No newline, Unicode, whitespace, SQL, or line-ending normalization is applied. Manifest canonical serialization is UTF-8 bytes of:

mosaic-drizzle-manifest-v1\n
<logicalIndex>\t<JSON.stringify(journalTag)>\t<migrationSha256>\n
... in ascending logicalIndex with no omitted index

manifestSha256 is SHA-256 of those canonical bytes. The manifest has no timestamp-derived ordering; folderMillis, legacy ledger id, and created_at are diagnostic only. KBN-101-03 corrects journal logical order metadata (including the current 0008/0009 anomaly) without changing shipped 0009 bytes.

The runner stores certification in drizzle.__mosaic_migration_manifest with exactly one active v1 row (manifest_version=1, manifest_sha256, certified_at). It is owned by mosaic_schema_owner; only the migrator after SET ROLE mosaic_schema_owner may insert/update it; runtime gets SELECT only, and PUBLIC gets no schema/table privilege. Existing databases receive this table through the runner after backup/preflight; it is not created by Gateway, a test, or manual SQL.

3.2 Reconciliation and 0009 transition

The expected ledger is the ordered list of v1 manifest tuples. The runner reads every observed drizzle.__drizzle_migrations.hash, maps each observed hash to exactly one manifest tuple, and rejects a missing, unknown, duplicate, ambiguous, corrupt, or stale-replica mapping. Equality is tuple-complete: every expected tuple occurs once, no additional tuple occurs, and the manifest digest matches. A count comparison or hash-set comparison is forbidden. Physical legacy insertion id, insertion timestamp, and order are expressly non-normative.

For an existing database:

  1. take the KBN-101-07 approved backup and capture a read-only inventory before changing anything;
  2. if 0009 is missing and its effects are absent, run the original shipped 0009 through mosaic-db-migrator and record it normally;
  3. if the 0009 hash is present but physically late, accept it when its one-to-one tuple mapping is exact;
  4. if an expected hash is missing while its effects are partial or complete, or catalog/ledger evidence conflicts, fail closed as DATABASE_MIGRATION_RECONCILIATION_AMBIGUOUS. Recovery is backup restoration or an explicit separately reviewed repair artifact with its own owner, tests, backup, rollback, and approval—not manual ledger insertion/adoption; and
  5. write/update the v1 certification row only after exact reconciliation and final catalog/readiness verification.

Required runner tests cover clean, pre-0009, skipped-0009/effects-absent, applied-late, duplicate, unknown, missing, corrupt tuple-pair, partial/full-effect ambiguity, stale replica, backup/restore, and rerun idempotence. The tests prove the 0009 SQL bytes are unchanged and logical journal ordering—not physical ledger order—controls reconciliation.

3.3 Fixed advisory-lock namespace

Before any preflight that can decide migration state, mosaic-db-migrator acquires pg_try_advisory_lock(1297044289, 1262636593). The constants are signed-int4-safe fixed namespace values: class 1297044289 (MOSA) and object 1262636593 (KBN1). The same max: 1 session retains it for preflight, reconcile, migrate, verify, release, and close. Failure returns DATABASE_MIGRATION_LOCKED immediately; a process crash releases it when the PostgreSQL connection closes. Runtime readiness remains unready while a holder is active and never waits by running migrations. Tests prove concurrent contention, crash/connection-loss release, readiness while held, and non-interference from an unrelated advisory key.

4. PostgreSQL roles, identifiers, and trusted sessions

Role creation, passwords, membership, database ownership, certificates, and Vault values are platform/IaC/operator work—not Drizzle/application migrations. Application SQL must not issue credential/role management statements or embed credentials.

Role Attributes and ownership Membership / session use
mosaic_platform_database_owner NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS; platform-only database owner after bootstrap. Never granted to application roles.
external platform bootstrap actor Provider/operator/IaC-controlled privileged identity outside the Mosaic role graph and Vault/application configuration. Creates/transitions the database and roles, then retires from application use.
mosaic_schema_owner NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS; owns only mosaic/drizzle schemas and application/ledger objects. It owns the mosaic_extensions schema but not its extension/member objects. Never an application login; no database/extension authority. A bootstrap-only schema CREATE grant is revoked after extension creation.
mosaic_extension_owner Dedicated NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS extension owner, distinct from platform/schema/migrator/runtime. It owns vector and all extension-member objects in mosaic_extensions: the explicit exception to the schema-owner object rule. No application login has membership, SET ROLE, credential, or inheritable grant to it. Only the external bootstrap actor may SET ROLE mosaic_extension_owner, only during bootstrap.
mosaic_migrator LOGIN NOINHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS. Only mosaic_schema_owner; runner verifies session_user=mosaic_migrator, SET ROLE mosaic_schema_owner, then current_user=mosaic_schema_owner.
mosaic_runtime_capability NOLOGIN and no ownership/administrative attributes. Holds only named runtime grants.
mosaic_runtime LOGIN INHERIT with no ownership/administrative attributes. Only mosaic_runtime_capability WITH INHERIT TRUE, SET FALSE, ADMIN FALSE; never owner/migrator member.

The fixed application/runtime schema is mosaic. Every application/runtime pooled connection executes and verifies exactly SET search_path TO pg_catalog, mosaic before its first application query; connection checkout repeats this after reset/reconnect. Transactional application operations use SET LOCAL search_path TO pg_catalog, mosaic and verify it before query execution. public and $user are forbidden in all runtime paths.

The sole runner has one narrowly bounded legacy-history bootstrap subphase for the immutable current journal: before it runs, external bootstrap revokes CREATE on public from PUBLIC, temporarily assigns its ownership to mosaic_schema_owner, and denies all runtime connections. In its locked max:1 migration session only, after SET ROLE mosaic_schema_owner, it uses the fixed legacy-only SET LOCAL search_path TO pg_catalog, public, mosaic_extensions solely to execute byte-immutable historical migrations 0000 through the current head. mosaic_extensions is non-writable in that session and is included only so immutable 0001 resolves its unqualified existing vector type; it is never in a runtime session path. After relocation/catalog verification and before manifest certification or any runtime readiness, the runner transfers public ownership to mosaic_platform_database_owner, revokes application USAGE/CREATE, and restores pg_catalog,mosaic. This compatibility subphase is not a runtime/operator option, accepts no configuration identifier, has no fallback, and is removed/disabled before KBN-101-08.

KBN-101-03 exclusively owns packages/db/src/schema.ts, packages/db/drizzle/meta/*, packages/db/drizzle/meta/_journal.json, the generated relocation migration, and exact database/Drizzle tests. It freezes one exported export const mosaic = pgSchema('mosaic'); every application pgTable and every application pgEnum must be declared through that export. The generated snapshot/journal and future db:generate output must target mosaic only; a static declaration/SQL test rejects a default-schema application pgTable/pgEnum, public application declaration, or future generated application DDL outside mosaic. Historical 0000 through current SQL and the shipped journal provenance are byte-immutable and execute only in the trusted legacy-public subphase; they are never rewritten to claim they originally targeted mosaic.

4.1 pgvector ownership and supported transition

The selected extension policy is fixed. Fresh databases: the external bootstrap actor temporarily SET ROLE mosaic_extension_owner, creates vector as CREATE EXTENSION vector WITH SCHEMA mosaic_extensions, validates pg_extension.extowner, extension-member ownership/schema/version, then revokes the temporary bootstrap-only schema grant. mosaic_extensions remains non-writable by runtime and grants runtime only catalog-proven USAGE/read-only function access. Runtime, migrator, and schema owner have no membership in mosaic_extension_owner and must be denied ALTER EXTENSION, DROP EXTENSION, and extension-member UPDATE/DDL.

PostgreSQL has no supported ALTER EXTENSION ... OWNER TO. No card may invent it, mutate system catalogs, use DROP ... CASCADE, or adopt legacy extension ownership. An existing vector is eligible for the clean in-place path only when pg_extension.extowner already resolves to the approved mosaic_extension_owner; after exact supported-version, extrelocatable=true, dependency, and complete expected member-set checks, the bootstrap actor may perform the tested ALTER EXTENSION vector SET SCHEMA mosaic_extensions. The same postflight verifies extowner, each member owner/schema, version, dependency inventory, and denial for runtime/migrator/schema owner.

A vector extension owned by a legacy runtime/single login is not activated in place. It fails closed before activation and requires this controlled shadow-database migration: (1) approved backup and read-only inventory; (2) create new database/roles; (3) privileged actor creates vector under mosaic_extension_owner; (4) sole runner applies migrations and relocation; (5) copy data with vector dimensions, row counts, checksums, FK, and sequence evidence; (6) quiesce/drain writers; (7) apply and verify final delta; (8) prove role/TLS/readiness; (9) atomically switch connections; and (10) retain the old database read-only for the approved rollback window. Partial/cancel/resume and rollback preserve the old read-only source until the switch; no target unable to shadow-migrate is eligible until separately reviewed. Required tests cover fresh, approved-owner clean existing relocation, legacy-owner shadow path, partial/resume/rollback, N-1, exact extowner/member/schema/version assertions, and ALTER/DROP/member-update denials for runtime, migrator, and schema owner.

schema.ts must emit mosaic_extensions.vector(...), and all vector casts/operators/functions in runner, application queries, fixtures, and generated SQL must explicitly qualify mosaic_extensions (for example OPERATOR(mosaic_extensions.<->)); mosaic_extensions is deliberately not added to runtime search_path.

Before relocation, the runner records a parameterized catalog inventory and dependency graph. It must classify, in dependency order, application enum/domain/base types; owned and identity sequences; application tables; table columns/defaults; functions/procedures; views/materialized views; extension-owned objects; then triggers, constraints/FKs, indexes, and dependent rules/policies. It moves base types, sequences, tables, and functions as required; OID-bound constraints/indexes follow their owning objects and are catalog-verified rather than recreated blindly. Every object must be expected exactly once and be in either the immutable legacy/bootstrap allowlist, the mosaic application allowlist, or the selected mosaic_extensions extension membership; unknown, extra, duplicate, cross-schema, dependency-cycle, or partial-resume state fails closed. No raw client-side identifier interpolation is allowed.

KBN-101-03 tests clean bootstrap, approved-owner existing relocation, legacy-owner shadow migration, interrupted/partial relocation resume, partial/cancel shadow resume, and pre-activation reverse rollback-before-activation; reverse rollback is allowed only before KBN-101-08 and restores the approved backup or the reviewed inverse relocation, never a runtime search_path bypass. Its N-1 order is: current legacy release → inactive bootstrap/runner and mosaic declarations → catalog relocation/generated-artifact verification or approved shadow migration → verified non-owner runtime activation. Tests include byte-immutable historical execution, no public application objects/declarations/future SQL, vector type/cast/operator query success under fixed pg_catalog,mosaic, exact extension-owner/member/schema/version assertions, ALTER/DROP denial for runtime/migrator/schema owner, and all extension eligibility negatives.

No SQL identifier may come from URL/config/environment/operator input. Catalog comparisons use parameter values. The fixed identifiers above are constants; the external bootstrap artifact alone may use server-side format('%I', fixed_allowlisted_identifier) after allowlist validation. Raw client-side interpolation for identifiers, SET search_path, database, schema, role, table, or extension names is forbidden. Tests include injection-shaped values, a poisoned pooled-session reset, and transaction SET LOCAL restoration negatives.

External bootstrap executes REVOKE CONNECT, TEMPORARY ON DATABASE <fixed_database> FROM PUBLIC, then grants CONNECT only to mosaic_runtime, mosaic_migrator, and the time-bounded external bootstrap actor while it is required. Certification fails if an unrelated login retains CONNECT or either application login retains TEMPORARY. Runtime receives USAGE on mosaic, named table/sequence grants through mosaic_runtime_capability, and USAGE on drizzle plus SELECT only on drizzle.__drizzle_migrations and drizzle.__mosaic_migration_manifest. INSERT, UPDATE, DELETE, TRUNCATE, and DDL rights on ledger/manifest are revoked. Revoke public CREATE and function EXECUTE; SECURITY DEFINER is forbidden unless a separately reviewed exception pins trusted path and grants only the capability role. Database TEMPORARY, role management, extension, schema, and object ownership are denied.

Immutable KBN relations, after KBN-100 creates them, grant runtime only SELECT, INSERT and explicitly deny UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER: task_events, artifacts, task_checkpoints, task_checkpoint_artifacts, and approval_decision_artifacts. KBN-100 retains RESTRICT/no-cascade semantics. Foundation certification verifies the role/schema boundary only; post-KBN-100 certification verifies this real deployed-role matrix.

5. Deployable verified-TLS bootstrap

mosaicstack/stack is the named repository/control plane. Ownership is intentionally non-overlapping: KBN-101-00 exclusively owns the versioned external bootstrap interface infra/pg-bootstrap/roles.sql, infra/pg-bootstrap/extensions.sql, and infra/pg-bootstrap/README.md, plus its bootstrap tests—roles, extension-owner transition, and no renderer/deployment manifests. KBN-101-05 exclusively owns tools/db/render-postgres-secrets.ts, its tests, the current docker-compose.yml, docker-compose.federated.yml, deploy/portainer/federated-test.stack.yml, tools/federation-harness/docker-compose.two-gateways.yml, and apps/gateway/Dockerfile; it consumes the versioned KBN-101-00 bootstrap interface and owns no bootstrap SQL. The named Mosaic deployment control plane / Jason is activation authority; the environment-specific IaC/Vault owner supplies only approved input secret versions and may not substitute an unreviewed current-repository artifact. The KBN-101-05 renderer is the only deployment handoff: it reads secret-provider references, validates owners/modes/digests/SANs, writes each output atomically (mkstemp on the target tmpfs, fsync, chmod/chown, atomic rename), and records only secret-version identifiers and hashes.

KBN-101-05 changes the Gateway image to fixed non-root USER 10001:10001. Gateway CA and Gateway leaf-certificate mounts, and its own Gateway private key only when it terminates its HTTPS listener, must be readable by 10001:10001; PostgreSQL private keys and migration-only material are never mounted there, and no secret is world-readable. PostgreSQL is not assigned a guessed UID/GID: its image must first be pinned by digest, and an image-inspection plus rendered Compose/Swarm test freezes the image's effective PostgreSQL UID:GID before the renderer selects mount owner/group. A digest, service UID/GID, rendered secret uid/gid/mode, or container USER mismatch is a KBN-101-05 failure. Mosaic applications never generate, self-sign, copy, or persist production certificates; the external bootstrap actor receives them only through the deployment secret mechanism and no plaintext development exception exists for production-like modes.

Material Vault target / deployment secret Mount, injection, and authorized consumer
Runtime URL secret-{env}/mosaic-stack/database/runtime (url) → mosaic-db-runtime-url-v1 Gateway only: /run/secrets/mosaic-db-runtime-url, 0600, 10001:10001; entrypoint maps it to DATABASE_URL only at process exec. It is denied to every migrator, storage runtime, fleet, ordinary CLI, and test except an explicit runtime-negative fixture.
Migration URL secret-{env}/mosaic-stack/database/migrator (url) → mosaic-db-migrator-url-v1 Each one-shot migrator only: /run/secrets/mosaic-db-migrator-url, 0600, fixed migrator UID:GID asserted by the image/render test; entrypoint maps it only to DATABASE_MIGRATION_URL. It is denied to Gateway, storage runtime, fleet, and ordinary CLI.
CA bundle secret-{env}/mosaic-stack/database/tls-ca (certificate) → mosaic-db-ca-v1 Gateway/migrator: /run/secrets/mosaic-db-ca.crt, 0444, owned by the consuming UID:GID; CA is public trust material. PostgreSQL receives a distinct read-only CA copy only when client-cert validation is enabled.
Gateway leaf certificate secret-{env}/mosaic-stack/federation/gateway-server-tls (certificate) → mosaic-gateway-server-cert-v1 Gateway only: /run/secrets/mosaic-gateway-server.crt, 0444, 10001:10001; renderer emits this exact Compose and Swarm target and validates it before start.
Gateway private key same Vault record (private_key) → mosaic-gateway-server-key-v1 Gateway only: /run/secrets/mosaic-gateway-server.key, 0400, 10001:10001; not mounted to migrator or PostgreSQL and never world-readable.
PostgreSQL leaf certificate secret-{env}/mosaic-stack/database/postgres-server-tls (certificate) → mosaic-postgres-server-cert-v1 PostgreSQL only: /run/secrets/mosaic-postgres-server.crt, 0444, frozen verified postgres UID:GID.
PostgreSQL private key same Vault record (private_key) → mosaic-postgres-server-key-v1 PostgreSQL only: /run/secrets/mosaic-postgres-server.key, 0400, frozen verified postgres UID:GID; never mounted to Gateway or migrator.

Compose uses identically named local secret references rendered by the named renderer into non-repository tmpfs paths; Swarm declares the same secrets and target paths with the tested uid, gid, and mode. KBN-101-05 rejects bind-mounted committed cert/key files, environment-encoded PEM, missing secrets, non-atomic renderer output, private-key access outside its named consumer (Gateway for Gateway key; PostgreSQL for PostgreSQL key), and any world-readable URL/key. The existing target Vault names are planned canonical paths and must be verified/provisioned by the deployment-owner input; the planning card does not claim they exist.

The server leaf SANs are frozen to actual connection DNS names, not a configurable alias:

topology PostgreSQL service DNS names that must be SANs Gateway leaf SANs / consumers
standalone compose DNS:postgres, DNS:localhost (only for the documented host-port disposable test path) DNS:gateway; its Gateway process consumes runtime URL + CA + Gateway leaf/key only
federated compose DNS:postgres-federated, DNS:localhost (only for the documented host-port disposable test path) DNS:gateway-federated; runtime/migrator consume only their database-specific material
Portainer/Swarm federated test stack DNS:postgres (the in-stack service endpoint used by Gateway and migrator) DNS:gateway; same consumer isolation
two-gateway harness DNS:postgres-a, DNS:postgres-b DNS:gateway-a, DNS:gateway-b; each Gateway gets only its own runtime URL, CA, and Gateway leaf/key; each mosaic-db-migrator-{a,b} gets only the matching migration URL and CA

A new topology requires a versioned amendment before issuance. The PostgreSQL container activation artifact sets ssl=on, ssl_cert_file, and ssl_key_file to those paths, uses a locked-down postgresql.conf include, and verifies file ownership/mode before start. The server health/readiness gate makes a verify-full CA/SAN-validated connection as the approved runtime/migrator identity; pg_isready alone is insufficient. Migration Job starts only after server TLS readiness. Gateway replicas start only after a successful runner result and independently pass verified-TLS, identity, search-path, and ledger readiness. In the two-gateway harness this ordering occurs independently as postgres-a → mosaic-db-migrator-a → gateway-a and postgres-b → mosaic-db-migrator-b → gateway-b; no Gateway starts against its database before its own runner certificate succeeds.

Fresh DB: provision CA/leaf/secrets and server TLS configuration before initial database bootstrap; bootstrap/extension prerequisites run, then the runner migrates over verified TLS, then runtime deploys. Existing DB: take the approved backup, provision/mount TLS material, and drain/scale to zero every N-1 runtime, worker, CLI maintenance process, and replica before TLS enforcement. Enable server TLS, terminate any residual non-TLS PostgreSQL backend sessions, set pg_hba.conf to hostssl for all application/migrator CIDRs with no matching host rule, reload/restart as required, and prove the non-TLS session count is zero. Only then prove verify-full through the existing endpoint, run reconciliation/migration, and roll the non-owner TLS runtime. There is no plaintext transition interval.

Rotation/rollback: stage a CA bundle containing old+new trust roots to runtime/migrator, validate a new server leaf with exact SANs, restart PostgreSQL and validate it, roll consumers, then remove the old root only after evidence. Credential rotation remains independent and never mounts migration material into Gateway. Before expiry, rollback restores the prior known-valid leaf/key and overlapping CA bundle, restarts PostgreSQL, enforces hostssl, terminates residual non-TLS sessions, and verifies verify-full; it never downgrades sslmode, restores a plaintext-only N-1 runtime after enforcement, or accepts plaintext. A pre-enforcement abort may restore the backed-up N-1 state only before hostssl is enabled and is recorded as an aborted—not activated—release. The runbook records expiry windows, secret versions, backup ID, drained-service/session evidence, activation actor, and validation result—not secret values.

Required disposable tests cover standalone compose, federated/Swarm, and the two-gateway harness positives using verified TLS, plus for both gateway/database pairs: missing CA, wrong CA, wrong PostgreSQL SAN, wrong Gateway SAN, sslmode downgrade, missing/mispermissioned server or Gateway key, runtime-with-migrator-secret, cross-pair secret leakage, rendered secret-consumer/UID/GID/mode isolation, legacy plaintext drain/termination/hostssl enforcement, and readiness-before-migration negatives. PGlite local tests are explicitly classified as non-PostgreSQL and do not satisfy a PostgreSQL TLS test.

6. Runtime verification and sanitized failures

Before accepting traffic, runtime queries only parameterized/sanitized identity and privilege metadata on its already-open verified-TLS connection. It fails closed for owner/migrator identity or assumability; superuser/CREATEROLE/CREATEDB/BYPASSRLS; object/schema/database ownership; TEMPORARY; unexpected function execute; missing inherited capability/sequence/ledger grants; any non-exact search path; wrong schema/database target; bad TLS; or manifest/ledger mismatch. After KBN-100 it also checks every immutable grant/denial and relation presence.

The runner performs reciprocal preflight under the same session/lock: dedicated migration DTO only, verified TLS, exact allowlisted target, migrator session_user, schema-owner current_user, exact trusted search path, and no unsafe attributes. It fails before DDL otherwise.

Stable sanitized codes are DATABASE_RUNTIME_URL_REQUIRED, DATABASE_MIGRATION_URL_REQUIRED, DATABASE_TLS_REQUIRED, DATABASE_TLS_VERIFICATION_FAILED, DATABASE_ROLE_UNSAFE, DATABASE_ROLE_GRANT_MISMATCH, DATABASE_SEARCH_PATH_UNSAFE, DATABASE_SCHEMA_MISMATCH, DATABASE_MIGRATION_RECONCILIATION_AMBIGUOUS, DATABASE_MIGRATION_LOCKED, and DATABASE_MIGRATION_IDENTITY_UNSAFE. Logs/metrics may contain code, tier, manifest fingerprint, role class, and correlation ID only; never DSN, username, host, database name, SQL parameter, secret, or raw catalog result. External health exposes only unavailable/not-ready.

7. Safe DAG, activation, and rollback authority

Every KBN-101 card remains one PR with exclusive ownership. Cards 0007 may merge only as prepared, inactive capability: no current owner-runtime deployment consumes their image/config, and no compatibility switch is exposed to a runtime operator. They must not retain ALLOW_LEGACY_*, runtime DDL, DATABASE_URL migration fallback, plaintext TLS, direct Drizzle, or test-only bypass flags. Current owner-runtime deployments remain on their known N-1 release until final activation.

Card Depends on Exact ownership and required result
KBN-101-00 platform bootstrap / IaC contract Exclusively owns infra/pg-bootstrap/roles.sql, infra/pg-bootstrap/extensions.sql, infra/pg-bootstrap/README.md, and bootstrap tests: role graph, mosaic_extension_owner, fresh/approved-existing/legacy-owner eligibility, shadow migration interface, and bootstrap evidence. It owns no renderer, Compose, Portainer, Swarm, or deployment manifest.
KBN-101-01 runtime config/DB boundary 00 Typed split DTOs, verified TLS config, fixed schema/session verifier, no PostgreSQL Gateway startup migration.
KBN-101-02 entrypoint closure 01 Owns all §2 runtime paths except runner/journal: storage, CLI, adapters, Gateway startup, tier probes, fleet backlog, and direct-DDL test closure.
KBN-101-03 runner/manifest/schema foundation 00,01 Exclusively owns packages/db/package.json, packages/db/src/cli.ts, packages/db/src/migrator/runner.ts, packages/db/src/migrator/config.dto.ts, packages/db/src/migrator/manifest.ts, packages/db/src/migrator/identity.ts, packages/db/src/migrator/tls.ts, docker/db-migrator.Dockerfile, their tests, packages/db/src/schema.ts, generated snapshots/journal/migration, and exact DB/command tests. It owns the published mosaic-db-migrator bin, canonical Git-blob/LF manifest v1, fixed lock, journal logical-order repair, runner-only legacy-public bootstrap, relocation/approved-owner/shadow-path tests, ledger/manifest grants, and no renderer/bootstrap artifact. Shipped 0009 bytes remain unchanged.
KBN-101-04 installer/wizard/config 01 Production-like config persists no DSN and has no fallback; only non-secret references/injected variables.
KBN-101-05 compose/Portainer deployment 00,01,03 Exclusively owns tools/db/render-postgres-secrets.ts and tests; current local/federated Compose, Portainer test stack, two-gateway harness, and apps/gateway/Dockerfile; gateway 10001:10001, verified PostgreSQL image UID/GID, secret references/targets, TLS server/client mounts, server configuration, one immutable-image mosaic-db-migrator --run Job per database before readiness, and standalone/federated/Swarm/two-gateway TLS/order tests. It consumes—not edits—the versioned infra/pg-bootstrap interface.
KBN-101-06 CI/test topology 02,03,05 Disposable identities and pre-migrated/runner test topology; owns the finite scanner classifier/allowlist review, every-path DATABASE_URL-only-before-connect denial matrix (including public-import and live-doc routes), runner-prepared persistent pgvector fixture, and repository checks rejecting DDL bypasses/insecure URLs.
KBN-101-07 Vault/runbook/observability 00,04,05,06 Named control-plane/Vault-bootstrap handoff, activation/rotation/incident/backup/rollback runbook, sanitized observability evidence, and replacement of direct-DDL operator documentation (README.md, CLAUDE.md, docs/guides/dev-guide.md, docs/guides/deployment.md, docs/federation/SETUP.md, packages/storage/src/{cli,migrate-tier}.ts).
KBN-101-08 foundation certification and atomic activation release 00…07 Independent review verifies all prepared cards and terminal-green CI; Mosaic control plane/Jason authorizes one ordered activation: backup → drain/scale zero all N-1 runtime clients → TLS server start → terminate non-TLS sessions and enforce hostssl/no host rule → verify no plaintext session → roles → runner → verified readiness → rolling non-owner runtime. Any red test/readiness/CI result aborts; no force-on-red or bypass. Remove/disable all temporary compatibility support before this gate.
KBN-101-09 post-KBN-100 certification KBN-100,08 Real deployed-role immutable INSERT/SELECT and UPDATE/DELETE-denial evidence plus independent security/Ultron approval.

Authority: Mosaic control plane/Jason is the sole activation and rollback authority. CI, Gateway, migrator, Coordinator, and Certifier cannot activate, waive a red result, or force release. Before an incompatible KBN-100 switch, the authority stops/scales runtime, uses the approved backup/restore or separately reviewed runner artifact, restores only a known TLS-compatible runtime with its runtime secret after hostssl enforcement, and verifies no plaintext sessions plus TLS/readiness. Migration URL is never injected into Gateway to enable rollback. KBN-100 starts only after KBN-101-08; KBN-105 starts only after KBN-101-09.

ASSUMPTION K101-A2: every eligible production-like deployment can schedule a dedicated migration Job/one-shot command and an operator/IaC-controlled TLS bootstrap. A target that cannot do both is ineligible for KBN certification.

8. Acceptance traceability

Requirement / acceptance criterion Required implementation evidence
K101-REQ-01 / AC-K101-01 DTO/command matrix covers local/PGlite, standalone, federated, and both harness pairs; every finite classified §2 path rejects DATABASE_URL-only before connection/DDL; no runtime fallback/default; --help, argv, import-compile, and live-operator-route negatives pass.
K101-REQ-02 / AC-K101-02 KBN-101-03 one-session fixed two-int lock, --run/--verify exit-code, contention/crash/readiness/unrelated-key tests; manifest-v1 canonical bytes/digest and all reconciliation states; Gateway/replica DDL impossibility.
K101-REQ-03 / AC-K101-03 KBN-101-03 catalog relocation and future-Drizzle-only-mosaic proof; fresh/approved-owner/shadow extension paths; pg_extension.extowner, member/schema/version, and runtime/migrator/schema-owner ALTER/DROP/member-update denials; KBN-101-01/03 role, path, TEMP, ledger/default-grant, and pool-reset tests.
K101-REQ-04 / AC-K101-04 KBN-101-00 bootstrap-interface and KBN-101-05 renderer/deployment tests; immutable-image exact Job commands and runner-before-readiness order; fresh/existing verified-TLS Compose/Swarm/two-gateway positives; both-pair CA/SAN/downgrade/key-permission negatives; exact UID/GID/mode and runtime/migrator secret-consumer rendering/CI negatives.
K101-REQ-05 / AC-K101-05 KBN-101-09 after KBN-100: real deployed runtime INSERT/SELECT success and UPDATE/DELETE denial for each frozen relation, with RESTRICT retention evidence.
K101-REQ-06 / AC-K101-06 KBN-101-00…08 prepared-card/no-intermediate-deploy evidence; one final activation authority record; N-1 drain/zero-plaintext-session/hostssl, backup/restore, CA overlap rotation, TLS-only rollback, Vault/redaction, and no-force-on-red evidence.
K101-REQ-07 / KBN sequence KBN-101-08 foundation certificate before KBN-100, KBN-101-09 real immutable-role certificate plus Ultron approval before KBN-105; KBN-100 rebases/restores Drizzle consistency and never bypasses the serial gates.
Delivery integrity One-card/one-PR DAG, exact file ownership, docs/link/contract checks, independent author≠reviewer re-review on the pushed exact head, and terminal-green CI for implementation cards.

9. Non-goals and residual authority

KBN-101 planning does not create roles, certificates, Vault paths, migrations, deployment artifacts, or a deployed certificate. It does not replace KBN-100s data migration, immutable retention, tenant constraints, or KBN-105 endpoint freeze. A PostgreSQL superuser/break-glass operator remains outside application containment and requires separate audited platform controls, backup evidence, and drills.