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, exposes federation/NIS2 telemetry surfaces, and hosts the operator-facing database-migration registry.

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. Platform compatibility environment routes always proxy the owning ReleaseOrchestrator API.

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), and src/Platform/__Libraries/StellaOps.Platform.Database/Migrations/Platform/ (the embedded Platform-owned migration stream). Migrations/Release/ is immutable mixed pre-1.0 history and is not embedded or selected by Platform startup. Verified 2026-07-11.

Anchors

Scope

Coordination

Platform docs are the starting point for new contributors; keep this summary in sync with module-specific dossiers and sprint references.

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

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:

  1. Environment variablesSTELLAOPS_*_URL values folded into ApiBaseUrls by StellaOpsEnvVarPostConfigure (an IPostConfigureOptions<PlatformServiceOptions>).
  2. YAML/JSON config — standard IOptions binding of Platform:EnvironmentSettings (and ../etc/platform.yaml / platform.yaml).
  3. Database overridesplatform.environment_settings (key/value), overlaid last via IEnvironmentSettingsStore. Keys follow ApiBaseUrls:{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:

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 a reserved Verification: namespace, grouped by the active regional crypto profile (world/fips/gost/sm/kcmvp/eidas, see ComplianceProfiles):

The platform STORES + SERVES these admin-entered values; it never generates keypairs. Rows are written through the same audited admin API (PUT /platform/envsettings/db/{key}). A single-region install may omit the <profile> segment (Verification:PacksRegistry:PublicKeyPem); resolution prefers the region-scoped row and falls back to the unscoped form.

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 the flat, service-local config keys each validator reads (PacksRegistry:Verification:PublicKeyPem, VexHub:SignatureTrustRoots:<fp>, …) and serves them at GET /platform/verification-settings/{service}?profile=<active> (VerificationSettingsEndpoints). That endpoint is never anonymous: a service pulling at boot authenticates with the shared internal HMAC secret (Router:IdentityEnvelopeSigningKey) in the X-Stella-Verification-Token header, falling back to the platform.setup.read scope. Validator-wave services consume it via PlatformVerificationSettingsConfigurationSource (in StellaOps.Hosting.RuntimeConfiguration), added to builder.Configuration before their verification options bind — one HTTP GET at startup, fail-soft (if the platform is unreachable the source contributes nothing and the existing fail-loud guard correctly aborts). Key changes require a service restart to take effect (config is read once at boot; the platform’s Valkey invalidation only refreshes the platform’s own cache).

Tenant directory (shared.tenants)

Platform owns the shared.tenants directory table (migration 000_shared_tenants_bootstrap.sql):

ColumnNotes
id UUIDCanonical tenant UUID (slug claims resolve to this via IPlatformTenantResolver).
tenant_id TEXT UNIQUETenant slug (e.g. default).
is_default BOOLEANOptional 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, metadataLifecycle/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

Database migration registry and host

StellaOps.Platform.Database is the canonical migration-module registry for the whole platform. MigrationModulePlugins.cs declares one IMigrationModulePlugin per registered schema owner, with Platform itself mapped to migration bookkeeping schema platform and resource prefix StellaOps.Platform.Database.Migrations.Platform. MigrationModuleRegistry discovers these via reflection.

Two consumption paths:

Authorization scopes

Platform maps OAuth scopes to named authorization policies in Program.cs via PlatformPoliciesPlatformScopes (Constants/). ops.admin is a global escape hatch for the crypto/KEK policies. Selected mappings:

PolicyRequired scope(s)Surface
HealthRead / HealthAdminops.health / ops.admin/api/v1/platform/health/*
QuotaRead / QuotaAdminquota.read|orch:quota / quota.admin|orch:quotaquotas + legacy quota compatibility
OnboardingRead / OnboardingWriteonboarding.read / onboarding.write/onboarding/*
PreferencesRead / PreferencesWriteui.preferences.read / ui.preferences.write/preferences/*, dashboard profiles
ContextRead / ContextWriteplatform.context.read / platform.context.write/api/v2/context/*
SearchRead / MetadataReadsearch.read / platform.metadata.readglobal search, metadata
AnalyticsReadanalytics.read/api/analytics/*, NIS2 telemetry
SetupRead / SetupWrite / SetupAdminplatform.setup.read / .write / .adminsetup wizard, env-settings DB layer, migration admin, seed
FederationRead / FederationManageplatform:federation:read / platform:federation:writefederated telemetry
SubjectAccessRead / SubjectAccessEraseplatform:sar:read / platform:sar:eraseGDPR subject-access
ActorIdentityRead (any-of)ui.read or platform:sar:readconsole actor-identity badge resolver (GET /api/v1/platform/actor-identity/{ref}) — any console user resolves the redaction-aware projection (i5 #17)
CryptoProviderRead / CryptoProviderAdmin / CryptoProfileAdmincrypto:read / crypto:admin / crypto:profile:admin (or ops.admin)crypto provider + compliance profile admin
OperatorSigningEnrollmentReadauthority:signing-keys.enroll (or crypto:read / ops.admin)narrow tenant compliance-profile projection for operator public-key enrollment
CryptoKekRead / CryptoKekRotatecrypto:kek:read / crypto:kek:rotate (or ops.admin)KEK control plane
TrustRead/Write/Admin, Script*, ReleaseControl*see PlatformScopes.cstrust 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 canonical StellaOpsScopes catalog, and seed-parity tests now cover platform.setup.read/write/admin, platform.metadata.read, onboarding.read/write, search.read, and ops.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:

PrefixEndpoint classNotes
/platform/envsettings.json, /platform/envsettings/dbEnvironmentSettingsEndpoints, EnvironmentSettingsAdminEndpointsFrontend config (anonymous) + DB-layer admin
/platform/verification-settings/{service}VerificationSettingsEndpointsRegion-scoped signature-verification keys for a backend service’s startup config source (shared-token authenticated)
/api/v1/platform/*PlatformEndpointshealth, quotas, onboarding, preferences, search, metadata
/api/v1/search, /api/v1/platform/searchPlatformEndpointsglobal search (legacy search path sends Deprecation/Sunset headers)
/api/v2/context, /api/v2/releases, /api/v2/topology, /api/v2/integrations, /api/v2/evidenceContext + read-model endpointsPlatform-owned aggregation-only read-model projections
/api/v1/telemetry/federation, /api/telemetry/nis2FederationTelemetryEndpoints, Nis2TelemetryEndpointsfederation + NIS2 telemetry
/api/v1/admin/migrationsMigrationAdminEndpointscross-module migration status/verify/run
/api/v1/admin/crypto/kek, /api/v1/admin/crypto-providers, /api/v1/admin/crypto/profilecrypto admin endpointsKEK control plane, provider catalog, profile validate
/api/v1/platform/connector-credentials, /api/v1/platform/connectorsConnectorCredentialsEndpoints, ConnectorsCatalogEndpointsCredential-at-rest store (see Connector credential store below); connector:credentials:read/:write
MapVerificationKeyLifecycleEndpoints, MapPackAdapterEndpoints, MapActorIdentityEndpoints, MapEvidenceThreadEndpointsverification-key lifecycle, pack adapters, actor identity, evidence threadsmapped in Program.cs; see source for exact prefixes
/api/v1/setup, /api/v1/admin (seed)SetupEndpoints, SeedEndpointsfirst-run setup wizard, demo seed
/api/v1/platform/localizationLocalizationEndpointstenant-scoped localization
/api/v1/release-control/bundles, /api/v1/release-orchestrator/environmentsrelease-control + environmentsread-model + RO environment proxy
/api/v1/administration/trust-signing, /api/v2/scripts, /api/v1/stella-assistantmisctrust signing, scripts, assistant
/healthz, /readyz, /health, /buildinfo.jsoninlineanonymous liveness/readiness + image-staleness self-check

Relocated stable paths keep their public URLs but are no longer Platform surfaces: /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. Authority reads it through Authority:IdentityProviderImport:PlatformConnectionString. Physical removal is deferred until import completion and a PostgreSQL snapshot are operator-approved under ADR-004; this sprint does not drop the table.

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:

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.

Fail-fast policy:

Current implementation status (2026-03-05):

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:

Prohibited in runtime read-model services:

Migration/admin allowlist (explicit boundary exceptions):

Enforcement:

Runtime Dependency Inventory (2026-03-05)

ComponentDependency categoryClassificationNotes
ReleaseReadModelServiceIReleaseControlBundleStoreAllowed runtime read-model dependencyRelease projection reads only via Platform-owned bundle-store contract.
TopologyReadModelServiceIReleaseControlBundleStore, IPlatformContextQueryAllowed runtime read-model dependencyTopology projection composes release bundles with context inventory through explicit query contracts.
SecurityReadModelServiceIReleaseControlBundleStore, IPlatformContextQueryAllowed runtime read-model dependencySecurity projection remains synthetic/read-only and does not call VEX/exception write stores directly.
IntegrationsReadModelServiceIReleaseControlBundleStore, IPlatformContextQueryAllowed runtime read-model dependencyIntegration freshness projection uses release run metadata and context inventory only.
PlatformContextServiceIPlatformContextStore (InMemory/Postgres)Allowed runtime dependency (module-local persistence)Exposes read-only IPlatformContextQuery plus preference write APIs; no foreign module coupling.
SeedEndpointsForeign StellaOps.*.Persistence* migration assembliesMigration/admin-only dependencyAllowed exception for demo seed execution only (platform.setup.admin).
MigrationModulePluginsForeign module migration assembliesMigration-only dependencyAllowed exception for schema migration orchestration, not part of runtime read endpoint execution path.

Advisory Commitments (2026-02-26 Batch)