Platform Service (StellaOps.Platform.WebService)
Purpose
Provide a single, deterministic aggregation layer for cross-service UX workflows (health, quotas, onboarding, preferences, global search) so the Console UI and CLI do not fan out to multiple modules directly.
Non-goals
- Replace module-owned APIs (Authority, Policy, Scanner, Orchestrator, etc.).
- Ingest or mutate raw evidence or policy overlays.
- Store high-volume evidence payloads (SBOMs, VEX, audit bundles).
Responsibilities
- Aggregate platform health and dependency status.
- Aggregate quota usage across Authority, Gateway, Orchestrator, and storage backends.
- Persist onboarding progress and tenant setup milestones.
- Persist dashboard personalization and layout preferences.
- Persist authenticated user language preference for shared Web/CLI locale selection.
- Provide global search aggregation across entities.
- Provide global context selectors (region/environment/time window) and per-user persistence for Pack 22 top-bar context.
- Provide Pack 22 release read-model projections for list/detail/activity/approvals queue views.
- Provide Pack 22 topology inventory read-model projections for regions/environments/targets/hosts/agents/promotion paths/workflows/gate profiles.
- Surface platform metadata for UI bootstrapping (version, build, offline status).
- Expose analytics lake aggregates for SBOM, vulnerability, and attestation reporting.
API surface (v1)
Health aggregation
- GET
/health - GET
/health/liveness - GET
/health/readiness - GET
/healthz - GET
/readyz - GET
/api/v1/platform/health/summary - GET
/api/v1/platform/health/readiness - GET
/api/v1/platform/health/dependencies - GET
/api/v1/platform/health/incidents - GET
/api/v1/platform/health/metrics - Top-level health aliases are anonymous container/orchestrator probes; the authenticated platform readiness contract remains
/api/v1/platform/health/readiness. GET /api/v1/platform/health/readinessis the canonical readiness contract for both setup gating and post-boot diagnostics.- Required setup-blocking dependencies currently include the five converged required setup steps (
database,valkey,migrations,admin-bootstrap,crypto-profile) plus livefrontdoorandauthorityprobes. - Optional post-boot dependencies are discovered from configured
STELLAOPS_*_URLendpoints and currently includerelease-orchestrator,policy-engine,scanner,signals,notify,scheduler,registry-token,sbomservice,packsregistry, andadvisoryai.
Quota aggregation
- GET
/api/v1/platform/quotas/summary - GET
/api/v1/platform/quotas/tenants/{tenantId} - GET
/api/v1/platform/quotas/alerts - POST
/api/v1/platform/quotas/alerts - Legacy
/api/v1/authority/quotas/*compatibility paths are served only fromPlatformEndpoints; Platform no longer maps a second synthetic quota compatibility host. POST /api/v1/authority/quotas/reportsandGET /api/v1/authority/quotas/reports/{reportId}now fail closed until a durable report/export backend exists./api/v1/jobengine/quotasand/api/v1/jobengine/quotas/summarynow return501 Not Implementedinstead of fabricated JobEngine quota payloads.
Onboarding
- GET
/api/v1/platform/onboarding/status - POST
/api/v1/platform/onboarding/complete/{step} - POST
/api/v1/platform/onboarding/skip - GET
/api/v1/platform/tenants/{tenantId}/setup-status
Preferences
- GET
/api/v1/platform/preferences/dashboard - PUT
/api/v1/platform/preferences/dashboard - GET
/api/v1/platform/preferences/language - PUT
/api/v1/platform/preferences/language - GET
/api/v1/platform/dashboard/profiles - GET
/api/v1/platform/dashboard/profiles/{profileId} - POST
/api/v1/platform/dashboard/profiles
Global search
- GET
/api/v1/search(alias to/api/v1/platform/search) - GET
/api/v1/platform/search - Legacy notice: both endpoints now emit deprecation metadata (
Deprecation,Sunset,Link,Warning) and are being replaced by Unified SearchPOST /api/v1/search/query.
Registry image search
- GET
/api/v1/registries/images/search?q=... - GET
/api/v1/registries/images/digests?repository=project/name - GET
/api/v1/registries/images/digests?repository=project/name&digest=sha256:... - Digest entries include
tag, immutabledigest,pushedAt, and OCI label metadata when the registry exposes it through artifact labels, annotations, or manifest config labels. The optionaldigestquery filters the registry artifact list before returning labels for an image-security detail page. Release Orchestrator uses those labels to populate component-versionsourceCommitSha,builtAt, and operator-facing version labels duringsync-versions. - Production requires an explicitly configured registry backend (
STELLAOPS_HARBOR_URLorPlatform:HarborFixtureUrl). When no backend is configured, unreachable, returns non-JSON, or returns a failing status, Platform returns503 Service Unavailableinstead of fabricating an empty success result. - The Harbor fixture default is available only in
DevelopmentandTestingfor deterministic local/test harnesses.
Metadata
- GET
/api/v1/platform/metadata - Response includes a capabilities list for UI bootstrapping; analytics capability is reported only when analytics storage is configured.
Localization
- GET
/platform/i18n/{locale}.json(anonymous, cacheable UI translation bundle) - GET
/api/v1/platform/localization/bundles/{locale} - GET
/api/v1/platform/localization/bundles/{locale}/{namespace} - GET
/api/v1/platform/localization/locales(catalog used by Web and CLI locale selectors) - PUT
/api/v1/platform/localization/bundles - DELETE
/api/v1/platform/localization/strings/{locale}/{key} - Backend locale resolution contract:
X-Locale->Accept-Language-> default locale. - Runtime bundle layering consumed by backend services: shared embedded
common-> service embedded bundle -> Platform override bundle. - Platform ships locale-complete
uiandplatformnamespace bundles foren-US,de-DE,bg-BG,ru-RU,es-ES,fr-FR,uk-UA,zh-TW,zh-CN; shared localization library now providescommonbundles for the same locale set. - Bundled locales currently shipped:
en-US,de-DE,bg-BG,ru-RU,es-ES,fr-FR,uk-UA,zh-TW,zh-CN.
Release Orchestrator compatibility
- Platform hosts
/api/v1/release-orchestrator/environments/*as a compatibility facade for Console release-management flows. - Supported families include environment CRUD plus
/settings, per-environment/targetsCRUD with/health-check, and/freeze-windowsCRUD. - The compatibility facade no longer uses production in-memory or in-process PostgreSQL environment stores. Platform always proxies these mutations to the owning ReleaseOrchestrator WebApi over tenant/auth-aware HTTP, including when both services use the same physical PostgreSQL installation.
- Platform hosts
/api/v2/scripts*against the real Release Orchestrator scripts backend on both runtime branches: direct library/schema binding when Platform has the scripts PostgreSQL connection, and an HTTP proxy to the owning Release Orchestrator WebApi when it does not. - The scripts facade no longer falls back to a local in-memory catalog; list/count/detail/version/validation/compatibility flows all resolve against the owning Release Orchestrator service or schema.
- Platform can mount the ReleaseOrchestrator federation controller at
/api/v1/federation/*whenSTELLAOPS_FEATURE_FEDERATION=true. The gate defaults tofalse; enabled single-region installs bind in-process federation registry/messaging defaults and logfederation.single_region_mode.
Notification admin routing
- Platform no longer serves synthetic
/api/v1/notify/*admin compatibility payloads for quiet-hours, throttles, escalation, localization, or digest schedule management. - Core notify toolkit flows remain on
/api/v1/notify/*through the owning Notify surface. - Advanced notification admin flows are owned by the Notifier frontdoor
/api/v1/notifier/*, which maps onto the service-local/api/v2/notify/*runtime. - Digest schedule CRUD remains unsupported in the live runtime; the Web console must present that surface as unavailable rather than fabricate records.
Synthetic compatibility harnesses
- Production does not map Platform-owned synthetic compatibility route groups for
/api/console/*,/api/v1/aoc/*,/api/v1/notify/*,/api/v1/signals/*, or/api/v1/jobengine/quotas*. - The durable Platform quota aliases under
/api/v1/authority/quotas/*remain served byPlatformEndpoints; production does not expose the separate synthetic quota compatibility host. - These route groups remain available only in
DevelopmentandTestingto support deterministic compatibility harnesses and legacy UI tests. - Production callers must use the owning service APIs or receive a normal
404 Not Foundwhen targeting the removed synthetic Platform route surface.
QA fixture readback
- GET
/api/qa/fixtures/advanced-assurance-golden - This setup-admin gated route is disabled by default. Enable it with
Platform:QaFixtures:Enabled=trueorSTELLAOPS_QA_FIXTURES_ENABLED=true. - The route reads only the configured local seed artifact directory from
Platform:QaFixtures:AdvancedAssuranceGoldenSeedDirectoryorSTELLAOPS_ADVANCED_ASSURANCE_GOLDEN_SEED_DIR. Callers cannot pass filesystem paths in the request. - Required artifacts are
seed-manifest.jsonandseed-integrity.json;seed-command-output.txtis reported when present. - Platform validates the seed manifest digest recorded by
seed-integrity.jsonbefore returning readback data. Missing artifacts return503; digest drift returns409. - The response proves deterministic local seed artifact integrity and stable IDs. It does not claim downstream module databases contain equivalent live rows or that any ASSURE scenario has passed.
Advanced assurance case summary
- GET
/api/assurance/cases/ASSURANCE-GOLDEN-PROD-PAYMENTS-001 - This operator-readable route uses the same QA fixture enablement and seed directory settings as
GET /api/qa/fixtures/advanced-assurance-golden. - Platform validates the seed manifest and integrity ledger before returning the seeded environment, primary service, fixture advisory, vulnerable package, release hotfix fields, and section readiness.
- The response reports
status=fixture-backed-blocked-live-importandsource.liveRowsImported=falseuntil module-owned write-through/import exists. Requests for other case IDs return404.
Advanced assurance scenario transactions
- GET/POST
/api/assurance/cases/ASSURANCE-GOLDEN-PROD-PAYMENTS-001/scenarios/{scenarioId}/{*transactionPath} - This operator-readable route resolves ASSURE-001 through ASSURE-022 scenario transaction paths from the golden fixture contract after the same seed manifest and integrity validation as the case summary route.
- Responses include deterministic case id, scenario id, transaction id, transaction path, stable required refs, evidence refs, seed manifest digest, and explicit no-overclaim flags.
- Platform remains aggregation-only:
source.liveRowsImported=false,noOverclaim.moduleWriteThrough=false, and legal compliance/regulator/live-external-verification claims are false until owning modules provide live evidence. - Negative/tamper-style transaction paths, including
api-03, return HTTP 422 withverdict=failed_closedand a fail-closed evidence ref so reruns cannot treat a blocked scenario as a warning-only success.
API surface (v2)
Global context
- GET
/api/v2/context/regions - GET
/api/v2/context/environments?regions= - GET
/api/v2/context/preferences - PUT
/api/v2/context/preferences - PUT
/api/v2/context/regions/{regionId} - PUT
/api/v2/context/environments/{environmentId}
The context region/environment PUT routes are setup-admin convergence APIs for operator-managed installs. They let setup scripts and local lab preparation add real deployment regions and environments without direct SQL.
PUT /api/v2/context/regions/{regionId} accepts:
{
"displayName": "Local Lab",
"sortOrder": 5,
"enabled": true
}
PUT /api/v2/context/environments/{environmentId} accepts:
{
"regionId": "local-lab",
"environmentType": "development",
"displayName": "Customer TEST Lab",
"sortOrder": 6,
"enabled": true
}
Both routes require platform.context.write. Region IDs and environment IDs are normalized to lower-case route IDs. Environment upsert requires the referenced region to already exist. Validation failures use deterministic error codes such as region_id_required, region_not_found, environment_id_required, environment_type_required, and environment_type_invalid.
Releases read model
- GET
/api/v2/releases - GET
/api/v2/releases/{releaseId} - GET
/api/v2/releases/activity - GET
/api/v2/releases/approvals
Topology inventory read model
- GET
/api/v2/topology/regions - GET
/api/v2/topology/environments - GET
/api/v2/topology/targets - GET
/api/v2/topology/hosts - GET
/api/v2/topology/agents - GET
/api/v2/topology/promotion-paths - GET
/api/v2/topology/workflows - GET
/api/v2/topology/layout - GET
/api/v2/topology/gate-profiles /api/v2/topology/layoutrequires theplatform.topology.readpolicy (orch:read) and returns a tenant-scoped positioned graph. A synchronized or fresh tenant with no topology returns HTTP200with emptynodes/edges, zero counts, and a40 x 40canvas; populated topology returns positioned region/environment nodes and routed promotion edges./api/v2/topology/hostsincludesProbeStatus,ProbeType, andProbeLastHeartbeat.- Current host probe contract is projection-derived rather than live Signals state:
ProbeLastHeartbeatmirrors the latest projected host sync timestamp,ProbeStatusisactivewhen that heartbeat is within two minutes of the freshest host heartbeat in the tenant snapshot andofflineotherwise, and missing heartbeat data yieldsnot_installed. - Probe type mapping is deterministic:
winrm_host->etw;docker_host,compose_host,ssh_host,ecs_service, andnomad_job->ebpf. - Topology projections prefer explicit component-version metadata when present. The following metadata keys shape the read model:
topologyTargetId,topologyTargetName,topologyHostId,topologyHostName,topologyAgentId,topologyAgentName, andtargetType. Additional metadata such as image digest, SCM URL/commit/branch, release ID, and external dependencies is retained as release/version evidence. For that customer’s TEST environment, Oracle is represented as an external dependency metadata node (db.app.example.com:1521/<oracle-sid>) and not as a deployed container target. - Topology reads are bound to the materialized Platform release-control run. The read model uses the version ID recorded on the latest materialization run, so a later unpublished bundle version cannot accidentally reshape topology before it is materialized.
Security read model
- GET
/api/v2/security/findings - GET
/api/v2/security/image-context?digest=sha256:... - GET
/api/v2/security/image-related-releases?digest=sha256:... - GET
/api/v2/security/disposition - GET
/api/v2/security/disposition/{findingId} - GET
/api/v2/security/sbom-explorer /api/v2/security/findingshonors optionaldigestand routes to a digest-scoped store path. Withoutdigest, it returns the tenant-wide security findings projection.- These security routes are owned by
StellaOps.Findings.Security.WebService, not Platform.region,environment,artifact, anddigestform one normalized, store-predicated scope shared with/api/risk/aggregated-status; see Findings.Security exposure read model. - The findings response retains the legacy envelope and adds normalized
scope, projection-derived nullableasOf, deterministicpagination(cursor/nextCursor/hasMore), and canonicalpivotBuckets[].drillDownmetadata.total, facets, and pivots are computed before pagination. /api/v2/security/image-contextresolves a digest to SBOM registry metadata (vuln.sbom_registry) so the UI can show the image reference, SBOM format/version, component counts, and rescan affordance without fabricating metadata./api/v2/security/image-related-releasesreadsrelease_orchestrator.release_componentsjoined torelease_orchestrator.releasesby tenant and digest. The response is bounded to the newest 100 rows and returns only releases that actually pin the requested immutable digest.- GET
/api/risk/aggregated-status(SPRINT_20260522_001) — aggregated risk surface for the Security dashboard “Top Assets at Risk” lens (security-top-risk-lens.component.ts->RISK_API.getAggregatedStatus). Owning service: Findings.Security (not Platform or policy-engine): the gateway routes this exact path to Findings.Security ahead of the general^/api/(risk|risk-budget)-> policy-engine rule, because the only real, queryable risk-bearing data is the Findings.Security projection. Requiresfindings:read(Findings.SecuritySecurityReadpolicy) and tenant context (400tenant_missingwhen absent); echoesX-StellaOps-TraceIdintotraceId.- Aggregate is DERIVED from real findings (
AggregatedRiskStatusServicereadsISecurityFindingProjectionStore->findings_security.security_finding_projection-> thevuln.sbom_canonical_match⋈vuln.advisory_canonicaljoin). No synthetic / hash-derived rows: with no findings the response is well-formed-but-empty (all-zerobySeverity/byCategory, emptytopRisks,overallScore0,stabletrend). - Response shape (FE
AggregatedRiskStatusinrisk.models.ts, camelCase):tenantId,computedAt,bySeverity(allRiskSeveritykeys),byCategory(allRiskCategorykeys; every finding is a CVE-derivedvulnerability),topRisks[](RiskProfileranked score-desc — findings grouped by package = the at-risk asset;id/title= package name so the lens routes?risk=<id>),recentTransitions([]— no per-finding transition-event source in Platform; honest empty),overallScore(0-100),trend,traceId, plus sharedscope, nullableasOf, andtotal. - Per-asset / overall score is a deterministic squash of real per-finding severity weights (critical 10, high 5, medium 2, low 1, info 0.2) with a ×1.5 reachability multiplier.
- Trend (TASK-3, SPRINT_20260522_001): the
trendblock is derived from periodic aggregate snapshots persisted infindings_security.security_risk_snapshot(per-severity counts +overall_scoreper(tenant_id, captured_at); Findings.Security embedded baseline migration001, auto-applied on startup). On each read the current aggregate is recorded as a snapshot (ISecurityRiskSnapshotStore, deduped to ~1/hour/tenant), thenComputeTrend(...)compares the currentoverallScoreagainst the most-recent snapshot beforenow - 24h: higher =>worsening(+changePercent), lower =>improving(-changePercent), |delta| < 0.5 =>stable. Honeststable/0 when no baseline before the window exists. Forward-fill only — snapshot history accrues from the first call onward (no backfill of past aggregates), so a real (non-stable) live trend appears after >24h of accrual.
- Aggregate is DERIVED from real findings (
Integrations read model
- GET
/api/v2/integrations/feeds - GET
/api/v2/integrations/vex-sources
Analytics (SBOM lake)
- GET
/api/analytics/suppliers - GET
/api/analytics/licenses - GET
/api/analytics/vulnerabilities - GET
/api/analytics/backlog - GET
/api/analytics/attestation-coverage - GET
/api/analytics/trends/vulnerabilities - GET
/api/analytics/trends/components
NIS2 telemetry effectiveness
- GET
/api/telemetry/nis2/effectiveness?tenantId=<tenantId>&window=<rolling-30d|rolling-90d> - Returns the
nis2-effectiveness-dashboard-v1contract for the tenant-scoped Console dashboard. - Requires
analytics.readand the resolved Platform tenant context. WhentenantIdis provided in the query string it must match the resolved header/context tenant; mismatches return a deterministictenant_conflict400 response. windowdefaults torolling-90dwhen omitted. Supported values arerolling-30dandrolling-90d; unsupported values return a deterministicinvalid_window400 response with the allowed set.- Response areas are always ordered
1..13and are backed byINis2AreaMetricsService.GetAreaReportAsync. Target-document and control-register snapshot hashes are nullable until the downstream N1 snapshot integrations are available. - Platform wires live threshold alerts with
AddNis2EffectivenessNotifyThresholdEmitter. Whennotify:queueis configured, live NIS2 KPI crossings enqueuenis2.effectiveness.threshold_breachedevents with the Notify queue contract; Testing and explicit development local harnesses can use the in-process emitter for deterministic endpoint coverage. - Platform fails closed for threshold alert emission outside those local harnesses when the Notify queue is absent. The dashboard response no longer reports a
notify-threshold-alerts-missingblocker; monthly signed effectiveness export remains tracked separately under ExportCenter.
Crypto provider catalog admin (SPRINT_20260503_008)
- GET
/api/v1/admin/crypto-providers/health(probes the catalog-registered providers) - GET
/api/v1/admin/crypto-providers/preferences/ PUT / DELETE (per-tenant preference CRUD, scopecrypto:adminorops.admin) - GET
/api/v1/admin/crypto-providers/compliance-overrides?tenantId=...(effective per-tenant NIS2/DORA/CRA override state; scopecrypto:profile:adminorops.admin) - PUT
/api/v1/admin/crypto-providers/compliance-overrides/{nis2|dora|cra}(sets one explicit tenant override; emitsplatform.update_compliance_pack) - DELETE
/api/v1/admin/crypto-providers/compliance-overrides/{nis2|dora|cra}(clears one tenant override so resolution falls back to installation defaults; emitsplatform.update_compliance_pack) - Compliance override responses include
workflowReadiness(disabled,configuration_required, orevidence_pending) andreadinessGaps. An enabled NIS2 pack explicitly remainsevidence_pendingafter setup prerequisites are present until live NIS2 effectiveness and reachability evidence arrives from the upstream workflows; this API is readiness state, not a compliance proof. - GET
/api/v1/admin/crypto-providers/catalog(list all catalog rows) - GET
/api/v1/admin/crypto-providers/catalog/{providerId}(get one entry) - POST
/api/v1/admin/crypto-providers/catalog(register a new well-known provider; emitsPlatform.CreateCryptoCatalogEntryaudit) - PUT
/api/v1/admin/crypto-providers/catalog/{providerId}(partial update; emitsPlatform.UpdateCryptoCatalogEntry) - DELETE
/api/v1/admin/crypto-providers/catalog/{providerId}(refused with 409 when an active tenant preference references the row; emitsPlatform.DeleteCryptoCatalogEntry) - All catalog endpoints require the
crypto:admin(orops.admin) scope and are tenant-scoped. - Catalog rows are seeded once from
Configuration/crypto-provider-catalog.seed.yaml(embedded resource); subsequent boots leave the table alone. Operators add a new well-known provider byPOST-ing a catalog row and deploying the matching code-shippedICryptoProviderplug-in.
Legacy alias compatibility (/api/v1/*)
- GET
/api/v1/context/regions(alias of/api/v2/context/regions) - GET
/api/v1/releases(alias of/api/v2/releases) - GET
/api/v1/topology/regions(alias of/api/v2/topology/regions) - GET
/api/v1/security/findings(alias of/api/v2/security/findings) - GET
/api/v1/integrations/feeds(alias of/api/v2/integrations/feeds) - GET
/api/v1/integrations/vex-sources(alias of/api/v2/integrations/vex-sources) - Alias usage telemetry is emitted as deterministic event keys (
alias_<method>_<route_pattern>) with tenant hash metadata only.
Data model
platform.dashboard_preferences(dashboard layout, widgets, filters, optional userlocalepreference key)platform.dashboard_profiles(saved profiles per tenant)platform.onboarding_state(step state, timestamps, actor)platform.quota_alerts(per-tenant quota alert thresholds)platform.search_history(optional, user-scoped, append-only)platform.translations(tenant + locale scoped translation override store)platform.context_regions(global region selector inventory)platform.context_environments(global environment selector inventory with region linkage)platform.ui_context_preferences(tenant + actor scoped region/environment/time-window selections)release.release_read_model(Pack 22 release list/detail projection root)release.release_activity_projection(cross-release timeline projection with run/approval correlation keys)release.release_approvals_projection(cross-release approval queue projection with blocker summaries)release.security_finding_projection(Pack 22 consolidated findings projection with pivot/filter fields)release.security_risk_snapshot(SPRINT_20260522_001 TASK-3: hourly aggregate risk snapshots — per-severity counts + overall_score per (tenant, captured_at) — backing the Security dashboard trend block)release.security_disposition_projection(read-only join projection for VEX + exception disposition state)release.security_sbom_component_projection(component-level SBOM explorer table projection)release.security_sbom_graph_projection(edge-level SBOM graph projection used by graph and diff modes)release.integration_feed_source_health(advisory feed source health/freshness projection)release.integration_vex_source_health(VEX source health/freshness projection with statement-format metadata)release.integration_source_sync_watermarks(source family synchronization watermark projection state)release.topology_region_inventory(region-level topology projection with deterministic ordering counts)release.topology_environment_inventory(environment-level topology projection with region linkage and aggregate counters)release.topology_target_inventory(target/component deployment inventory projection)release.topology_host_inventory(host runtime inventory projection linked to targets and agents)release.topology_agent_inventory(agent fleet projection with capability and assignment summaries)release.topology_promotion_path_inventory(region-aware promotion-path projection with workflow and gate links)release.topology_workflow_inventory(workflow template projection for topology routes)release.topology_gate_profile_inventory(gate profile projection bound to region/environment inventory)release.topology_sync_watermarks(projection synchronization watermark state for deterministic replay/cutover checks)platform.crypto_provider_catalog(DB-driven catalog of well-known crypto providers; seeded from YAML on first boot, mutable via the catalog admin endpoints; SPRINT_20260503_008)platform.tenant_crypto_preferences(per-tenant active provider selection and algorithm scope priority)platform.tenant_compliance_profile(single per-tenant compliance profile plus nullable NIS2/DORA/CRA pack override columns;NULLinherits installation defaults)- Schema reference:
docs/db/schemas/platform.sql(PostgreSQL; the live host owns only durable stores, whileTestingharnesses inject any required in-memory stores explicitly).
Startup migration convergence
- Platform applies the embedded
StellaOps.Platform.Database.Migrations.Platforminstallation/control-plane stream with migration bookkeeping inplatform.schema_migrations; a fresh database converges without manualpsqlor init-script ordering assumptions. - The pre-1.0
Migrations/Releasebaseline remains immutable forward-only history but is excluded from the Platform assembly. The Platform stream retains its installation state and compatibility projections while deliberately omitting authoritativerelease.environments,release.targets,release.regions,release.agents, infrastructure bindings, and deletion lifecycle tables. - ReleaseOrchestrator is the only startup migration owner for deployment topology and image-placement state. Platform’s environment compatibility routes call that service and its topology reader is read-only.
- Analytics materialized views that are refreshed with
REFRESH MATERIALIZED VIEW CONCURRENTLYmust have column-only unique indexes. Expression-based unique indexes are not valid refresh identity for PostgreSQL concurrent refresh and are replaced by follow-up migrations before analytics maintenance runs.
Dependencies
- Authority (tenant/user identity, quotas, RBAC)
- Gateway (rate-limit status and request telemetry)
- Orchestrator (job quotas, SLO state)
- Notifier (alert policies and delivery status)
- Policy/Scanner/Registry/VexHub (search aggregation sources)
Runtime boundary policy
- Runtime read-model services (
/api/v2/releases,/api/v2/topology/*,/api/v2/security/*,/api/v2/integrations/*) must depend only on explicit query contracts: IReleaseControlBundleStoreIPlatformContextQuery- Current host probe enrichment is derived only from the topology projection timestamps already returned by
IReleaseControlBundleStore. - Inventory collection no longer binds the Release Orchestrator
NoOpRemoteCommandExecutorin production. When no production executor backend exists, Platform resolves a fail-closed executor that records inventory collection as unavailable instead of pretending a no-op backend is a live integration. - Future live runtime probe or observed inventory enrichment for
/api/v2/topology/hostsmust still arrive through an explicit query contract. Read-model services must not reach into foreign persistence directly. - Foreign module persistence references are migration/admin-only and limited to explicit allowlist surfaces (
SeedEndpoints,MigrationModulePlugins). - Runtime read endpoints must not inject foreign
*.Persistence*types,DbContextfrom other modules, or migration runners directly. - Guard tests:
src/Platform/__Tests/StellaOps.Platform.WebService.Tests/PlatformRuntimeBoundaryGuardTests.cs.
Security and scopes
- Authorization is applied to each child route. The common
/api/v1/platformgroup enforces tenant context only and must not add a module-wide scope requirement; this keeps unrelated capabilities independently least-privileged. - Health:
ops.health(summary),ops.admin(metrics) - Quotas:
quota.read(summary),quota.admin(alerts/config) - Onboarding:
onboarding.read,onboarding.write - Preferences:
ui.preferences.read,ui.preferences.write - Context:
platform.context.read,platform.context.write - Releases read model:
orch:read(platform.releasecontrol.readpolicy mapping in Platform service) - Topology read model:
orch:read(platform.topology.readpolicy mapping in Platform service) - Security read model:
findings:read(platform.security.readpolicy mapping in Platform service) - Integrations feed read model:
advisory:read(platform.integrations.readpolicy mapping in Platform service) - Integrations VEX source read model:
vex:read(platform.integrations.vex.readpolicy mapping in Platform service) - Search:
search.readplus downstream service scopes (findings:read,policy:read, etc.) - Metadata:
platform.metadata.read - Analytics:
analytics.read - Federation status:
platform:federation:read - Federation operations:
platform:federation:write
Determinism and offline posture
- Stable ordering with explicit sort keys and deterministic tiebreakers.
- All timestamps in UTC ISO-8601.
- Cache last-known snapshots for offline rendering with “data as of” markers.
Analytics ingestion configuration
Analytics ingestion runs inside the Platform WebService and subscribes to Scanner, Concelier, and Attestor streams. Configure ingestion with Platform:AnalyticsIngestion:
Platform:
AnalyticsIngestion:
Enabled: true
PostgresConnectionString: "" # optional; defaults to Platform:Storage
AllowedTenants: ["tenant-a"]
Streams:
ScannerStream: "orchestrator:events"
ConcelierObservationStream: "concelier:advisory.observation.updated:v1"
ConcelierLinksetStream: "concelier:advisory.linkset.updated:v1"
AttestorStream: "attestor:events"
StartFromBeginning: false
Cas:
RootPath: "/var/lib/stellaops/cas"
DefaultBucket: "attestations"
Attestations:
BundleUriTemplate: "bundle:{digest}"
BundleUriTemplate supports {digest} and {hash} placeholders. The bundle: scheme maps to cas://<DefaultBucket>/{digest} by default. Verify offline bundles with stella bundle verify before ingestion.
Analytics maintenance configuration
Analytics rollups + materialized view refreshes are driven by PlatformAnalyticsMaintenanceService when analytics storage is configured.
Use BackfillDays to recompute recent rollups on the first maintenance run (set to 0 to disable).
Platform:
Storage:
PostgresConnectionString: "Host=...;Database=...;Username=...;Password=..."
AnalyticsMaintenance:
Enabled: true
RunOnStartup: true
IntervalMinutes: 1440
ComputeDailyRollups: true
RefreshMaterializedViews: true
BackfillDays: 7
Observability
- Metrics:
platform.aggregate.latency_ms,platform.aggregate.errors_total,platform.aggregate.cache_hits_total - Logs include
traceId,tenantId,operation, and cache-hit indicators.
Gateway exposure
The Platform Service is exposed via Gateway and registered through Router discovery. It does not expose direct ingress outside Gateway in production.
Setup Wizard
The Platform Service owns the installation-scoped setup wizard used by /setup-wizard/wizard for first-run control-plane bootstrap and later reconfiguration checks.
Current runtime behavior:
- Authoritative wizard state is persisted in
platform.setup_sessionsvia migration063_PlatformSetupSessions.sql. - Installation-scoped environment settings and the
SetupCompletemarker now converge throughplatform.environment_settingskeyed only bykey. Migration064_EnvironmentSettingsInstallationScopeConvergence.sqlupgrades older compose-created tables that still used the legacy(tenant_id, key)primary key. - The persisted session document keeps only non-sensitive
draftValuesplus step state, timestamps, and check results. - Sensitive step inputs retained for resume/apply are stored separately in
platform.setup_session_secretsvia migration066_PlatformSetupSessionSecrets.sql. Session reads expose onlysecretDraftsmetadata (key,stepId,updatedAtUtc), never plaintext. - Probe/apply hydrate retained setup secrets server-side, and finalize deletes the retained secret records for the completed session.
- Setup session reads now also include a required-only readiness snapshot so CLI and UI status flows can show operational blockers without treating optional post-boot services as setup failures.
- Setup secret protection key precedence is
Platform:Setup:SecretProtectionKey,STELLAOPS_SECRETS_ENCRYPTION_KEY, thenSTELLAOPS_BOOTSTRAP_KEY. - The live wizard now owns the five required control-plane steps the running control plane can truthfully validate and converge plus one optional advisory bootstrap step:
database,valkey,migrations,admin,crypto, andsources. - The
cryptostep is shown as Cryptography and persists regional crypto plus compliance-evidence defaults intoplatform.environment_settings. Canonical keys and soft-probe behavior are documented in Platform Cryptography and Compliance Defaults. - The optional
sourcesstep defaults to StellaOps Mirror, supports manual advisory/VEX source selection, and preserves truthful skip semantics so a fresh install can clearly show advisories as off until the operator enables them. - The
sourcesstep now performs a real reachability check before it can pass. Mirror/source connectivity failures stay on the setup step instead of being downgraded to a background warning. - Certificate or hostname validation failures are surfaced directly. Local browser automation may ignore local certs, but product advisory aggregation still requires a hostname-valid certificate.
- Tenant onboarding remains outside the bootstrap wizard. Integrations, feeds, notifications, environments, agents, branding, and related repeatable operations continue on
/setup/*and module-owned authenticated APIs. probeis diagnostic only. Onlyapplycan move a step into the converged state.
API surface (v1)
Sessions
GET /api/v1/setup/sessions- Get the current installation-scoped setup sessionGET /api/v1/setup/sessions/current- Alias for the current installation-scoped sessionGET /api/v1/setup/sessions/{sessionId}- Read a specific persisted session by IDPOST /api/v1/setup/sessions- Create a new session or force a restartPOST /api/v1/setup/sessions/resume- Resume the current session or create onePUT /api/v1/setup/sessions/{sessionId}/config- Persist non-sensitive draft valuesPOST /api/v1/setup/sessions/{sessionId}/finalize- Finalize the current session with convergence checksPOST /api/v1/setup/sessions/finalize- Compatibility finalize path
Session payloads distinguish:
draftValues- non-sensitive persisted config onlysecretDrafts- retained-secret metadata only; no plaintext secret valuesreadiness- required-only operational readiness summary for installation bootstrap
Steps
POST /api/v1/setup/sessions/{sessionId}/steps/{stepId}/probe- Run a diagnostic probe without completing the stepPOST /api/v1/setup/sessions/{sessionId}/steps/{stepId}/apply- Apply the current step and persist the new statePOST /api/v1/setup/sessions/{sessionId}/steps/{stepId}/execute- Compatibility mutation wrapper (dryRun=truemaps to probe)POST /api/v1/setup/sessions/{sessionId}/steps/{stepId}/reset- Reset a step to pendingPOST /api/v1/setup/sessions/{sessionId}/steps/{stepId}/skip- Compatibility endpoint retained for older clients; the current control-plane steps are all requiredGET /api/v1/setup/sessions/{sessionId}/steps/{stepId}/checks- Read current check resultsPOST /api/v1/setup/sessions/{sessionId}/steps/{stepId}/checks/run- Re-run checks for a stepPOST /api/v1/setup/sessions/{sessionId}/steps/{stepId}/prerequisites- Evaluate prerequisites for a stepPOST /api/v1/setup/steps/{stepId}/test-connection- Compatibility probe endpoint used by older clients
Definitions
GET /api/v1/setup/definitions/steps- List the current live step definitions
Setup step identifiers
| Step ID | Title | Required | Depends On | Notes |
|---|---|---|---|---|
database | PostgreSQL Database | Yes | - | Probe verifies reachability. Apply records convergence against the current runtime connection. |
valkey | Valkey / Redis Setup | Yes | - | Probe verifies cache reachability. Apply records convergence against the current runtime connection. |
migrations | Database Migrations | Yes | database | Probe reports pending migration state. Apply runs the migration-admin path against the canonical Platform and ReleaseOrchestrator registry modules and re-validates convergence. |
admin | Admin Bootstrap | Yes | migrations | Probe validates bootstrap prerequisites. Apply ensures the bootstrap admin exists. |
crypto | Cryptography | Yes | admin | Probe validates the requested regional crypto profile and emits soft warnings for unmet NIS2/DORA/CRA prerequisites. Apply records the canonical Crypto:* and Compliance:* installation defaults. |
sources | Advisory & VEX Sources | No | admin | Probe validates mirror or manual source selections. Apply persists source enablement through Concelier and schedules initial aggregation for newly enabled sources. |
Legacy aliases accepted during the compatibility window:
authority -> adminusers -> admincryptography -> crypto
Former tenant-onboarding steps such as vault, scm, registry, notify, environments, agents, telemetry, llm, and settingsstore are no longer valid setup targets. Platform returns explicit handoff guidance to the authenticated onboarding surfaces instead.
Setup session states
| Status | Description |
|---|---|
NotStarted | Setup not begun |
InProgress | Setup in progress |
Completed | All steps completed |
CompletedPartial | Required steps completed, optional skipped |
Failed | Required step failed |
Abandoned | Setup abandoned by user |
Setup step states
| Status | Description |
|---|---|
Pending | Not yet started |
Passed | Completed successfully |
Failed | Validation failed |
Skipped | Explicitly skipped (optional steps only) |
Current | Reserved contract value for active-step projections |
Blocked | Reserved contract value for dependency failures |
Anonymous posture
- Before installation setup is marked complete, setup session APIs may resolve to the installation scope without an authenticated caller.
- After setup is marked complete, anonymous setup session reads and mutations return
401and the normal authenticatedplatform.setup.*policies apply. - After setup is marked complete, an authenticated operator may still create a new installation-scoped setup session for reconfiguration on the same
/api/v1/setup/sessionscontract; the expected browser flow is anonymous401followed by authenticated session creation after sign-in. Finalizesucceeds only after every required control-plane step has converged and the required readiness dependencies are not blocked.
Security and scopes
- Health:
ops.health(summary, readiness, dependencies, incidents),ops.admin(metrics) - Read:
platform.setup.read - Write:
platform.setup.write - Admin:
platform.setup.admin
Offline posture
- Sessions include
DataAsOfUtcfor offline rendering with stale indicators - Step results cached with Doctor check pass/fail status
- Suggested fixes generated for failed checks
Related documentation
- UX flow specification:
docs/setup/setup-wizard-ux.md - CLI guide:
docs/modules/cli/guides/setup-guide.md - Local operator runbook:
docs/INSTALL_GUIDE.md - Live proof:
src/Web/StellaOps.Web/scripts/live-setup-wizard-state-truth-check.mjs
