SBOM Service Architecture

Verified against commit 942a18727c (2026-09-26). Re-verify with: grep -rnE 'Map(Get|Post|Put|Delete|Group)\(' src/SbomService/StellaOps.SbomService --include=*.cs && ls src/SbomService/__Libraries/StellaOps.SbomService.Persistence/Migrations/*.sql && grep -n 'CheckId =>' src/SbomService/StellaOps.SbomService/Doctor/SbomServiceDoctorChecks.cs

Scope: canonical SBOM storage, projections, ledger history, lineage, registry-source state, the sbom.versions owner stream, and durable SBOM event backlogs. Consumers: Console (Web), the CLI, Findings (version stream and raw documents), OfflineKit audit bundles, and AdvisoryAI (SBOM context).

Authenticated tenant boundary. SBOM Service accepts a validated Authority bearer token or a Router-generated signed identity envelope. Tenant identity comes only from the canonical stellaops:tenant claim. Legacy tenant/user headers cannot authenticate a request. Existing tenant/tenantId request fields are optional compatibility assertions: an omitted value uses the authenticated tenant, an equal value is accepted, and a mismatch is rejected before repository access.

1) Mission and boundaries

Repository-qualified Composer PURLs survive CycloneDX/SPDX graph extraction, durable graph storage and exact-PURL queries without losing their qualifiers (PCD-332). Official Drupal, explicitly different and unqualified package identities remain separate; tenant fencing still applies. Native PostgreSQL round-trip controls cover both document formats.

PCD-332 source verified against 1e52c13050 (2026-10-06). Re-verify: pwsh ./tools/scripts/test-targeted-xunit.ps1 -Project src/SbomService/__Tests/StellaOps.SbomService.Persistence.Tests/StellaOps.SbomService.Persistence.Tests.csproj -Method "*RepositoryIdentity*" -BuildProjectReferences -MaxThreads 1.

2) Runtime layout

Upload memory

The upload service borrows the request’s parsed JSON element; base64 uploads decode and parse once. Validation, normalization, digest generation and dependency extraction use that same element. Clean documents are checked for the mandatory JSON NUL escape in their borrowed UTF-8 bytes before the decoded JSON-Pointer walk runs. The walk still decides candidate matches, so a literal backslash followed by u0000 remains valid.

The legacy ledger digest remains SHA-256 of the existing compact JSON serialization, streamed through the central incremental hash implementation. It is distinct from the exact first-writer sidecar digest. Sidecar persistence sends one owned UTF-8 byte buffer as a PostgreSQL binary parameter, converting it to the existing TEXT column inside PostgreSQL. Whitespace, property order, UTF-8 content, first-writer deduplication and tenant access checks remain unchanged. Ledger persistence also serializes provenance, normalized components and replay inputs directly to UTF-8 binary parameters, converted to the existing JSONB columns in PostgreSQL. Ledger reads use sequential access and deserialize Npgsql’s owned JSON document directly, removing the intermediate UTF-16 JSON string. The same reader handles every tenant-scoped and artifact query; pagination consumes its total count after the version columns to preserve sequential order. No schema migration accompanies these allocation changes. The family deployment and offline bundle budget 768 MiB as a capacity complement. At 512 MiB the optimized upload/read candidate accepted the first real Debian/Ubuntu pair, but the next request failed in the existing Router bridge’s full-payload ToArray before application binding. That run sampled 398.754 MiB current memory and 353.148 MiB working set, with a managed allocation failure, 0 restarts and OOMKilled=false. The bridge still buffers its envelope and body; those costs are outside the service-only allocation counter. The increased budget does not replace the allocation controls or the back-to-back deployed-limit forcing function. A clean d11e6889ad image accepted three consecutive Debian/Ubuntu pairs (6.96/8.21 MB) at 768 MiB on 2026-10-13. The 75 samples peaked at 482.227 MiB current memory and 473.211 MiB working set; anonymous 401, NUL 400, zero OOM and zero restarts also passed. Verification covers six uploads; sustained concurrency requires separate qualification.

Sidecar reads also retain UTF-8: PostgreSQL supplies the TEXT as UTF-8 bytes through a sequential reader, raw export responses send those bytes directly, and dependency backfill parses the same buffer. Production-answer availability reads only sidecar headers through a tenant-derived access grant; it never fetches a multi-megabyte document merely to report an export link.

Allocation checks are opt-in (STELLAOPS_PERFORMANCE_TESTS=1) in SbomUploadAllocationTests: inline and base64 8.26 MB uploads, two concurrent uploads, and an operator-provided document (STELLAOPS_SBOM_ALLOCATION_FIXTURE). These isolate upload-service allocations from request binding and database/network costs; deployment acceptance additionally requires back-to-back real documents at the configured container limit. SbomDocumentSidecarStorageTests additionally bounds two real PostgreSQL reads and two HTTP exports of an 8.21 MB document, including byte-equivalent output. These catch read-side copies that the upload tests’ synchronous repository substitutes cannot measure. It also bounds two real ledger reads with 8.34 MB of normalized metadata and verifies retained metadata and tenant isolation. Model strings remain necessary owned data; the JSON transport representation is not materialized as a second full UTF-16 string.

2.0a Schema convergence has exactly one owner

The runtime defensive-DDL mechanism is retired (SPRINT_20260823_003 SBT-6, closing W3-07 D-W307-2). Sixteen repository/service types used to run CREATE TABLE IF NOT EXISTS on first touch — 82 call sites creating 22 distinct tables with no ledger bookkeeping — a fourth migration mechanism alongside this service’s startup migrator, the central migrator and the CLI, predating the §2.7 auto-migration contract. It is gone. The embedded startup migrations in StellaOps.SbomService.Persistence are the only thing that creates or alters this schema, and SbomServiceDefensiveDdlRetirementTests fails if a repository grows an EnsureTable*Async again.

2.1 Durable state

2.2 Truthful unsupported surface

2.3 Tenant-scoped derived read models

sbom.catalog and sbom.component_lookups were created installation-wide — no tenant column — so every authenticated caller could read every row. Migration 006_catalog_component_lookup_tenant_id.sql adds the durable tenant key (SPRINT_20260712_007, SBOM-READMODEL-007-001):

3) Primary APIs

All routes below are minimal-API endpoints mapped in Program.cs, Endpoints/SbomVersionStreamEndpoints.cs, or Services/RegistrySourceEndpointExtensions.cs; the host has no MVC controllers. Endpoints are gated by the Sbom.Read, Sbom.Write, Sbom.Attest, Sbom.Internal, or Sbom.DoctorHealth named policies (constants in Auth/SbomPolicies.cs); see Section 8a for the auth model.

3.1 Upload, versions, and ledger

3.2 Lineage graph (host minimal-API)

Lineage is minimal-API only. The dead MVC LineageController/LineageStreamController that formerly declared duplicate routes under api/v1/lineage (and consumed the unwired StellaOps.SbomService.Lineage runtime services) were removed in BL-3 (SPRINT_20260713_002). The endpoints above are the sole api/v1/lineage surface; there is no route-prefix collision to reconcile. The stream/optimize/levels/metadata/cache operations the old LineageStreamController declared were never mapped and had no live equivalent.

3.3 Change-trace compatibility

3.4 Artifact associations

3.5 Console / component lookup / entrypoints

3.6 Registry sources and webhooks

3.7 Health, build info, and Doctor

4) Internal APIs

All internal endpoints require Sbom.Internal / sbom:operate. Event, asset, inventory, resolver, ledger-audit, analysis, backfill, and retention APIs are installation-wide because their current repository contracts are installation-wide. /internal/orchestrator/* is tenant-scoped and always binds storage access to the authenticated tenant.

5) Ingestion and ledger behavior

6) Events and derived state

7) Registry source management

8) Determinism and offline posture

8a) Authentication and authorization model

9) Configuration

9a) Observability

10) Proof status

11) Open gaps