OpenAPI Aggregation
This document describes how the StellaOps Gateway aggregates OpenAPI documentation from connected microservices into per-service and merged specifications.
Overview
The Gateway uses fan-out aggregation for OpenAPI. It enumerates HELLO-registered services from router state, fetches each service’s own OpenAPI document over the internal .stella-ops.local network, and then serves both a per-service envelope and a merged OpenAPI 3.1 document. This provides:
- Unified API documentation: All services documented in one place
- Dynamic updates: Documents refresh from live service specs on a 30-second cache TTL
- Standard compliance: OpenAPI 3.1.0 with native JSON Schema draft 2020-12 support
- Multiple formats: Available as JSON or YAML
- Efficient caching: single-flight refresh plus ETag-based client caching for the merged legacy document
OAPI-059-001 Per-Service OpenAPI Audit (2026-05-18)
This audit records the per-service OpenAPI state after OAPI-059-001. It is intentionally limited to endpoint publication and does not redesign the gateway aggregation flow.
The Program path column names the live source. Hosts that were later folded, merged or withdrawn are struck through, name the live host that carries their surface, and give the frozen path under src/__Obsoleted/ (re-verified against src/ on 2026-09-25, DEF-2).
| Service host | Program path | Emits OpenAPI after this pass? | Path(s) | OAPI-059-001 result |
|---|---|---|---|---|
| AdvisoryAI.WebService | src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Program.cs | Yes | /openapi/v1.json | Removed Environment.IsDevelopment() gate. |
src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Program.cs (live advisoryai-web); the standalone host is frozen under src/__Obsoleted/AdvisoryAI/StellaOps.OpsMemory.WebService/ | Yes, through advisoryai-web | /openapi/v1.json on advisoryai-web | Folded 2026-09-07 (AAI-10). The AdvisoryAI host maps the OpsMemory endpoints from StellaOps.OpsMemory.Application. The OAPI-059-001 change here (default AddOpenApi/MapOpenApi, un-gated Swagger) was on the retired host. | |
src/Evidence/StellaOps.Evidence.WebService/Program.cs (live evidence-web); the standalone host is frozen under src/__Obsoleted/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/ | Yes, through evidence-web | /openapi/v1.json on evidence-web | Folded 2026-09-04 (EVD-10a). evidence-web composes AttestorWebServiceComposition from src/Evidence/__Libraries/StellaOps.Attestor.Application/ and registers with the router as evidence, not attestor. | |
| Signer.WebService | src/Attestor/StellaOps.Signer/StellaOps.Signer.WebService/Program.cs | Yes | /openapi/v1.json | Added default AddOpenApi/MapOpenApi; no new package was required. |
| Authority | src/Authority/StellaOps.Authority/StellaOps.Authority/Program.cs | Yes | /.well-known/openapi | Existing custom static-document provider; unchanged in this pass. |
src/Authority/StellaOps.Authority/StellaOps.Authority/Program.cs (live authority); the standalone host is frozen under src/__Obsoleted/Authority/StellaOps.IssuerDirectory.WebService/ | Yes, through authority | Authority’s /.well-known/openapi and /openapi/v1.json | Folded 2026-09-11 (AUTH-10). Authority maps the issuer-directory endpoints from src/Authority/__Libraries/StellaOps.IssuerDirectory.Api/; the microservice name issuer-directory is not carried. | |
src/__Obsoleted/Concelier/StellaOps.Concelier.WebService/Program.cs | No live host | None | Retired 2026-09-11 under VULN-G6; Vulnerabilities publishes the successor document. | |
src/Vulnerabilities/StellaOps.Vulnerabilities.WebService/Program.cs (live Vulnerabilities hub); the standalone host is frozen under src/__Obsoleted/Concelier/StellaOps.Excititor.WebService/ | No live host of its own | /openapi/v1.json on the Vulnerabilities host | Retired 2026-09-11 under VULN-B1. The Vulnerabilities hub owns VEX ingestion and publication; /openapi/excititor.json is no longer served. | |
src/__Obsoleted/Doctor/StellaOps.Doctor.WebService/Program.cs | No live host | None | Deployable retired by DOC-5 stage 4 (2026-08-17); source withdrawn by PCD-73 (2026-09-27). | |
src/Evidence/StellaOps.Evidence.WebService/Program.cs (live evidence-web); the standalone host is frozen under src/__Obsoleted/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.WebService/ | Yes, through evidence-web | /openapi/v1.json on evidence-web | Folded 2026-09-04 (EVD-10a). evidence-web maps the EvidenceLocker endpoint groups under /api/evidence/v1 from src/Evidence/__Libraries/StellaOps.EvidenceLocker.Application/. | |
src/OfflineKit/StellaOps.OfflineKit.WebService/Program.cs (live offlinekit-web); the standalone host is frozen under src/__Obsoleted/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/ | Yes, through offlinekit-web | /openapi/v1.json on offlinekit-web | Withdrawn 2026-09-12 (OK-10). offlinekit-web composes the ExportCenter domain from src/OfflineKit/__Libraries/. The retired host’s /.well-known/openapi and /openapi/export-center.* discovery endpoints are not carried. | |
src/Findings/StellaOps.Findings.WebService/Program.cs (live findings-web); the standalone host is frozen under src/__Obsoleted/Findings/StellaOps.Findings.Ledger.WebService/ | Yes, through findings-web | /openapi/v1.json on findings-web | Frozen 2026-08-28 (FND-20). findings-web carries 61 of the retired host’s 99 routes; the other 36 were withdrawn. | |
| — deleted 2026-08-27 (SPRINT_20260722_010 FND-10) | n/a | n/a | The host is retired, so the aggregate no longer advertises /riskengine/*. Before FND-10 it published /riskengine/risk-scores/* as though reachable while no gateway route existed — that inconsistency is now resolved by deletion. | |
| Integrations.WebService | src/Integrations/StellaOps.Integrations.WebService/Program.cs | Yes | /openapi/v1.json, /swagger/v1/swagger.json, /swagger | Added default AddOpenApi/MapOpenApi; un-gated existing Swagger middleware. |
src/JobEngine/StellaOps.JobEngine.WebService/PacksRegistryComposition.cs (live jobengine host); the standalone host is frozen under src/__Obsoleted/JobEngine/StellaOps.PacksRegistry/ | Yes, through jobengine | /openapi/v1.json on jobengine | Merged into JobEngine (JOB-9). The OAPI-059-001 change here (removed Environment.IsDevelopment() gate) was on the retired host. | |
src/JobEngine/StellaOps.JobEngine.WebService/Program.cs (live jobengine host); the standalone host is frozen under src/__Obsoleted/JobEngine/StellaOps.Scheduler.WebService/ | Yes, through jobengine | /openapi/v1.json on jobengine | Frozen 2026-09-12 (JOB-9). Its Program.cs became the JobEngine composition root; the router registers the service as jobengine, not scheduler. | |
src/__Obsoleted/Notifier/StellaOps.Notifier/StellaOps.Notifier.WebService/Program.cs | No web target | None | Retired. notifier-web merged into notify-web at NTF-5 and the whole tree was frozen at NTF-10 (2026-09-08); it is in no solution and has no matrix row. | |
| Notify.WebService | src/Notify/StellaOps.Notify.WebService/Program.cs | Yes | /openapi/v1.json | Removed the local no-op OpenApiExtensions.WithOpenApi shim and stale .WithOpenApi() group calls that collided with or errored against the framework extension; added the central Microsoft.AspNetCore.OpenApi package plus default AddOpenApi/MapOpenApi; local restore/build passed. |
| Platform.WebService | src/Platform/StellaOps.Platform.WebService/Program.cs | Yes | /openapi/v1.json | Removed Environment.IsDevelopment() gate; Platform conformance skip removed. |
| (host deleted 2026-09-08, 023 GRA-10) | n/a | n/a | Added default AddOpenApi/MapOpenApi and un-gated the Swagger middleware in this pass; the host was retired into graph-api, whose own /openapi/v1.json row is Graph.Api above. | |
src/__Obsoleted/Remediation/StellaOps.Remediation.WebService/Program.cs | No live host | None | Frozen 2026-08-19 (W3-14). Console remediation surfaces are AdvisoryAI’s in-module autofix; the marketplace design has no successor host. | |
| Replay.WebService | src/Replay/StellaOps.Replay.WebService/Program.cs | Yes | /.well-known/openapi | Existing custom provider; unchanged in this pass. |
| SbomService | src/SbomService/StellaOps.SbomService/Program.cs | Yes | /openapi/v1.json | Added the central Microsoft.AspNetCore.OpenApi package and default AddOpenApi/MapOpenApi; local no-dependency build passed. |
| Scanner.WebService | src/Scanner/StellaOps.Scanner.WebService/Program.cs | Yes | /openapi/v1.json when Microsoft.AspNetCore.OpenApi is present | Existing MapOpenApiIfAvailable reflection helper; unchanged in this pass. |
| Tester.WebService | src/Tester/StellaOps.Tester.WebService/Program.cs | No | None | Explicitly forbidden for this worker; left for Tester-owned sprint. |
| Timeline.WebService | src/Timeline/StellaOps.Timeline.WebService/Program.cs | Yes | /openapi/v1.json, /swagger/v1/swagger.json, /swagger | Active matrix target timeline-web; includes the merged timeline-indexer endpoints and worker responsibilities. |
| TimelineIndexer.WebService | archived | Merged into timeline-web | None as an active matrix target | The host project was archived to src/__Obsoleted/Timeline/StellaOps.TimelineIndexer.WebService/ on 2026-08-10 and no longer builds; services-matrix.env never had an active timeline-indexer-web/-worker key. Rebuild and live proof use timeline-web. |
| Unknowns.WebService | src/Unknowns/StellaOps.Unknowns.WebService/Program.cs | Yes | /openapi/v1.json, /swagger/v1/swagger.json, /swagger | Added default AddOpenApi/MapOpenApi; un-gated existing Swagger middleware. |
(tree deleted by a8472b6e10) | No live host | None | Successor is the Vulnerabilities hub, src/Vulnerabilities/StellaOps.Vulnerabilities.WebService/Program.cs, which serves /openapi/v1.json. | |
(tree deleted by a8472b6e10) | No live host | None | Successor is the Vulnerabilities hub, src/Vulnerabilities/StellaOps.Vulnerabilities.WebService/Program.cs, which serves /openapi/v1.json. | |
| Workflow.WebService | src/Workflow/StellaOps.Workflow.WebService/Program.cs | Yes | /openapi/v1.json | Added explicit Microsoft.AspNetCore.OpenApi 10.0.1 because this project disables central package management; local no-dependency build passed. |
src/__Obsoleted/__Libraries/StellaOps.Verdict.WebService/Program.cs | No live host | None | Never activated; withdrawn by PCD-73 (2026-09-27, owner ruling). The Verdict library and CLI stay; there is no Verdict HTTP API. | |
src/__Obsoleted/AirGap/StellaOps.AirGap.Controller/Program.cs | No live host | None | Source retired 2026-09-10 (OK-5). Platform’s environment-state API owns custody; src/Platform/StellaOps.Platform.WebService/Program.cs serves /openapi/v1.json. | |
src/__Obsoleted/AirGap/StellaOps.AirGap.Time/Program.cs | No live host | None | Source retired 2026-09-10 (OK-5). RFC 3161 verification lives in src/OfflineKit/__Libraries/StellaOps.AirGap.Time.Verification/, a library with no HTTP host of its own. |
OpenAPI wiring added 2026-05-20 (Sprint 059 finisher) to the four previously-unwired active hosts so the fan-out reaches them: Graph.Api, Registry.TokenService, ReleaseOrchestrator.WebApi, and Signals now register AddEndpointsApiExplorer()+AddOpenApi() and expose app.MapOpenApi().AllowAnonymous() at /openapi/v1.json (mirroring the policy-engine pattern). Microsoft.AspNetCore.OpenApi was added to the Graph and Signals csproj; registry-token and release-orchestrator already referenced it.
Additional HTTP hosts observed but not changed in this WebService-scoped pass: SmRemote.Service and Gateway.WebService (Policy.Gateway was merged into policy-engine and withdrawn by PCD-73). Publish-helper alignment on 2026-05-18 added missing active matrix keys for attestor, doctor-web, packsregistry-web, replay-web, symbols, vexlens-web, and airgap-time; it intentionally did not add inactive or merged keys such as timeline-indexer-web, notifier-web, remediation-web, or verdict-web.
Runtime probe note (2026-05-18): the local compose stack was running, but OAPI-059-001’s runtime acceptance criterion is not yet met. Direct host probes were used because the checked container image did not expose curl, wget, or python3; docker exec stellaops-platform ... only found dotnet. Probes returned 200 with OpenAPI 3.1.1 for Platform, 401 for EvidenceLocker/ExportCenter/ReachGraph/BinaryIndex, 400 for Notify, and 404 for the other edited services that are currently deployed in the stack. These results reflect the running containers, not a rebuilt deployment containing this worker’s new package/wiring edits.
Runtime probe note (2026-05-19 follow-up): GET https://stella-ops.local/api/openapi/aggregate still returns 34 services with ok=7, missing=24, and error=3; Scanner remains missing. Direct host-mapped probes confirm valid specs for Platform (http://127.1.0.3/openapi/v1.json, 273 paths), EvidenceLocker (http://127.1.0.7/openapi/v1.json, 35 paths), PacksRegistry (http://127.1.0.34/openapi/v1.json, 16 paths), Authority (http://127.1.0.4/.well-known/openapi, 6 paths), Concelier (http://127.1.0.9/.well-known/openapi, 175 paths), and Excititor (http://127.1.0.10/openapi/excititor.json, 13 paths). Scanner is healthy and built from current HEAD, and /app/Microsoft.AspNetCore.OpenApi.dll is present in the container, but http://127.1.0.8/openapi/v1.json and common alternate paths return 404. Treat Scanner as a route-registration/runtime blocker before claiming merged-spec coverage.
Runtime proof (2026-05-20, Sprint 059 finisher — RESOLVED): after wiring + rebuilding the four unwired hosts (graph, signals, registry-token, release-orchestrator) and recreating their containers, GET https://stella-ops.local/api/openapi/aggregate returns 35 services with ok=33, error=1, missing=1 = 94.3% (up from 82.9%). All four newly-wired hosts serve OpenAPI 3.1.1 at /openapi/v1.json (verified service-to-service). The merged GET /openapi.json (OpenAPI 3.1.0) contains Authority, Platform, EvidenceLocker, and Concelier paths. Residual non-ok: findings-ledger (error, DNS-alias; Findings hard-constraint area) and scanner (missing, route-registration, by-design) — both out-of-scope for this worker; the ≥95% target is reached automatically once either residual is fixed by its owner.
Fan-Out Aggregation (OAPI-059-002)
StellaOps.Gateway.WebService owns the runtime fan-out implementation:
OpenApiAggregatorenumerates distinctInstanceDescriptor.ServiceNamevalues fromIGlobalRoutingState.GetAllConnections().- Each service is probed with a 5-second per-service timeout. The default path is
/openapi/v1.json. Authority prefers that generated surface and falls back to its curated/.well-known/openapidocument. Provider-specific candidates are defined byOpenApiAggregator.ResolveOpenApiPaths. - Results are cached for 30 seconds and refreshed with a
SemaphoreSlimsingle-flight gate so concurrent operator requests do not fan out repeatedly. GET /api/openapi/aggregatereturns one entry per service withstatus,path,spec,fetchedAtUtc, anddurationMs.GET /openapi.jsonremains the legacy merged document. Itspathsobject is built from everystatus: okupstream spec, projected through the gateway route table where a route mount exists. Schema references are service-prefixed during merge to avoid collisions.GET /openapi.yamlserializes the same merged document as YAML.
Route projection uses the destination service key. The configured authority-upstream.stella-ops.local hostname identifies Authority, so its /console operations also appear under the served /authority/console mount (PCD-352). This explicit hostname alias does not assign another service’s paths to Authority or infer service identity from an arbitrary -upstream suffix. Verified against 378e73656a (2026-10-05). Reverify with pwsh tools/scripts/test-targeted-xunit.ps1 -Project src/Router/__Tests/StellaOps.Gateway.WebService.Tests/StellaOps.Gateway.WebService.Tests.csproj -Method '*AuthorityUpstreamAlias*' -BuildProjectReferences -Restore.
CLI/owner compatibility verified against f261640997 on 2026-10-04 (UTC). The CLI consumes these owner contracts through api spec list|download and advisoryai sources prepare. Listing reads the per-service envelope and merged JSON metadata; service downloads extract a named status: ok specification. YAML serialization preserves JSON strings, numbers, booleans and nulls as YAML scalar values. The owner endpoint and aggregator are compiled into the CLI’s loopback contract tests, which exercise the real client and registered commands. The API command guide records routes, cache rules and the rerun command. This proof establishes local source compatibility; deployed estate coverage must be measured from its returned service statuses.
Status values:
| Status | Meaning |
|---|---|
ok | A valid OpenAPI document was fetched and parsed. |
missing | The service responded but no candidate path returned a valid OpenAPI document. |
timeout | The service did not complete within the per-service timeout. |
error | The fan-out HTTP request failed before a usable response was received. |
Regeneration Entrypoint
OAPI-059-004 adds the deterministic documentation regeneration entrypoint:
pwsh -File .\tools\scripts\docs\regen-from-openapi.ps1 -BaseUrl https://stella-ops.local -Token $env:STELLA_ADMIN_TOKEN
The script reads /api/openapi/aggregate, processes only status: ok service entries, writes each raw spec to docs/modules/<service>/openapi/v1.json, and renders docs/modules/<service>/api-reference.md through the first-party .NET doc generator. Each generated markdown file starts with the OAPI-059 sentinel header and must not be hand-edited. For offline or CI fixture verification, pass -AggregatePath <aggregate.json> and a temporary -OutRoot.
Postman collection generation is intentionally skipped in OAPI-059 because the selected generator is first-party .NET and no offline OpenAPI-to-Postman converter was approved or vendored.
Truthfulness Guardrails
OAPI-059-005 adds .gitea/workflows/openapi-truthfulness.yml and the local wrapper .gitea/scripts/test/run-openapi-truthfulness-check.sh.
generated-docsverifies every OAPI-059-stampeddocs/modules/<service>/api-reference.mdis byte-identical to output regenerated fromdocs/modules/<service>/openapi/v1.json.changelogverifies every changeddocs/modules/<service>/openapi/v1.jsonhas a newdocs/changelog/openapi/*.mdentry mentioning the affected service.- Local hook template:
devops/hooks/pre-commit. Enable withgit config core.hooksPath devops/hooks.
Contributor flow for endpoint contract changes: change the endpoint source, rerun tools/scripts/docs/regen-from-openapi.ps1, commit the generated spec and api-reference.md, and add docs/changelog/openapi/<date>-<service>-<summary>.md.
Runtime Flow
Client -> Gateway /api/openapi/aggregate
Gateway -> router state: distinct HELLO-registered services
Gateway -> http://<service>.stella-ops.local/<openapi-path>
Gateway -> cached per-service envelope
Gateway -> merged /openapi.json and /openapi.yaml views
Deprecated Legacy HELLO Flow
┌──────────────┐ HELLO ┌──────────────┐ GET /openapi.json ┌──────────────┐
│ Billing │ ──────────► │ │ ◄───────────────────── │ Client │
│ Service │ + schemas │ Gateway │ │ │
└──────────────┘ │ │ OpenAPI 3.1.0 │ │
│ │ ─────────────────────► │ │
┌──────────────┐ HELLO │ │ unified document └──────────────┘
│ Inventory │ ──────────► │ │
│ Service │ + schemas └──────────────┘
└──────────────┘
This HELLO-broadcast model is deprecated. It expected services to broadcast schemas and endpoint metadata in the HELLO payload, but Sprint 059 probes showed /openapi.json returning an empty paths object because service schemas were not broadcast in practice. The HELLO payload still identifies connected services; the OpenAPI source of truth is now each service’s own spec endpoint.
Configuration
OpenApiAggregationOptions
Configure OpenAPI aggregation in your Gateway configuration:
# router.yaml or appsettings.yaml
OpenApi:
Title: "My API Gateway"
Description: "Unified API for all microservices"
Version: "2.0.0"
ServerUrl: "https://api.example.com"
CacheTtlSeconds: 60
Enabled: true
LicenseName: "BUSL-1.1"
ContactName: "API Team"
ContactEmail: "api@example.com"
TokenUrl: "/auth/token"
Configuration Reference
| Property | Type | Default | Description |
|---|---|---|---|
Title | string | "StellaOps Gateway API" | API title in OpenAPI info section |
Description | string | "Unified API aggregating all connected microservices." | API description |
Version | string | "1.0.0" | API version number |
ServerUrl | string | "/" | Base server URL |
CacheTtlSeconds | int | 60 | Cache time-to-live in seconds |
Enabled | bool | true | Enable/disable OpenAPI aggregation |
LicenseName | string | "BUSL-1.1" | License name in OpenAPI info |
ContactName | string? | null | Contact name (optional) |
ContactEmail | string? | null | Contact email (optional) |
TokenUrl | string | "/auth/token" | OAuth2 token endpoint URL |
Disabling OpenAPI
To disable OpenAPI aggregation entirely:
OpenApi:
Enabled: false
Endpoints
Discovery Endpoint
GET /.well-known/openapi
Returns metadata about the OpenAPI document:
Response:
{
"openapi_json": "/openapi.json",
"openapi_yaml": "/openapi.yaml",
"aggregate_json": "/api/openapi/aggregate",
"etag": "\"5d41402abc4b2a76b9719d911017c592\"",
"generated_at": "2025-01-15T10:30:00.0000000Z"
}
Per-Service Aggregate
GET /api/openapi/aggregate
Returns one entry per HELLO-registered service:
{
"generatedAtUtc": "2026-05-18T00:00:00.0000000Z",
"services": [
{
"service": "platform",
"status": "ok",
"path": "/openapi/v1.json",
"fetchedAtUtc": "2026-05-18T00:00:00.0000000Z",
"durationMs": 12.4,
"spec": {
"openapi": "3.1.0",
"paths": {}
}
},
{
"service": "scanner",
"status": "missing",
"fetchedAtUtc": "2026-05-18T00:00:00.0000000Z",
"durationMs": 4.1
}
]
}
OpenAPI JSON
GET /openapi.json
Returns the full OpenAPI 3.1.0 specification in JSON format.
Headers:
Cache-Control: public, max-age=60ETag: "<content-hash>"Content-Type: application/json; charset=utf-8
Conditional Request:
GET /openapi.json
If-None-Match: "5d41402abc4b2a76b9719d911017c592"
Returns 304 Not Modified if content unchanged.
OpenAPI YAML
GET /openapi.yaml
Returns the full OpenAPI 3.1.0 specification in YAML format.
Headers:
Cache-Control: public, max-age=60ETag: "<content-hash>"Content-Type: application/yaml; charset=utf-8
Security Mapping
In fan-out mode, operation security metadata comes from each upstream OpenAPI document and is preserved in the merged view. The legacy HELLO-backed generator mapped router RequiringClaims into security schemes as described below; that behavior remains historical reference until the HELLO generator is removed.
Claim to Scope Mapping
When endpoints define RequiringClaims, these are converted to OAuth2 scopes:
// Endpoint with claim requirements
[StellaEndpoint("POST", "/invoices")]
[RequireClaim("billing:write")]
public sealed class CreateInvoiceEndpoint : IStellaEndpoint<...>
Becomes in OpenAPI:
{
"paths": {
"/invoices": {
"post": {
"security": [
{
"BearerAuth": [],
"OAuth2": ["billing:write"]
}
]
}
}
}
}
Security Schemes
The Gateway generates two security schemes:
BearerAuth
HTTP Bearer token authentication (always present):
{
"BearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "JWT Bearer token authentication"
}
}
OAuth2
Client credentials flow with collected scopes (only if endpoints have claims):
{
"OAuth2": {
"type": "oauth2",
"flows": {
"clientCredentials": {
"tokenUrl": "/auth/token",
"scopes": {
"billing:write": "Access scope: billing:write",
"billing:read": "Access scope: billing:read",
"inventory:read": "Access scope: inventory:read"
}
}
}
}
}
Scope Collection
Scopes are automatically collected from all connected services. If multiple endpoints require the same claim, it appears only once in the scopes list.
Legacy HELLO Compatibility
x-stellaops-gateway-auth.requiresAuthentication is emitted from the Gateway’s effective authorization semantics, not only the raw endpoint flag.
- If a microservice HELLO payload comes from an older router-common contract that does not include
requiresAuthentication, the Gateway fails closed. - For
allowAnonymous: falseendpoints with no explicit auth flag, the Gateway treats the route as authenticated-only. - Public routes must be explicitly marked
AllowAnonymousin the microservice to avoid accidental protection.
Generated Document Structure
The aggregated OpenAPI document follows this structure:
{
"openapi": "3.1.0",
"info": {
"title": "StellaOps Gateway API",
"version": "1.0.0",
"description": "Unified API aggregating all connected microservices.",
"license": {
"name": "BUSL-1.1"
},
"contact": {
"name": "API Team",
"email": "api@example.com"
}
},
"servers": [
{
"url": "/"
}
],
"paths": {
"/invoices": {
"post": {
"operationId": "billing_invoices_POST",
"tags": ["billing"],
"summary": "Create invoice",
"description": "Creates a new draft invoice",
"security": [
{
"BearerAuth": [],
"OAuth2": ["billing:write"]
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/billing_CreateInvoiceRequest"
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/billing_CreateInvoiceResponse"
}
}
}
},
"400": { "description": "Bad Request" },
"401": { "description": "Unauthorized" },
"404": { "description": "Not Found" },
"422": { "description": "Validation Error" },
"500": { "description": "Internal Server Error" }
}
}
},
"/items": {
"get": {
"operationId": "inventory_items_GET",
"tags": ["inventory"],
"summary": "List items",
"responses": {
"200": { "description": "Success" }
}
}
}
},
"components": {
"schemas": {
"billing_CreateInvoiceRequest": {
"type": "object",
"required": ["customerId", "amount"],
"properties": {
"customerId": { "type": "string" },
"amount": { "type": "number" },
"description": { "type": ["string", "null"] },
"lineItems": {
"type": "array",
"items": { "$ref": "#/components/schemas/billing_LineItem" }
}
}
},
"billing_CreateInvoiceResponse": {
"type": "object",
"required": ["invoiceId", "createdAt", "status"],
"properties": {
"invoiceId": { "type": "string" },
"createdAt": { "type": "string", "format": "date-time" },
"status": { "type": "string" }
}
},
"billing_LineItem": {
"type": "object",
"required": ["description", "amount"],
"properties": {
"description": { "type": "string" },
"amount": { "type": "number" },
"quantity": { "type": "integer", "default": 1 }
}
}
},
"securitySchemes": {
"BearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "JWT Bearer token authentication"
},
"OAuth2": {
"type": "oauth2",
"flows": {
"clientCredentials": {
"tokenUrl": "/auth/token",
"scopes": {
"billing:write": "Access scope: billing:write"
}
}
}
}
}
},
"tags": [
{
"name": "billing",
"description": "billing microservice (v1.0.0)"
},
{
"name": "inventory",
"description": "inventory microservice (v2.0.0)"
}
]
}
Schema Prefixing
Schemas are prefixed with the service name to prevent naming conflicts:
| Service | Original Type | Prefixed Schema ID |
|---|---|---|
billing | CreateInvoiceRequest | billing_CreateInvoiceRequest |
inventory | GetItemResponse | inventory_GetItemResponse |
Tag Generation
Tags are automatically generated from connected services:
- Tag name: Service name (lowercase)
- Tag description: Service description from
OpenApiInfoor auto-generated
Operation IDs
Operation IDs follow the pattern: {serviceName}_{path}_{method}
Example: billing_invoices_POST
Cache Behavior
TTL-Based Expiration
The document cache expires based on CacheTtlSeconds (default: 60 seconds):
OpenApi:
CacheTtlSeconds: 30 # More frequent regeneration
Setting CacheTtlSeconds: 0 regenerates the document on every request (not recommended for production).
Connection-Based Invalidation
The cache is automatically invalidated when:
- A new microservice connects (HELLO received)
- A microservice disconnects (connection closed)
This ensures the OpenAPI document always reflects currently connected services.
ETag Consistency
The ETag is computed from the document content hash (SHA256). This ensures:
- Same content = same ETag
- Content changes = new ETag
- Clients can use conditional requests to avoid re-downloading unchanged documents
Recommended Client Strategy
// Store ETag from previous response
let cachedETag = localStorage.getItem('openapi-etag');
const response = await fetch('/openapi.json', {
headers: cachedETag ? { 'If-None-Match': cachedETag } : {}
});
if (response.status === 304) {
// Use cached document
return getCachedDocument();
}
// Store new ETag and document
localStorage.setItem('openapi-etag', response.headers.get('ETag'));
const document = await response.json();
cacheDocument(document);
return document;
Service Registration
Microservice Options
Configure service metadata that appears in OpenAPI:
services.AddStellaMicroservice(options =>
{
options.ServiceName = "billing";
options.ServiceDescription = "Invoice and payment processing service";
options.ContactInfo = "billing-team@example.com";
});
Service OpenAPI Info
The ServiceOpenApiInfo is sent in the HELLO payload:
{
"instance": { ... },
"endpoints": [ ... ],
"schemas": { ... },
"openApiInfo": {
"title": "billing",
"description": "Invoice and payment processing service"
}
}
This description appears in the tag entry for the service.
Troubleshooting
Document Not Updating
- Check
CacheTtlSeconds- may need to wait for TTL expiration - Verify service connected successfully (check Gateway logs)
- Force refresh by restarting the Gateway
Missing Schemas
- Ensure
[ValidateSchema]attribute is applied to endpoints - Check for schema parsing errors in Gateway logs
- Verify endpoint implements
IStellaEndpoint<TRequest, TResponse>
Security Schemes Not Appearing
- OAuth2 scheme only appears if endpoints have claim requirements
- Check
RequiringClaimsis populated on endpoint descriptors - Verify claim types are being transmitted correctly
See Also
- Schema Validation - JSON Schema validation reference
- API Overview - General API conventions
- Router architecture - Gateway routing, authorization, and system-path implementation details
