VexHub Architecture

Scope. Architecture and operational contract for VexHub, the Stella Ops VEX aggregation service that normalizes, validates, and distributes VEX statements with deterministic, offline-friendly outputs. This dossier is for service implementers, operators configuring VEX sources, and integrators consuming the distribution API.

Last source verification: 2026-07-25 (active migrations, source-trust repository, Excititor projection and operator-authoring paths, and VexLens curation routes).

Related: VexLens (trust scoring / consensus over VexHub statements) · Excititor / Concelier (upstream connectors and AOC-checked claim projection, §9) · Policy Engine (consumes consensus).

1) Purpose

VexHub collects VEX statements from multiple upstream sources, validates and normalizes them, detects conflicts, and exposes a distribution API for internal services and external tools (Trivy/Grype). It is the canonical aggregation layer that feeds VexLens trust scoring and Policy Engine decisioning.

2) Responsibilities

Non-goals: policy decisioning (Policy Engine), consensus computation (VexLens), raw ingestion guardrails (Excititor AOC).

2.1) Registry and Plugin Boundary

VexHub is a host-owned VEX validation, normalization, conflict, and distribution service. Its registry-shaped inputs are data/config by default, not executable plugins:

OpenVEX/CSAF/CycloneDX validation and normalization remain host-owned unless a future sprint creates a VexHub-owned signed normalizer interface. Optional source connector code belongs to Concelier/Excititor overlays because those services own upstream fetching and AOC-checked claim projection. The base VexHub image must not include optional connector payloads, and importing VEX data or source metadata must not execute code.

3) Component Model

Status note. The VexHub WebService, Core pipelines, Persistence layer, distribution API, and the admin conflict-resolution endpoint are implemented and wired. The standalone background ingestion worker described in earlier drafts does not exist as a deployed component.

4) Data Model

Storage-model decision (2026-07-01): the deliberate vex.claims (Excititor, all claims) / vexhub.statements (this schema, enforced/worker-projected only) double-store is documented and recommended KEEP — see storage-model-decision.md for the full blast-radius map of both consumers, the enforced-vs-advisory asymmetry, measured footprint, revisit triggers, and the projection-parity guard.

The PostgreSQL schema is vexhub (constant VexHubDataSource.DefaultSchemaName). Embedded migrations are applied on startup (AddStartupMigrations): 001_v1_vexhub_baseline.sql is the squashed pre-1.0 baseline, 002_source_trust_curation.sql establishes operator-owned source trust, and 003_statement_source_document_lookup.sql adds the projection-repair lookup index. Historical incrementals remain under Migrations/_archived/pre_1.0/; the active top-level migration directory is the ledger authority. VexHub statements are global, not tenant-scopedVexHubDataSource documents that VEX statements and provenance are shared across all tenants; there is no tenant_id column on any VexHub table.

Tables (actual columns after the active embedded migration ledger converges):

Additional schema objects: updated_at columns are maintained by vexhub.update_updated_at triggers; a monitoring view vexhub.statistics aggregates counts. Historical full-text/trigram statement-search storage was removed because VexHub repositories use equality/range predicates and no supported API route issues FTS/trigram predicates. There is no export_cursor table — bulk export reads statements directly at request time.

UTC timestamps and stable ordering keys apply; there are no per-tenant ordering keys because the schema is global.

5) API Surface

All endpoints are under the /api/v1/vex group. Read endpoints require the vexhub:read policy; the conflict-resolution endpoint requires the vexhub:admin policy (see §7). Routes are defined in VexHubEndpointExtensions.MapVexHubEndpoints.

One gateway route in this namespace is intentionally owned elsewhere: POST /api/v1/vex/statements/simple is translated to Excititor POST /excititor/statements/simple before the generic VexHub route matches. It requires the deployment tenant header plus vex.admin, and authors through Excititor’s tenant-scoped raw-document and claim pipeline before projecting the result into this service’s global statement store. VexHub does not expose a parallel statement-create endpoint.

Read group (vexhub:read):

Admin group (vexhub:admin):

Health/ops endpoints (anonymous, outside the auth group): GET /health, GET /health/liveness, GET /health/readiness (all return { Status, Service }), the OpenAPI document (MapOpenApi), and a build-info endpoint (MapBuildInfoEndpoint).

Repository queries order results deterministically; UTC timestamps and content digests (content_digest) provide stable identity and dedup.

GET /api/v1/vex/stats returns the dashboard contract consumed by the console VEX surfaces:

The stats endpoint must keep working on fresh installs even when a committed EF compiled-model stub is empty; runtime model fallback is required until a real optimized model is generated. The service must also auto-apply embedded SQL migrations for schema vexhub on startup so wiped volumes converge without manual SQL bootstrap. The squashed baseline carries forward the historical removal of pg_trgm and full-text statement-search storage; 004_drop_unused_statement_search_vector.sql is archived pre-1.0 history, not an active top-level migration. Product, vulnerability, digest, source, status, verification, ingestion-time, alias, and uniqueness indexes remain because repository queries and measured plans still depend on equality/range access paths. The WebService exposes anonymous /health, /health/liveness, and /health/readiness probes for the hardened runtime image healthcheck; VEX data readiness remains enforced by repository and ingestion endpoints.

Console VEX Runtime Contract

6) Determinism & Offline Posture

7) Security & Auth

7.1) Export Contract

7.2) Configuration

All options bind from the VexHub configuration section (VexHubOptions.SectionName = "VexHub"); PostgreSQL binds from the Postgres section, and Router from Router. Defaults reflect VexHubOptions:

Webhook subscriptions are modeled (WebhookSubscription, event types statement.created/updated/flagged, conflict.detected/resolved, source.polled/failed) and a vexhub.webhook_subscriptions table exists, but webhook delivery is gated by Distribution:EnableWebhooks and tied to the ingestion/polling path that is currently inactive on the WebService host.

8) Observability

9) Integration Points

10) Testing Strategy

11) Unified Source Catalog (Sprint 20260503-012)

The advisory and VEX provider catalogs are projected through a single read endpoint so CLI/UI consumers do not have to special-case “advisory” vs. “VEX” surfaces.

Last updated: 2026-05-30 (doc↔code reconciliation against src/VexHub/ and the Excititor→VexHub projection sink in src/Concelier/StellaOps.Excititor.Worker/VexHub/).