Integrations deploy — INT-5 staged own-database repoint

Sprint: SPRINT_20260722_024 INT-5 (recipe S7+S8). Historical: the two PolicyStorage__* / ScannerStorage__* rows in the environment table below substituted the generic estate connection, which SPRINT_20260913_001 ORP-3 and SPRINT_20260722_021 PLT-3 retired on 2026-09-14 — integrations-web now reads Policy through its owner API and Scanner through the authenticated projection feed. Do not re-create those keys. Historical window overlay: devops/compose/docker-compose.integrations-consolidated.yml (retired from source by INT-7 after the successful window). Route swap (separate, INT-6): integrations-cutover-int4-route-swap.md.

This runbook covers the compose half of the INT-6 window: repointing integrations-web onto stellaops_integrations. It stages nothing live — the overlay is deliberately outside the default config_files list — and it is written to be read before the window opens, because two of its preconditions are not fixable inside the window.

Post-window canonical state (INT-7, 2026-08-24). The procedure below is retained as the executed/rollback record; do not add the retired overlay to a new compose command. docker-compose.stella-services.yml now carries the own Integrations DSN, Scanner retained-feed/auth configuration, Doctor settings, and the explicit separate Policy DSN. integrations-web resolves the canonical own-database variable directly and StellaOps.Integrations.Persistence is its only migration assembly. Rollback still uses the recorded predecessor images and database snapshot; no legacy schema or database was dropped by INT-7.

What the overlay changes

Applied, the overlay adds environment to the existing integrations-web key:

KeyValueWhy
ConnectionStrings__IntegrationsDb${STELLAOPS_POSTGRES_INTEGRATIONS_CONNECTION:?}the query plane
ConnectionStrings__Defaultsamethe host’s second resolution hop
STELLAOPS_POSTGRES_INTEGRATIONS_CONNECTIONsameso the doctor can compare intent against reality
ScannerStorage__Postgres__ConnectionString${STELLAOPS_POSTGRES_CONNECTION:?}keeps the Scanner reads on the shared database (D-INT4-4)
PolicyStorage__Postgres__ConnectionString${STELLAOPS_POSTGRES_CONNECTION:?}keeps the Policy + tenant-catalog reads there (D-INT7-1)
Doctor__Registration__Enabled${INTEGRATIONS_DOCTOR_REGISTRATION_ENABLED:-false}default-off doctor registration

It creates no new deployable, no new image, and no new publish key: the OD-2 re-scope (ADR-041 D2) moved the registry token service to the Registry family with its own database birth, so this family has exactly one compose key and it already exists.

Read this before opening the window

1. Setting the canonical variable does NOT repoint this service

integrations-web resolves ConnectionStrings:IntegrationsDb, then ConnectionStrings:Default, then a compiled-in Host=localhost;Database=stellaops_integrations literal. It never reads STELLAOPS_POSTGRES_INTEGRATIONS_CONNECTION — only the consolidated DAL does, and nothing deployed may reference that assembly before the window (D-INT3-1).

So a window that exports the canonical variable and stops there leaves the EF context, the startup migrations and the KEK control plane on the shared platform database, with a clean startup and a green route suite. That is why the overlay maps one variable into both ConnectionStrings keys.

The compiled-in literal is the second half of the same trap: with no configuration at all this host starts and points at a localhost PostgreSQL that does not exist inside a container. GET /doctor/integrations/checks reports both states distinctly — doctor.integrations.database.own-database-cutover fails critical either when the canonical variable is set and the query plane did not move, or when no surface resolved at all.

2. Two foreign read planes follow the repoint, and both fail SOFT

This is the item that costs the window if it is missed, and it is two defects with one shape:

PlaneResolution chain in Program.csOn failure
Scanner (scanner.*)ScannerStorage:Postgres:ConnectionStringConnectionStrings:DefaultConnectionStrings:IntegrationsDbcaught, logged at Warning/Debug; every registry image reads unevaluated
Policy (policy.gate_decisions + shared.tenants)PolicyStorage:Postgres:ConnectionStringPolicy:Postgres:ConnectionStringConnectionStrings:PolicyDb?? connectionStringcaught, logged at Warning; every gate verdict comes back empty

Neither key has ever been set for this service, so today both planes ride the Integrations DSN and work only because scanner.*, policy.* and shared.tenants are co-located in the shared database. Repointing the query plane takes both with it and the reads start throwing 42P01 at request time — caught, soft, and byte-identical to a correctly configured empty estate. Set both keys before the split, not after.

The policy plane is the worse of the two and it is the one nobody had named:

3. The discovery-outbox bridge cannot be staged in compose at all

Scanner.Worker’s RegistryImageDiscoveredBridgeService reads integrations.registry_image_discoveries by cross-schema SQL over Scanner’s own data source — no ProjectReference, no ownership-matrix entry. The moment the repoint applies, that FROM stops resolving. The bridge already swallows unavailability and retries, so registry auto-scan quietly stops bridging discovered images to scans, and a row-count parity check still passes.

No environment variable on this service fixes that, and pointing Scanner at the Integrations database would invert the ownership the window exists to establish. The cure is the discovery event (DC-15), which is why INT-6 depends on INT-3b. Until it lands, doctor.integrations.discovery-bridge.scan-plane-visibility is the alarm — and it deliberately reports Warning, not healthy, when this host has no Scanner DSN to measure the bridge’s plane through: the bridge may well be broken and the deployment simply cannot tell.

4. The crypto slice is a data move, and row counts will not catch it

integrations.integrations.credential_kek_version references crypto.kek_versions.version. The crypto schema is shared-library-owned rather than family-owned and the original design inventory missed it (D-INT3-5), so a window that copies only the integrations schema produces a database full of credentials that cannot be opened — with matching row counts on both sides.

doctor.integrations.credential-custody.kek-resolvable is the forcing function. It matches on (kek_id, version), not version alone: the ledger is keyed by both, so a destination whose crypto slice was seeded from another service’s ledger can hold version 1 under the wrong kek_id, and a version-only probe would call that resolvable while the credential stays unopenable.

5. The advisory catalog is already broken, and this overlay must not “fix” it

The SPRINT_20260722_008 G1 cutover moved the vuln schema to stellaops_vuln on its own container, so per-digest severity counts already return empty and log at DEBUG (D-INT3-6) — a live regression, independent of this window. The overlay deliberately sets no advisory DSN, and the conformance lint asserts that: the fix is the DC-30 v2 repoint onto the corpus artifact / hub read API. A connection string here would “fix” the symptom by re-creating the cross-database read the vuln-plane separation removed.

Preconditions (all must hold before the window)

  1. INT-3b’s discovery event has landed (item 3). Otherwise registry auto-scan silently stops the moment the repoint applies.
  2. STELLAOPS_POSTGRES_INTEGRATIONS_CONNECTION is set in the operator env file. The overlay uses :? on it, so compose refuses rather than starting the container against an empty connection string.
  3. stellaops_integrations exists, has converged the consolidated baseline, and carries the crypto slice (item 4).
  4. The gateway route swap is sequenced with this repoint, not before it (DC-19) — see the INT-4 runbook.
  5. Doctor registration stays OFF unless this service’s Authority client already holds platform:doctor:register. Flipping it early is a 403 loop against Platform.

Procedure

# 1. Rebuild the image from working-tree code (the doctor SDK is a new payload).
devops/docker/build-service-publish.sh integrations-web

# 2. Apply the overlay. A same-tag image does NOT swap on `up -d` — --force-recreate
#    is mandatory, every time.
docker compose \
  -f devops/compose/docker-compose.stella-infra.yml \
  -f devops/compose/docker-compose.stella-services.yml \
  -f devops/compose/docker-compose.integrations-consolidated.yml \
  up -d --force-recreate --no-deps integrations-web

# 3. Expect the service to re-send HELLO over Valkey. A gateway
#    503 "no instances available" right after the swap is fixed by one restart.
docker restart stellaops-integrations-web   # only if the gateway 503s

# 4. Verify the swap took: fresh image id, healthy, clean logs.
docker inspect --format '{{.Image}} {{.State.Health.Status}}' stellaops-integrations-web

config_files labels are heterogeneous across this stack: recreate each service with its OWN -f list, never with a list assembled for a different service.

Forcing function — what to check, and what not to accept

GET /doctor/integrations/checks under an ops.health-scoped token is the whole verification surface. A healthy container is not evidence here: every failure this window can produce is soft, and the service stays healthy through all of them.

Required after the repoint:

CheckExpectedIf it fails
…database.own-database-cutoverInfo, database.connected = stellaops_integrations, consolidated baseline applied, crypto.kek-ledger = presentthe repoint did not take, or the crypto slice did not travel
…scan-plane.dsn-attributionInfo, source = ScannerStorage:Postgres:ConnectionStringthe explicit key was dropped — registry image security state is silently gone
…policy-plane.dsn-attribution (historical; retired 2026-09-14, successor …policy-decisions.owner-api)Info, source = PolicyStorage:Postgres:ConnectionStringsame, for policy verdicts and tenant resolution — this posture no longer exists; see ORP-3
…discovery-bridge.scan-plane-visibilityInforegistry auto-scan has stopped bridging (item 3)
…credential-custody.kek-resolvableInfo, credentials.kek-unresolvable = 0inline-sealed credentials are unopenable
…severity-plane.advisory-reachableWarning is CORRECT pre-DC-30-v2do not “fix” it with a wider DSN (item 5)
…registry-auth.credential-readinessWarning while any active registry has credential_backend = 'none'provision credentials before the S7 bearer-auth flip on the interim zot

Plus the two data-plane forcing functions INT-6 owns: an inline-sealed credential round-trip (proves the crypto slice landed) and a discovered image bridging to a scan (proves D-INT3-3 was cured rather than silently broken).

2026-08-23 INT-6 executed receipt

The reversible window completed. The durable, secret-free receipt is 2026-08-23-integrations-int6-live. It is authoritative for the executed topology where INT-3b replaced the two raw-SQL planes described above with retained owner feeds.

Operational conclusions that must survive future reruns:

Doctor-SDK prune trap

The image now ships StellaOps.Doctor.Plugin.Abstractions.dll, which matches the *.Plugin.*.dll prune pattern by name. Verified for this stage against devops/docker/build-service-publish.sh: is_host_runtime_plugin_payload allowlists it for every key, so the image keeps it. If integrations-web ever crashes at boot with a FileNotFoundException for that assembly, that allowlist regressed.

Rollback

Drop the overlay from the -f list and force-recreate. The canonical file’s integrations-web key is untouched by this stage, so the rollback is the pre-window configuration exactly — but the DATA move is not rolled back by compose (ADR-004 is forward-only; recovery is a PostgreSQL snapshot restore, see migration-recovery.md).

The executed window preserved these exact reversible artifacts:

SurfaceAccepted rollback
integrations-webRecreate from stellaops/integrations-web:int6-pre-bf7731500e (sha256:efe0890788714b780ca826a6eec48a4ebb86006f328c6f4ee5730245f5b2396f) after dropping the image/own-DB overlays.
scanner-webRecreate from stellaops/scanner-web:int6-pre-bf7731500e (sha256:00a9a0f97b7102e920f429470935786cf4843038de18da70babce04acc03e6a4).
scanner-workerRecreate from stellaops/scanner-worker:int6-pre-bf7731500e (sha256:95b0bbc909697592c075b0f87be39090eff66a3a2d48d77754a861d0561ac228), drop the runtime plugin override, and remount the byte-exact predecessor bundle snapshot (1,910 files; aggregate SHA256 dcf436c2fe4f21bd04ecdc8120a8e44e4127cf0a675b6ba6b3403da9f07b1f91).
AuthorityDrop the INT-6 client override and recreate once from the exact predecessor compose chain. Image stays sha256:226a98d1699c4c74837b5e237e3943865a11fc3dc032babef7a590e09da72fdb.
Router/ConsoleDrop the route override and recreate from the snapshotted pre-window compose chain. Router image stays sha256:ff4a61b7224831ef6b20ceb9ec2e87caf8cd8ef760246098eca559bb1cd079e3; the committed source/config can be re-applied later without rebuilding Router.
PostgreSQLThe old source database and rows were retained; no drop/delete occurred. Repoint the service to the old DSN for application rollback. Schema downgrade still requires snapshot restore under ADR-004.
CanariesBoth records are retained in Disabled state with is_deleted = false; do not delete them.

The ignored runtime bundle paths are operator-local evidence, not source publication. Before closeout, all generated files under devops/plugins/scanner/** were restored byte-for-byte to HEAD; the accepted worker mounted only the ignored coherent bundle (1,909 files, aggregate SHA256 e4a2d1dc90cb1c7f46bdac7da9819f260331e9ceb851ffd0fb1424c7711f0df3).