Platform architecture (summary)
Audience: Platform WebService owners, service authors adopting the cross-cutting contracts, and operators wiring Console configuration. This is the orientation summary; defer to the linked module dossiers for contract-level detail.
This module covers two things: (1) the cross-cutting contracts and guardrails that every Stella Ops service must follow, and (2) the Platform WebService (StellaOps.Platform.WebService) — the runtime that backs the Console UI, serves frontend configuration, owns installation identity/settings and the cross-service tenant directory, and exposes federated-telemetry/NIS2 surfaces plus rebuildable read projections. It does not host ReleaseOrchestrator operational endpoints or migration authority.
Installation versus deployment ownership: Platform owns the Stella Ops installation control plane (
platform.*, setup state, installation settings, tenant directory, and read projections). ReleaseOrchestrator owns deployment topology and image placement (release.environments,release.targets, inventory/digests, promotions, and deployment execution). A shared physical PostgreSQL installation does not make Platform a second writer or migration owner for ReleaseOrchestrator tables. Environment, script, and regional-federation operations are hosted by ReleaseOrchestrator and routed there directly by Gateway. Platform also no longer compiles or hosts the dormant RO EvidenceThread implementation; its former Platform surface is absent rather than proxied.
Source of truth for the runtime described below:
src/Platform/StellaOps.Platform.WebService/Program.cs(composition root + endpoint map),src/Platform/StellaOps.Platform.WebService/Constants/PlatformScopes.cs+PlatformPolicies.cs(authorization), andsrc/Platform/__Libraries/StellaOps.Platform.Persistence/Migrations/Platform/(the embedded Platform-owned migration stream). The formerMigrations/Release/stream was never embedded or selected by Platform startup and was archived on 2026-09-14 (SPRINT_20260914_005 PEF-2) underMigrations/_archived/pre_1.0/mig061/Release/v1/. CM-6 boundary statements are verified againstd3358c84319b0da835e944d70f360f7df526d9c5; re-verify withrg -n "ProjectReference Include=.*ReleaseOrchestrator" src/Platform -g '*.csproj'andrg -n "Map(EvidenceThread|ReleaseOrchestratorEnvironment|Script)|FederationController|AddReleaseOrchestratorFederation" src/Platform/StellaOps.Platform.WebService -g '*.cs'.
Anchors
- High-level system view:
../../ARCHITECTURE_REFERENCE.md - Architecture overview:
../../ARCHITECTURE_OVERVIEW.md - Platform overview:
architecture-overview.md - Platform service definition:
platform-service.md - Cryptography & compliance defaults (first-run setup):
cryptography-and-compliance.md - Aggregation-Only Contract:
../concelier/guides/aggregation-only-contract.md(referenced across ingestion/observability docs)
Scope
- Identity & tenancy: Authority-issued OpToks, tenant scoping, RBAC, short TTLs; see Authority module docs. Tenant identity is resolved exclusively from the envelope-attached
stellaops:tenantclaim viaIStellaOpsTenantAccessor; raw tenant headers are not honoured. - Tenant directory: Platform owns
shared.tenants, the platform-wide tenant directory (slug → UUID resolution,is_default,default_regionresidency fallback), and — since SPRINT_20260722_016 AUTH-25 (2026-09-08) — writes it. Authority owns tenant lifecycle writes and emits them on thetenantscatalog; platform-web drains that feed and projects it into the directory over its own connection. Authority holds no connection tostellaops_platform(ADR-039). - Frontend configuration: anonymous
GET /platform/envsettings.jsonserves the AngularAppConfig, merged from three layers (env vars → YAML/JSON → DBplatform.environment_settings). - AOC & provenance: services ingest evidence without mutating/merging; provenance preserved; determinism required.
- Offline posture: Offline Kit parity, sealed-mode defaults, deterministic bundles.
- Platform Service: aggregation endpoints for health, quotas, onboarding, preferences, and global search, plus read-model projections (releases, topology, security, integrations, evidence), federated telemetry, NIS2 effectiveness telemetry, crypto/KEK administration, and GDPR subject-access. (The migration admin API it used to carry is retired — see Database migration registry and host below.)
- Migration catalog and Platform bootstrap:
StellaOps.Platform.Persistencestill holds the local/DR migration-module registry, but Platform no longer exposes any central migration face (SPRINT_20260722_026 CM-2). Every schema-owning service auto-migrates its own database on startup (§2.7), reports its own state through itsdb.migration-statusdoctor check, and Platform applies onlyStellaOps.Platform.Persistence.Migrations.Platform— it never creates authoritative deployment-topology tables such asrelease.environments,release.targets,release.regions,release.agents, orrelease.infrastructure_bindings. - Compatibility truthfulness: Platform-owned aliases may aggregate or proxy real module contracts, but Platform must not ship synthetic notify admin, quota/report, Signals, AOC, Console, registry, or inventory-command payloads on live runtime routes. Synthetic compatibility route groups (
/api/v1/console/*,/api/v1/aoc/*,/api/v1/notify/*,/api/v1/signals/*) are mapped only inDevelopment/Testing. The synthetic/api/v1/jobengine/quotas*group is retired in every environment; Platform is not its owner. - Observability baseline: metrics/logging/tracing patterns reused across modules; collectors documented under Telemetry module. Platform emits the
StellaOps.Platform.Aggregationmeter and NIS2 area metrics. - Determinism: stable ordering, UTC timestamps, content-addressed artifacts, reproducible exports.
Coordination
Platform docs are the starting point for new contributors; keep this summary in sync with module-specific dossiers and sprint references.
Notify build seam ruled (Q-19 + Q-24, 2026-08-27): contracts plus enqueue client. Platform’s backup and NIS2 emitters (
BackupRunEventPublisher,Nis2EffectivenessNotifyThresholdEmitter) stop compilingStellaOps.Notify.Queue/Notify.Modelsimplementation and repoint onto two verified-closed Notify-owned projects: the BCL-only wire-contract project shared with Q-11 and the separate enqueue-onlyStellaOps.Notify.EventClient. Publishing through domain-neutralStellaOps.Eventingwas rejected for these emitters — NIS2/backup notification ordering and delivery semantics must not change as a side effect of a build-boundary fix. The client preserves the existing Redis/NATS bytes, stream/subject, idempotency, partition, attributes and failure behavior without exposing lease/claim, delivery queues, health checks, persistence, hosts or migrations. Executed bySPRINT_20260722_026CM-7 — see the delivery note below.
Notify seam delivered (2026-09-05, CM-7).
StellaOps.Platform.WebServiceno longer referencesStellaOps.Notify.Queue. It compilesStellaOps.Notify.Contracts(BCL-only) andStellaOps.Notify.EventClient(enqueue-only), both classifiedcross-service-client-sdk/notify, and composesAddNotifyEventClientagainst the samenotify:queuesection with the same binding and the same fail-closed behavior. The three production consumers —BackupAlertService,BackupRunEventPublisher,Nis2EffectivenessNotifyThresholdEmitter— depend onINotifyEventPublisher; nousingdirective changed, because the moved types kept theirStellaOps.Notify.Models/StellaOps.Notify.Queuenamespaces and both predecessor assemblies forward every moved public type. Parity is structural rather than two implementations agreeing:StellaOps.Notify.Queuecomposes the same producer, and both encode through oneNotifyEventWireEncoder.Verified against source commit
257f05bbd3and re-verified at the landing mergedede3cfe8ac9b45632a8087617f90eb19a4d51bd: the Release publish’sdeps.jsoncompiles noStellaOps.Notify.Queue; theplatform|notifyregister pin shrank 3 → 2. CM-2’s 2026-09-09 source increment removes the remaining migration-plane reference andNotifyMigrationModulePlugin, retiring that pair. The host build also exposed the NIS2 threshold payload still arriving throughNotify.Models; that payload and its two routing records now live unchanged inNotify.Contracts, with the existing Models assembly forwarding their identities for prebuilt consumers. Platform no longer compilesNotify.PersistenceorNotify.Models. Re-verify withpwsh tools/scripts/build-boundary/generate-build-boundary-report.ps1 -Checkandpwsh ./tools/scripts/test-targeted-xunit.ps1 -Project src/__Tests/architecture/StellaOps.Architecture.Contracts.Tests/StellaOps.Architecture.Contracts.Tests.csproj -Class "*BuildBoundaryConformanceTests*".
ReleaseOrchestrator build seam delivered (Q-10, 2026-08-25): contract-seam. Platform and ReleaseOrchestrator remain distinct services — one-family was rejected to preserve the distinct RO/agent-core trust boundary. Platform compiles no RO implementation: its sole RO project edge is the producer-owned, zero-reference
StellaOps.ReleaseOrchestrator.Topology.Contractswire project. Environment/script/federation endpoints are absent from Platform and owned by RO; the former Platform EvidenceThread surface is absent and no dormant EvidenceThread implementation is hosted. The ruled ownership direction remains: RO owns environment/script/deployment operations and drives deployment agents (pull or push); RO reads Platform’s non-release settings by Platform’s contract/API only. Execution:SPRINT_20260722_026CM-6 (settlement receipt there).
Platform WebService runtime
StellaOps.Platform.WebService is the Console backend. It is a router-microservice (AddRouterMicroservice(serviceName: "platform")) that reaches the gateway over the Valkey/Redis transport and is authenticated with Authority-issued bearer tokens verified by AddStellaOpsResourceServerAuthentication. All bound options live under the Platform configuration section (PlatformServiceOptions).
Storage and startup posture
- Storage is selected by
Platform:Storage:PostgresConnectionString. When present the service binds Postgres-backed stores; otherwise it binds in-memory stores. Startup fails fast (InvalidOperationException) when the connection string is absent outside theTestingenvironment — there is no silent localhost fallback. Platform:Storage:Schemadefaults toplatform. Startup migration bookkeeping is owned byplatform.schema_migrations. The Platform stream createsplatform.*, the cross-cuttingshared.tenants/shared.actor_identitydirectory, and existing Platform-owned compatibility projections underrelease.*; it does not create ReleaseOrchestrator’s authoritative environment/target/agent topology tables.
Release topology projection owner seam
Platform rebuilds its topology inventory without opening the ReleaseOrchestrator database. In the PostgreSQL runtime it reads only active tenant UUIDs from its own shared.tenants, then calls the RO owner directly at GET /internal/v1/release-orchestrator/topology-snapshot once per tenant. The internal call does not traverse Gateway and has no Router route or SQL fallback.
The call uses the producer-owned schema-v1 contract project and a signed Router identity envelope whose tenant is fixed to the requested UUID, whose only scope is release:read, and whose lifetime is two minutes. STELLAOPS_RELEASE_ORCHESTRATOR_URL is the only accepted owner-base configuration; missing or invalid HTTP(S) configuration fails startup. A non-success response, malformed or unsupported payload, duplicate/empty identifiers, tenant mismatch, or cross-tenant row from the owner fails closed rather than returning a partial projection.
The owner snapshot returns regions, environments, targets, and agents in deterministic order. Agent capabilities are the ordinal-sorted distinct union of the built-in capability enum and extended capability strings; the owner maps canonical Stale status to wire status offline. Platform maps the validated wire response into its own rebuildable release.topology_*_inventory and context projections. It never reads RO’s release.regions, release.environments, release.targets, or release.agents tables.
Subject-access erasure owner seam
POST /api/v1/operator/sar/{actorRef}/erase remains a Platform-owned, tenant-scoped platform:sar:erase workflow. Platform first erases the mutable shared.actor_identity projection, then asks the Evidence owner whether immutable legacy capsules must be retained under Article 17(3). Platform does not query evidence_locker.* tables and does not treat an unavailable owner as an empty retention result.
The owner call uses the producer-owned StellaOps.EvidenceLocker.Contracts wire records and a short-lived signed Router identity envelope containing the original authenticated tenant claim, requester subject, correlation ID and only evidence:create. Before sending, the same Platform tenant resolver used by the erasure API must resolve that claim to the requested canonical UUID. The UUID remains the actor-data identity; replacing the claim would break the Evidence owner’s configured tenant allowlist. Missing authentication, an unknown tenant or a different resolved UUID refuses the owner call before transport. Configuration is selected once at composition:
STELLAOPS_EVIDENCE_URLorPlatform:EvidenceUrlselects the consolidated/api/evidence/v1/evidence/legacy-capsule-erasure-refusals/{actorRef}route.- Without a configured canonical origin, the source default is
http://evidence.stella-ops.local:8080. Predecessor configuration cannot select a route.
This is configuration precedence, not runtime failover. Missing envelope-signing material, transport failure, any non-success status, or an empty/malformed/internally inconsistent response fails the request; it never retries a second owner host, opens a shared database, or fabricates completed. The identity update and owner evaluation remain two ordered service operations, not a distributed transaction. See SPRINT_20260722_021 PLT-1A and the EVD-9 runbook.
Environment-state custodian (platform.environment_state)
Platform is the generic custodian of environment posture — never its interpreter (owner ruling A2; docs-archive/modules/export-center/consolidation-design.md §6.3a). platform.environment_state stores one versioned state document per (class, scope) — {class, scope, state_json, version, declared_at, declared_by} — and the same row shape serves airgap-seal, time-anchor, and every future class.
- Custody only.
PUT/GET /api/v1/platform/environment-state/{class}/{scope}.state_jsonis opaque: the only inspection Platform performs is “is this valid JSON”. A class→policy registry inEnvironmentStateEndpointsdecides who may declare each class, and a class that is not registered is refused with 403 — Platform will not hold state it cannot attribute an owner for. Adding a class is a dictionary entry, never a code path; a conformance test (EnvironmentStateAgnosticismGuardTests) fails the build if air-gap semantics or a per-class branch appear in the custodian source. - Monotonic versions. Every write bumps
versionin SQL (version = environment_state.version + 1), so concurrent declarations cannot mint the same version — a replica treats an equal version as “not newer” and would silently drop one. - Replication, not RPC. Each write appends a
catalog.changedevent on theenvironment_statecatalog stream (key{class}:{scope}, payload = the full document) in the same transaction as the row. Consumers drain it into their own database and interpret it locally, so a sealed estate never calls back and posture survives the producer being down. - Deliberately not
platform.environment_settings: that is layered key/value resolution of effective values; this is versioned documents with declaration custody.
Composer pipeline and /platform/envsettings.json
The anonymous GET /platform/envsettings.json endpoint (alias GET /envsettings.json for direct service access) returns the Angular frontend AppConfig (EnvironmentSettingsResponse: Authority/OIDC settings, ApiBaseUrls, optional telemetry/welcome/doctor blocks, and a Setup state). The payload is built by EnvironmentSettingsComposer.ComposeAsync from three layers, lowest-to-highest priority:
- Environment variables —
STELLAOPS_*_URLvalues folded intoApiBaseUrlsbyStellaOpsEnvVarPostConfigure(anIPostConfigureOptions<PlatformServiceOptions>). - YAML/JSON config — standard
IOptionsbinding ofPlatform:EnvironmentSettings(and../etc/platform.yaml/platform.yaml). - Database overrides —
platform.environment_settings(key/value), overlaid last viaIEnvironmentSettingsStore. Keys followApiBaseUrls:{service}for per-service base URLs, or scalar names (ClientId,TokenEndpoint,OtlpEndpoint,WelcomeTitle,DoctorFixEnabled,PlatformVersion, …).
PlatformVersion — the installation’s product version
PlatformVersion is the single source of the product version the console shell displays (the string under the brand mark in the sidebar). It is an ordinary environment setting: composed through the three layers above, served in the platformVersion field of /platform/envsettings.json, and changeable at runtime by an operator via PUT /platform/envsettings/db/PlatformVersion without a rebuild.
Two properties are deliberate:
- No compile-time default.
PlatformEnvironmentSettingsOptions.PlatformVersionis null unless a layer supplies a value, the endpoint omits the field when it is blank, and the Angular shell then renders no version element at all. A stale version is worse than no version on a product whose claim is verifiable evidence, so nothing falls back to a literal. - The row is established by migration. Because there is no default,
Migrations/Platform/092_PlatformVersionSetting.sqlinserts the row (v1.0.0-RC1) so the value is present on every database the service starts against. It is idempotent and skips the write when the value already matches.
This replaced a hardcoded string in app-sidebar.component.ts, which had drifted from the 1.0.0-alpha1 in the service .csproj files and from the 1.0.0 the CLI and GET /platform/metadata report. Note that PlatformVersion governs only what the console displays; assembly/InformationalVersion values remain per-project and are not derived from it.
DB-layer overrides are managed through the authenticated admin API GET/PUT/DELETE /platform/envsettings/db[/{key}] (platform.setup.read to list, platform.setup.admin to mutate; mutations are audited). EnvironmentSettingsRefreshService (hosted) re-reads the DB layer on the Platform:Cache:EnvironmentSettingsRefreshSeconds cadence (default 60 s) and reacts to Valkey pub/sub dirty signals when ConnectionStrings:Redis is configured.
Region-scoped signature-verification material (Verification:*)
Operator-entered, region-scoped signature-verification keys/trust-roots are stored as ordinary platform.environment_settings rows (no schema change) under reserved verification namespaces, grouped by the active regional crypto profile (world/fips/gost/sm/kcmvp/eidas, see ComplianceProfiles):
Verification:<profile>:PacksRegistry:PublicKeyPem— materialized active RSA public key (PEM) for pack signature verification.VerificationKeyMeta:<profile>:pack-public-key:<keyId>— version-ledger JSON. Every Pack record retains its exact non-secret public PEM so active + retiring trust survives consumer restarts.Verification:<profile>:VexHub:TrustRootFingerprints— non-secret membership index; VexHub HMAC root bytes are sealed in the encrypted credential store and never served here.Verification:<profile>:VexHub:EnableSignatureVerification— explicittrue/falseacknowledgement.
The platform STORES + SERVES these admin-entered values; it never generates keypairs. Pack lifecycle rows are written through the verification-key lifecycle API, not arbitrary environment-setting mutation. The existing generic admin PUT retains only a strict first-install bootstrap for an exact known-profile scoped Pack scalar while no Pack ledger exists; exact retry is idempotent, while replacement, metadata/progress mutation, and generic deletion are rejected. Generic PUT/DELETE attempts against lifecycle-owned keys return HTTP 409 with stable code verification_setting_lifecycle_owned; a strict-bootstrap conditional race returns 409 verification_root_state_conflict. A pre-existing single-region legacy install may omit the <profile> segment (Verification:PacksRegistry:PublicKeyPem); resolution still falls back to that read-only legacy form and the next lifecycle introduce captures it before rotation.
Because the /platform/envsettings.json composer is frontend-only and drops unknown keys, backend services do not receive this material through that payload. Instead, VerificationSettingsResolver maps the stored rows into flat, service-local configuration values and serves them at the existing GET /platform/verification-settings/{service}?profile=<active> route (VerificationSettingsEndpoints). For PacksRegistry the response contains the compatible active scalar (PacksRegistry:Verification:PublicKeyPem), a canonical JSON live trust set (PacksRegistry:Verification:TrustSetJson), and its deterministic epoch (PacksRegistry:Verification:TrustEpoch). No new route is involved.
The Pack trust set is schema stellaops.packsregistry.verification-trust-set/v1. Its roots have fixed keyId, lowercase status (active/retiring), and exact publicKeyPem fields; they are ordinal-sorted by key id then status. A Pack key id is stored in exact trimmed form, contains no control characters, and is at most 256 UTF-8 bytes. Introduce rejects a violating id before any write, while publication fails closed on a violating retained row and requires exact operator recovery rather than truncation or relabeling. The epoch is sha256: plus lowercase SHA-256 of UTF-8 without BOM over profile + "\n" + canonicalJson. A real Pack ledger must contain exactly one active root and at most one retiring root to publish. A distinct introduce is rejected atomically while that retiring root exists; exact retry remains idempotent, and operators must finish/invalidate rather than stack rotations. Exactly one RSA PUBLIC KEY / RSA PUBLIC KEY PEM block plus surrounding whitespace is accepted; private-key or mixed content is rejected before introduce writes so secret material cannot enter the non-secret ledger or response. Platform normalizes every live RSA root by importing the PEM, exporting canonical DER SubjectPublicKeyInfo, and hashing it with SHA-256; duplicate normalized keys fail publication even when their key ids or PEM encodings differ. Archived Pack roots are audit-only because packs are mutable and re-enveloped; revoked roots are excluded. Missing/malformed metadata, missing or invalid included PEM, duplicate key ids or normalized RSA keys, case-insensitive key-id collisions or lifecycle-key variants, multiple retiring roots, or an ordinal active-ledger/scalar mismatch fails closed rather than silently shrinking trust.
The direct Pack lifecycle snapshot is bounded before values are materialized: one profile may retain at most 256 root rows, each lifecycle value may occupy at most 64 KiB UTF-8, and the full snapshot may occupy at most 4 MiB UTF-8. PostgreSQL reads byte lengths first with sequential access and limits output to one row beyond the ceiling. Both PostgreSQL and in-memory mutation stores validate the complete projected state against the same bounds before their first write. Separately, the complete outer PacksRegistry verification-settings JSON response must fit the consumer’s 64 KiB startup transport ceiling: introduce/bootstrap serialize the exact projected dictionary with the endpoint options before write, and retained over-bound publication fails closed. A corrupt or historically over-bound snapshot fails publication and mutation closed for owner-approved recovery; terminal audit history is never silently omitted, truncated, or reclassified as live trust.
For scalar-only legacy installations, Platform synthesizes the honest identity legacy-pem-sha256-<sha256-of-exact-UTF8-PEM-text>. The first lifecycle introduce persists that scalar as a retiring record before overwriting it with the new active PEM. A legacy active ledger record that lacks publicMaterial is similarly backfilled from the scalar before demotion; an unrecoverable value aborts before any write and requires operator repair.
Every retained Pack ledger row must therefore carry parseable public material before a later introduce. A pre-repair archived/revoked row that lacks publicMaterial cannot be compared cryptographically and blocks rotation before any write; it is never silently ignored or deleted. The generic settings API is not a repair seam. The operator must recover the exact original public PEM from custody/audit evidence and obtain an owner-approved, audited ledger repair; if that material is unavailable, rotation remains fail-closed and must be escalated rather than relabeling the historical key.
Pack lifecycle writes cross the flat settings store through a narrow conditional-mutation seam. Its expected-state token covers the scoped and unscoped compatible scalars, every Pack ledger row, and Pack re-envelope progress for the profile. PostgreSQL takes a deterministic profile advisory transaction lock, bypasses the general settings cache for an in-transaction re-read/token comparison, and commits the retiring row, active row, scoped scalar, and re-envelope seed as one atomic batch. In-memory mode performs the same compare-and-batch operation under one gate. A concurrent mutation receives a typed conflict before any write; this prevents two active rows and last-writer scalar selection across replicas. An exact retry of the already-active key id and ordinal-equal PEM is idempotent (including commit-then-response-loss), while reuse of that key id with different material is rejected. A new key id whose normalized RSA SPKI fingerprint matches any Pack ledger member is also rejected, including an alternate PKCS#1/SPKI PEM encoding and archived/revoked history; invalidation cannot make the same cryptographic key eligible under a new label. A distinct key is also rejected while any root remains retiring, so the live trust set cannot grow through stacked rotations. Pack invalidate and progress ingestion use the same mutation boundary, and both generic store implementations reject lifecycle-owned Pack keys from ordinary set/batch/delete calls. Thus the admin route and internal fixed-key callers cannot bypass profile serialization. Under the locked unique snapshot, an update or delete resolves a case-insensitive logical match to the exact pre-existing physical key spelling; canonical spelling is created only when no row exists. This keeps PostgreSQL and in-memory behavior aligned without silently duplicating a mixed-case legacy row. Pack key ids are case-insensitively unique, while exact ordinal active-key/PEM retries retain their idempotent behavior.
Typed conditional-state races from introduce, invalidate, or Pack progress ingest are returned as HTTP 409 with stable code verification_root_state_conflict; validation errors remain 400. Invalidating a sole active Pack root clears its scalar and leaves publication fail-closed. A later consequences-gated introduce may establish one cryptographically distinct active root only when the ledger has zero active members, no retiring member, and no active scalar row; its normalized RSA SPKI fingerprint must not match any retained archived or revoked row. Those audit rows are preserved unchanged and never auto-promoted.
The verification-settings endpoint uses application-level authentication: a service pulling at boot presents the shared internal HMAC secret (Router:IdentityEnvelopeSigningKey) in X-Stella-Verification-Token, with authenticated platform.setup.read as the fallback. Validator-wave services consume the flat response via PlatformVerificationSettingsConfigurationSource (in StellaOps.Hosting.RuntimeConfiguration), added to builder.Configuration before their verification options bind — one HTTP GET at startup, fail-soft at the source (if Platform is unreachable it contributes nothing) followed by the service’s fail-loud startup guard. Pack publication bypasses the general settings cache and reads a direct coherent Pack snapshot on every request, so a fresh consumer cannot receive pre-revoke trust from a warm Platform replica. VexHub retains the general cached read. Key changes still require a consuming service restart because configuration is read once at boot.
Tenant directory (shared.tenants)
Platform owns the shared.tenants directory table (migration 000_shared_tenants_bootstrap.sql):
| Column | Notes |
|---|---|
id UUID | Canonical tenant UUID (slug claims resolve to this via IPlatformTenantResolver). |
tenant_id TEXT UNIQUE | Tenant slug (e.g. default). |
is_default BOOLEAN | Optional installation-default marker. A partial unique index allows at most one marked row; existing and clean installations may legitimately have none. Request tenant resolution does not use this marker. |
default_region VARCHAR(16) | NOT NULL DEFAULT 'unspecified', constrained to ^[a-z0-9][a-z0-9_-]{0,15}$. Platform-owned data-residency fallback consumed by EvidenceLocker when a request carries no explicit region (migration 078_SharedTenantDefaultRegion.sql). |
status, name, display_name, settings, metadata | Lifecycle/state owned by Authority and propagated via ISharedTenantsPropagator; see ../authority/tenant-model.md. |
IPlatformTenantResolver resolves request claims by an active, case-insensitive tenant_id slug (or accepts an already-canonical UUID); it does not infer the request tenant from is_default. The optional local/demo well-known actor seed follows the same invariant. Historical seed S078_SeedWellKnownActorIdentity.sql looked for is_default=true and can therefore be recorded as a successful no-op on older directories. Forward-only S079_RecoverWellKnownActorIdentity.sql recovers those histories by targeting the active canonical default slug without creating a tenant, changing is_default, overwriting a self-upserted row, or restoring erased PII. See the well-known actor seed recovery runbook.
Federation and NIS2 telemetry endpoints
- Federated telemetry —
/api/v1/telemetry/federation/participation/*is tenant-scoped consent and authenticated fact ingress (telemetry:participation:read|write,telemetry:facts:write);/installation/*is installation status, bundles, intelligence, privacy budget, and trigger (platform:federation:read|write). Grant/revoke/fact actors come from authenticated identity. Platform migrations 084-086 persist installation identity, proofs, eligible facts, atomic privacy spending, consent-set material, bundles, peer delivery receipts, pending outbox rows, and imported aggregate intelligence underplatform.telemetry_federation_*. Enabled startup rejects fallback in-memory stores. Egress is destination-allowlisted and sealed-mode fail-closed. - NIS2 effectiveness telemetry —
GET /api/telemetry/nis2/effectiveness?tenantId=&window=returns the live thirteen-area NIS2 effectiveness dashboard (nis2-effectiveness-dashboard-v1) derived from Telemetry Core KPI samples. Authorized byanalytics.read, tenant-scoped,window∈{rolling-30d, rolling-90d}(defaultrolling-90d). It carries honestknownBlockersfor the target-override audit API and the monthly signed export, which are not yet wired.
Database migration module and host
StellaOps.Platform.Persistence declares Platform’s own migration module statically: PlatformMigrationModule (name Platform, migration bookkeeping schema platform, resource prefix StellaOps.Platform.Persistence.Migrations.Platform). It is consumed by platform-web’s AddStartupMigrations call and by the guided-setup wizard (PlatformSetupService / PlatformSetupMigrations), which admits only that module and fails closed on any other name. The MigrationModuleInfo / MigrationModuleSourceInfo record types and MigrationModuleConsolidation remain for those two consumers.
The central migration-plugin mechanism is deleted (SPRINT_20260722_021 PLT-4, DC-26, 2026-09-14). IMigrationModulePlugin.cs, MigrationModulePluginDiscovery.cs (assembly scan, plugins/migrations directory and STELLAOPS_MIGRATION_PLUGIN* env-var probing), MigrationModulePlugins.cs, ReleaseMigrationRunner.cs, ServiceCollectionExtensions.cs and the static MigrationModuleRegistry (GetModules / FindModule) no longer exist, and the CLI no longer references StellaOps.Platform.Persistence. Nothing discovers modules any more, so nothing can converge a schema it does not own. Each deployable service owns and auto-migrates its own database from its own embedded Startup-band SQL; one service = one database + own role (ADR-039). The per-plugin retirement notes below are the dated history of how the registry shrank to zero.
The registry was no longer a Platform runtime surface before that. SPRINT_20260722_026 CM-2 retired every central face: /api/v1/admin/migrations/* and /api/v1/admin/seed-demo were deleted along with their gateway route (2026-08-02), PlatformMigrationAdminService was folded into the assembly-internal Services/PlatformSetupMigrations.cs used only by guided setup and guarded so it can apply only Platform’s own module (2026-08-04), and stella admin seed-demo — a third face that ran startup migrations across every registered module — was deleted (2026-08-18). Platform therefore offers no way to migrate, verify, or seed another service’s database. Three conformance pins in DatabaseOwnershipConformanceTests keep it that way: CentralMigrationAdminApi_StaysRetired, CentralMigrationAdminService_StaysFolded, and CentralMigrator_HasExactlyOneSurvivingCliFace.
What replaced it:
- Status — each service’s own
db.migration-statusdoctor check, read throughGET /doctor/platform-web/checks(SPRINT_20260722_009 DOC-1). There is no cross-module status view by design; a central one could only be built by reaching into databases the caller does not own. - Convergence — each service’s startup migrations (§2.7). No operator action in steady state.
Manual DR applier — theDeleted 2026-09-14 (PLT-4). It was the one surviving consumer of the registry, kept for air-gap disaster recovery; the precondition for deleting it is that no Release-category (stella system migrations-*command group.100+/rollback) or Data-category (DM) migration exists anywhere insrc/**/Migrations/*.sql, guarded byNoManualCategoryMigrationsTests. A pending one now fails the owning host closed with the remediation “renumber into the Startup band, or apply by hand perdocs/runbooks/database/migration-recovery.mdand record the ledger row”.
The remaining in-process path:
Startup auto-migration — at boot Platform calls
AddStartupMigrations(schemaName: "platform", moduleName: "Platform", ...)with theStellaOps.Platform.Persistence.Migrations.Platformprefix, so only the Platform-owned stream is applied automatically. Since PLT-4 (2026-09-14) that stream’s098_v1_release_topology_inventory_projection.sqlcreates therelease.topology_*projection tables platform-web writes in its own database; a freshstellaops_platformconverges to the schemascatalog_replica crypto eventing platform public release release_app shared(evidence:docs/implplan/_evidence/20260914-plt4-closeout-3/README.md). W3-07 / CM-2 removed the orphanedSbomLineageregistry module, its source implementation edge, and the two Platform-owned preflight/repair resources on 2026-08-24; recorded evidence showed its parallel tables had never existed live, and the SbomService host owns the canonicalsbomchain. The old Release-stream baseline (since 2026-09-14, SPRINT_20260914_005 PEF-2:Migrations/_archived/pre_1.0/mig061/Release/v1/001_v1_platform_database_release_baseline.sql) is frozen history that was never embedded. The same sprint added099_v1_release_app_tenant_function_convergence.sql, which converges therelease_app.require_current_tenant()body live estates inherited from pre-consolidation writers (plpgsql,SECURITY DEFINER, readsapp.tenant_id) to the embedded 001 body (sql, readsapp.current_tenant_id, the key Platform’s stores set); the four RLS policies onrelease.control_bundle*call it. ReleaseOrchestrator alone embeds and applies the deployment-topology migrations that createrelease.environmentsand related source tables. Seed-category migrations run only whenPLATFORM_BOOTSTRAP_ENABLED=true; the clean default seeds no demo tenants. Because the runner records a successful no-op, operators recovering S078 must verify that the active canonicaldefaultslug exists before enabling the one-time S079 seed pass, then restore the bootstrap flag tofalse; see the linked recovery runbook above.RO plugin retirement — CM-6 removed
ReleaseOrchestratorMigrationModulePluginand its source-project reference fromStellaOps.Platform.Persistence. RO’s database is therefore absent from both Platform startup and the local-DR registry; RO converges and reports its own database from its own host.Evidence-family plugin retirement (2026-09-05) — CM-2 removed
AttestorMigrationModulePlugin(schemaproofchain) andEvidenceLockerMigrationModulePlugin(schemaevidence_locker) together with the twoStellaOps.Platform.Persistenceproject references they alone consumed (StellaOps.Attestor.Persistence,StellaOps.EvidenceLocker.Infrastructure). The SPRINT_20260722_011 EVD-9 window executed on 2026-09-05, so the consolidated evidence family serves both schemas fromstellaops_evidencethroughStellaOps.Evidence.Persistence.Consolidated. Neither plugin was ever reachable from Platform startup or guided setup — both admit only thePlatformmodule — so the face this deletion closes is the unguarded local CLI applierstella system migrations-run --module Attestor|EvidenceLocker. Platform’s own startup behaviour is unchanged.EvidenceMigrationModulePluginis untouched: itsevidenceschema belongs to the unrelated shared librarysrc/__Libraries/StellaOps.Evidence.Persistence(D-EVD3-14).Notify plugin retirement (2026-09-09) — CM-2 removes
NotifyMigrationModulePluginand its soleNotify.Persistencereference after NTF-9’s database cutover and NTF-10’s predecessor retirement. The removed plugin matched all four embedded legacy Notify migrations; it could apply them through the local CLI and was not inert. Both surviving Notify hosts callAddConsolidatedNotifyPersistencefor own-database startup convergence, followed by repository-onlyAddNotifyRuntimeRepositories. Removing the central plugin prevents the CLI from applying that predecessor lineage to another database. Platform startup and guided setup continue to admit only Platform’s own module.IssuerDirectory plugin retirement (2026-09-09) — CM-2 removes
IssuerDirectoryMigrationModulePluginand its soleIssuerDirectory.Persistencereference after the AUTH-9 fold completed on 2026-09-08. Authority migration024_authority_issuer_directory_tables.sqlowns the separateissuerschema throughauthority.schema_migrations; the folded runtime registers no second migration host. The removed prefix-free plugin could reach all four predecessor resources through the local CLI. Platform’s graph loses both IssuerDirectory.Persistence and IssuerDirectory.Core; the standalone host’s remaining source retirement stays with AUTH-10.
Authorization scopes
Platform maps OAuth scopes to named authorization policies in Program.cs via PlatformPolicies → PlatformScopes (Constants/). ops.admin is a global escape hatch for the crypto/KEK policies. Selected mappings:
| Policy | Required scope(s) | Surface |
|---|---|---|
HealthRead / HealthAdmin | ops.health / ops.admin | /api/v1/platform/health/* |
QuotaRead / QuotaAdmin | quota.read|orch:quota / quota.admin|orch:quota | quotas + legacy quota compatibility |
OnboardingRead / OnboardingWrite | onboarding.read / onboarding.write | /onboarding/* |
PreferencesRead / PreferencesWrite | ui.preferences.read / ui.preferences.write | /preferences/*, dashboard profiles |
ContextRead / ContextWrite | platform.context.read / platform.context.write | /api/v2/context/* |
SearchRead / MetadataRead | search.read / platform.metadata.read | global search, metadata |
AnalyticsRead | analytics.read | /api/analytics/*, NIS2 telemetry |
SetupRead / SetupWrite / SetupAdmin | platform.setup.read / .write / .admin | setup wizard, env-settings DB layer, migration admin, seed |
FederationRead / FederationManage | platform:federation:read / platform:federation:write | Platform federated telemetry only; RO applies the same canonical scopes to its separately hosted regional-federation controller |
SubjectAccessRead / SubjectAccessErase | platform:sar:read / platform:sar:erase | GDPR subject-access |
ActorIdentityRead (any-of) | ui.read or platform:sar:read | console actor-identity badge resolver (GET /api/v1/platform/actor-identity/{ref}) — any console user resolves the redaction-aware projection (i5 #17) |
EnvironmentStateRead (any-of) | envstate:read or airgap:status:read | read any environment-state document (GET /api/v1/platform/environment-state/{class}/{scope}) |
EnvironmentStateAirgapSealWrite (any-of) | envstate:airgap-seal:write or airgap:seal | declare the airgap-seal state document; the legacy grant is aliased so existing sealing authority survives the re-homing |
EnvironmentStateTimeAnchorWrite | envstate:time-anchor:write | declare the time-anchor state document (separate authority: declaring trusted time is not sealing) |
CryptoProviderRead / CryptoProviderAdmin / CryptoProfileAdmin | crypto:read / crypto:admin / crypto:profile:admin (or ops.admin) | crypto provider + compliance profile admin |
OperatorSigningEnrollmentRead | authority:signing-keys.enroll (or crypto:read / ops.admin) | narrow tenant compliance-profile projection for operator public-key enrollment |
CryptoKekRead / CryptoKekRotate | crypto:kek:read / crypto:kek:rotate (or ops.admin) | KEK control plane |
TrustRead/Write/Admin, Script*, ReleaseControl* | see PlatformScopes.cs | trust signing, scripts, release-control bundles |
(The frontend OIDC scope superset requested by the SPA lives in PlatformEnvironmentSettingsOptions.Scope.)
Operator signing provider-change composition
PUT /api/v1/admin/crypto-providers/compliance-profile/ and PUT /api/v1/admin/crypto-providers/preferences compare the resolved decision-signing provider before and after the durable Platform write. An unchanged provider creates no new transition, but the request still claims and resumes any existing transition for that tenant. A changed provider is composed through the owning services: Authority resolves enabled users whose effective permissions include exception approval, IssuerDirectory retires only incompatible active DecisionSigning keys, and Notifier accepts platform.crypto-provider-changed only when keys were actually retired. The event targets only retired-key subjects and deep-links to /administration/profile; its event id, payload ordering, and idempotency key are deterministic for replay. Platform:OperatorProviderChange:IssuerId selects the operator key namespace and defaults to operator-signing.
OSK-P5R makes the composition restart-safe. PostgreSQL migration 088 owns platform.operator_provider_change_outbox; the profile/preference mutation and transition intent commit in one transaction. The row freezes tenant, previous/current provider, ordered algorithms, original actor, stable operation id, stage, attempt/backoff, Authority recipients, the IssuerDirectory receipt, and the exact Notify idempotency key/body. It never stores a bearer token or Router envelope. The first Notify send uses the body returned by the outbox checkpoint write (not the pre-write serialization), so PostgreSQL jsonb normalization and replay use identical bytes. Completion is recorded only after Notify accepts. Development/testing uses the same mutation/outbox contract in memory; an async mutation gate makes the setting write and transition enqueue one atomic critical section without blocking an async call under a monitor lock.
The mutation request tries reconciliation immediately and a same-value retry ignores scheduled backoff to resume the existing operation. A production hosted worker, registered after Platform startup migrations, claims due rows across tenants after restart. Ambient authenticated bearer/verified Router-envelope auth is used on the request path; the worker mints a fresh two-minute, tenant-bound service identity envelope from the configured Router:IdentityEnvelopeSigningKey for each downstream request. The persisted original actor remains audit/event metadata while the reconciler service identity is the executing actor. IssuerDirectory keys requests by the stable operation id and replays the original immutable receipt after a lost response. There is no destructive dead-letter transition: a failure releases the lease and keeps the row pending with attempt_count, bounded exponential next_attempt_at (maximum five minutes), and truncated last_error for operator observability; recovery always resumes the same operation. Platform:OperatorProviderChange:ReconciliationIntervalSeconds controls the idle poll interval (default 5, allowed 1-300 seconds).
The IssuerDirectory grace cutoff preserves lifecycle/verification context; Platform does not interpret it as Policy authorization for new exceptions. That authorization rule remains open in OSK-P5.
Scope-catalog gap closed (verified 2026-07-19). Authority migration
S041_platform_console_scope_catalog.sql, the canonicalStellaOpsScopescatalog, and seed-parity tests now coverplatform.setup.read/write/admin,platform.metadata.read,onboarding.read/write,search.read, andops.admin. These Platform policies no longer depend on bypass-network scope satisfaction.
Endpoint surface (route prefixes)
Mapped in Program.cs. Highlights beyond the core /api/v1/platform group:
| Prefix | Endpoint class | Notes |
|---|---|---|
/platform/envsettings.json, /platform/envsettings/db | EnvironmentSettingsEndpoints, EnvironmentSettingsAdminEndpoints | Frontend config (anonymous) + DB-layer admin |
/platform/verification-settings/{service} | VerificationSettingsEndpoints | Region-scoped signature-verification keys for a backend service’s startup config source (shared-token authenticated) |
/api/v1/platform/* | PlatformEndpoints | health, quotas, onboarding, preferences, search, metadata |
/api/v1/search, /api/v1/platform/search | PlatformEndpoints | global search (legacy search path sends Deprecation/Sunset headers) |
/api/v2/context, /api/v2/releases, /api/v2/topology, /api/v2/integrations, /api/v2/evidence | Context + read-model endpoints | Platform-owned aggregation-only read-model projections |
/api/v1/telemetry/federation, /api/telemetry/nis2 | FederationTelemetryEndpoints, Nis2TelemetryEndpoints | federation + NIS2 telemetry |
/api/v1/admin/migrations | (deleted) | RETIRED 2026-08-02 (SPRINT_20260722_026 CM-2) — status is each service’s own db.migration-status doctor check; convergence is startup migrations |
/api/v1/admin/crypto/kek, /api/v1/admin/crypto-providers, /api/v1/admin/crypto/profile | crypto admin endpoints | KEK control plane, provider catalog, profile validate |
/api/v1/platform/connector-credentials, /api/v1/platform/connectors | ConnectorCredentialsEndpoints, ConnectorsCatalogEndpoints | Credential-at-rest store (see Connector credential store below); connector:credentials:read/:write |
MapVerificationKeyLifecycleEndpoints, MapPackAdapterEndpoints, MapActorIdentityEndpoints | verification-key lifecycle, pack adapters, actor identity | mapped in Program.cs; see source for exact prefixes |
/api/v1/setup | SetupEndpoints | first-run setup wizard; the central demo-seed face is retired |
/api/v1/platform/localization | LocalizationEndpoints | tenant-scoped localization |
/api/v1/release-control/bundles | release-control endpoints | Platform-owned read-model projection |
/api/v1/administration/trust-signing, /api/v1/stella-assistant | misc | trust signing, assistant |
/healthz, /readyz, /health, /buildinfo.json | inline | anonymous liveness/readiness + image-staleness self-check |
Relocated stable paths keep their public URLs but are no longer Platform surfaces: RO owns /api/v1/release-orchestrator/environments*, /api/v2/scripts*, and /api/v1/federation/*; /api/v1/score/* is owned by Signals, /api/v1/function-maps/* by Scanner, /api/v1/policy/interop/* by Policy Engine, and /api/v2/security/* plus /api/risk/aggregated-status by Findings.Security. Router configuration is the external ownership source of truth.
PostgreSQL-backed topology reads retain a process-local last-known snapshot and its synchronization watermarks for brief connection-exhaustion recovery. The cache is isolated by tenant, bounded to 2,048 tenant entries, and expires each value after five minutes. It is populated only by a successful inventory read or upsert, preserves an authoritative null snapshot sentinel, and is consulted only when PostgreSQL reports SQLSTATE 53300 (too_many_connections). Missing or expired last-known values and every other storage failure continue to propagate, so the recovery cannot hide schema, authorization, cancellation, or unknown infrastructure failures.
Identity provider ownership boundary
Identity-provider configuration is Authority-owned and process-global. The canonical operator surface is /console/admin/identity-providers, protected by authority:idp.read / authority:idp.write. Platform no longer maps /api/v1/platform/identity-providers, registers its former read/write policy, proxies Authority status, performs LDAP/OIDC/SAML probes, or carries an identity-provider persistence service. The console and CLI must call Authority directly so there is one mutation and runtime-status contract.
Migration 087_v1_platform_identity_provider_configs.sql is intentionally retained byte-for-byte as forward-only history (normalized SHA-256 2a76024dca7c92436b2b6498673059c3e13360957ea1e51191b87685f8251bf2) and as the input to Authority’s one-time, cross-database global-provider importer. Its retirement comment is applied by the additive 089_v1_platform_identity_provider_configs_retirement_comment.sql migration; an already-applied migration is never edited to describe later lifecycle state. Platform has no EF entity, DbSet, store, or endpoint that can write platform.identity_provider_configs; therefore the table is empty on a fresh installation and vestigial after an upgrade/import. Nothing reads it any more: SPRINT_20260722_016 AUTH-4 (X7) deleted Authority’s cross-database importer and the Authority:IdentityProviderImport:* settings, so the table is now frozen and unread on both sides. Physical removal is still deferred until a PostgreSQL snapshot is operator-approved under ADR-004; that drop is its own destructive window.
Connector credential store (ConnectorCredentials/)
The Console backend hosts a credential-at-rest subsystem under StellaOps.Platform.WebService/ConnectorCredentials/, mapped by MapConnectorCredentialsEndpoints() / MapConnectorsCatalogEndpoints() (Program.cs). It stores integration credentials (SCM/CI/registry/secrets connectors) encrypted at rest and is the reason the crypto-provider + KEK control plane exist on Platform. Key components:
PostgresCredentialStore(ICredentialStore) — durable, tenant-scoped credential rows.ConnectorCredentialAead— envelope (AEAD) encryption of secret material, keyed through the KEK control plane; regional crypto substituted viaRegionalCryptoPluginActivation(no cloud-managed KMS default, per the on-prem invariant).ConnectorCredentialReSealStore+ConnectorCredentialExpirySweeper— re-seal on key rotation and expiry-driven cleanup.ConnectorCredentialsChangePublisher— change notifications to consumers (Concelier/Excititor resolve credentials via the/resolveroute underconnector:credentials:read).UnifiedCredentialAuditEmitter(ICredentialAuditEmitter) — audit trail for every mutation.
Authorization: every route checks connector:credentials:read or connector:credentials:write in-handler (ConnectorCredentialsEndpoints.cs); both scopes are in the canonical catalog. This is a security-relevant surface — treat credential read/rotate as privileged and audited.
Shared Storage Driver Contract (Sprint 312)
This contract is the default for all stateful StellaOps webservices unless a module ADR explicitly overrides it.
Storage:Driver- Accepted values:
postgres,inmemory,filesystem. - Production default:
postgres. inmemoryandfilesystemare non-production/testing-only and must be explicitly configured.
- Accepted values:
Storage:ObjectStore:Driver- Accepted values at platform key level:
rustfs,seed-fs. - Module runtime contracts may narrow this set and must fail fast for unsupported values.
- Use only for blob/object payload channels (artifacts, snapshots, package blobs).
- Accepted values at platform key level:
ConnectionStrings:Default- Required when
Storage:Driver=postgresunless a service-specific connection key is provided. - Service-specific key, when present, takes precedence over
ConnectionStrings:Default.
- Required when
Fail-fast policy:
- Non-development runtime must fail startup when required storage configuration is missing (no silent localhost/file fallback).
- Development runtime may use localhost/file defaults only when explicitly intended for local workflows.
Current implementation status (2026-03-05):
PacksRegistry: Postgres metadata/state + seed-fs payload channel for pack/provenance/attestation blobs; startup rejectsrustfsand unknown object-store drivers.TaskRunner: Postgres run state/log/approval + seed-fs artifact payload channel; startup rejectsrustfsand unknown object-store drivers in both WebService and Worker.RiskEngine: Postgres-backed result store (riskengine.risk_score_results) with explicit in-memory test fallback.Replay: Postgres snapshot index + seed-fs snapshot blob store; startup rejectsinmemoryoutsideTesting, rejectsrustfs, and rejects unknown object-store drivers.OpsMemory: connection precedence aligned toConnectionStrings:OpsMemory -> ConnectionStrings:Default, with non-development fail-fast.Platform: Postgres-backed platform-owned state (platform.*,release.*); startup rejects missingPlatform:Storage:PostgresConnectionStringoutsideTesting, and in-memory stores are injected only by explicitTestingharnesses.Platform compatibility harnesses: synthetic/api/console/*,/api/v1/aoc/*,/api/v1/notify/*, and/api/v1/signals/*route groups are mapped only inDevelopmentandTesting; durable/api/v1/authority/quotas/*aliases remain owned byPlatformEndpoints. The synthetic/api/v1/jobengine/quotas*group is retired in every environment.Platform registry search: production requires an explicitly configured registry backend and returns503for missing/failing backends instead of empty fixture success.Platform inventory collection: production does not bindNoOpRemoteCommandExecutor; missing executor integration resolves to a fail-closed Platform executor that reports inventory collection unavailable.Platform QA fixture readback:/api/qa/fixtures/advanced-assurance-goldenis disabled by default and reads only configured local seed artifacts; it validatesseed-integrity.jsonagainstseed-manifest.jsonand never accepts request-supplied filesystem paths.Platform advanced-assurance case summary:/api/assurance/cases/ASSURANCE-GOLDEN-PROD-PAYMENTS-001reuses validated seed artifacts and reportsliveRowsImported=falseso operators can distinguish global fixture readback from module-owned write-through/import.- Shared artifact infrastructure:
AddUnifiedArtifactStorebinds the S3 object store plus PostgreSQL artifact index and registers embedded startup migrations forevidence.artifact_index. Process-local in-memory artifact stores are test-project fixtures only and are not exposed through production shared-library DI. - Shared Evidence Pack library:
AddEvidencePackno longer binds process-local pack storage by default. Callers must register a durableIEvidencePackStore; otherwise the shared library resolves a fail-closed store that throws on use. Tests and explicit local harnesses may still opt intoUseInMemoryEvidencePackStore. - Shared HLC library:
AddHybridLogicalClockwithout an explicit state store binds fail-closedIHlcStateStore; production callers must choose a durable store such as PostgreSQL, while local/test harnesses must callAddHybridLogicalClockInMemoryexplicitly.
Platform Runtime Read-Model Boundary Policy (Point 4 / Sprint 20260305-005)
Platform runtime read-model APIs are aggregation-only and must stay behind explicit query contracts. Runtime read handlers must not take direct dependencies on foreign module persistence internals.
Approved runtime query contracts:
IReleaseControlBundleStore(release/topology/security/integration projections over release-control bundles + runs).IPlatformContextQuery(read-only access to region/environment context inventory).
Prohibited in runtime read-model services:
- Direct constructor dependencies on foreign
StellaOps.*.Persistence*namespaces. - Direct
DbContext,NpgsqlDataSource, or module-specific migration runner dependencies from non-admin read endpoints.
Non-runtime migration allowlist (explicit boundary exception):
Deleted 2026-09-14 (SPRINT_20260722_021 PLT-4, DC-26) together with the CLI recovery path; the allowlist entry is gone.src/Platform/__Libraries/StellaOps.Platform.Persistence/MigrationModulePlugins.csis the shrinking local/DR registry consumed by the explicit CLI recovery path.StellaOps.Platform.Persistencecompiles no foreign module’s migration assembly.
Enforcement:
- Guard tests in
src/Platform/__Tests/StellaOps.Platform.WebService.Tests/PlatformRuntimeBoundaryGuardTests.csfail when constructor contracts drift or foreign persistence references appear outside the allowlist above.
Runtime Dependency Inventory (2026-03-05)
| Component | Dependency category | Classification | Notes |
|---|---|---|---|
ReleaseReadModelService | IReleaseControlBundleStore | Allowed runtime read-model dependency | Release projection reads only via Platform-owned bundle-store contract. |
TopologyReadModelService | IReleaseControlBundleStore, IPlatformContextQuery | Allowed runtime read-model dependency | Topology projection composes release bundles with context inventory through explicit query contracts. |
SecurityReadModelService | IReleaseControlBundleStore, IPlatformContextQuery | Allowed runtime read-model dependency | Security projection remains synthetic/read-only and does not call VEX/exception write stores directly. |
IntegrationsReadModelService | IReleaseControlBundleStore, IPlatformContextQuery | Allowed runtime read-model dependency | Integration freshness projection uses release run metadata and context inventory only. |
PlatformContextService | IPlatformContextStore (InMemory/Postgres) | Allowed runtime dependency (module-local persistence) | Exposes read-only IPlatformContextQuery plus preference write APIs; no foreign module coupling. |
MigrationModulePlugins | Deleted 2026-09-14 (PLT-4, DC-26) | The former non-runtime exception for the CLI recovery path no longer exists; PlatformMigrationModule declares only Platform’s own module. |
Advisory Commitments (2026-02-26 Batch)
SPRINT_20260226_223_Platform_score_explain_contract_and_replay_alignmentdefines deterministic score/explain/replay contract behavior for CLI and Web consumers.SPRINT_20260226_230_Platform_locale_label_translation_correctionscompletes locale label correction baseline for cross-language operator UI consistency.- Cross-module advisory translation tracking is maintained in
docs/product/advisory-translation-20260226.md.
