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:

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 hostProgram pathEmits OpenAPI after this pass?Path(s)OAPI-059-001 result
AdvisoryAI.WebServicesrc/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Program.csYes/openapi/v1.jsonRemoved Environment.IsDevelopment() gate.
OpsMemory.WebServicesrc/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-webFolded 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.
Attestor.WebServicesrc/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-webFolded 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.WebServicesrc/Attestor/StellaOps.Signer/StellaOps.Signer.WebService/Program.csYes/openapi/v1.jsonAdded default AddOpenApi/MapOpenApi; no new package was required.
Authoritysrc/Authority/StellaOps.Authority/StellaOps.Authority/Program.csYes/.well-known/openapiExisting custom static-document provider; unchanged in this pass.
IssuerDirectory.WebServicesrc/Authority/StellaOps.Authority/StellaOps.Authority/Program.cs (live authority); the standalone host is frozen under src/__Obsoleted/Authority/StellaOps.IssuerDirectory.WebService/Yes, through authorityAuthority’s /.well-known/openapi and /openapi/v1.jsonFolded 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.
Concelier.WebServicesrc/__Obsoleted/Concelier/StellaOps.Concelier.WebService/Program.csNo live hostNoneRetired 2026-09-11 under VULN-G6; Vulnerabilities publishes the successor document.
Excititor.WebServicesrc/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 hostRetired 2026-09-11 under VULN-B1. The Vulnerabilities hub owns VEX ingestion and publication; /openapi/excititor.json is no longer served.
Doctor.WebServicesrc/__Obsoleted/Doctor/StellaOps.Doctor.WebService/Program.csNo live hostNoneDeployable retired by DOC-5 stage 4 (2026-08-17); source withdrawn by PCD-73 (2026-09-27).
EvidenceLocker.WebServicesrc/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-webFolded 2026-09-04 (EVD-10a). evidence-web maps the EvidenceLocker endpoint groups under /api/evidence/v1 from src/Evidence/__Libraries/StellaOps.EvidenceLocker.Application/.
ExportCenter.WebServicesrc/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-webWithdrawn 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.
Findings.Ledger.WebServicesrc/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-webFrozen 2026-08-28 (FND-20). findings-web carries 61 of the retired host’s 99 routes; the other 36 were withdrawn.
RiskEngine.WebService— deleted 2026-08-27 (SPRINT_20260722_010 FND-10)n/an/aThe 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.WebServicesrc/Integrations/StellaOps.Integrations.WebService/Program.csYes/openapi/v1.json, /swagger/v1/swagger.json, /swaggerAdded default AddOpenApi/MapOpenApi; un-gated existing Swagger middleware.
PacksRegistry.WebServicesrc/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 jobengineMerged into JobEngine (JOB-9). The OAPI-059-001 change here (removed Environment.IsDevelopment() gate) was on the retired host.
Scheduler.WebServicesrc/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 jobengineFrozen 2026-09-12 (JOB-9). Its Program.cs became the JobEngine composition root; the router registers the service as jobengine, not scheduler.
Notifier.WebServicesrc/__Obsoleted/Notifier/StellaOps.Notifier/StellaOps.Notifier.WebService/Program.csNo web targetNoneRetired. 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.WebServicesrc/Notify/StellaOps.Notify.WebService/Program.csYes/openapi/v1.jsonRemoved 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.WebServicesrc/Platform/StellaOps.Platform.WebService/Program.csYes/openapi/v1.jsonRemoved Environment.IsDevelopment() gate; Platform conformance skip removed.
ReachGraph.WebService(host deleted 2026-09-08, 023 GRA-10)n/an/aAdded 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.
Remediation.WebServicesrc/__Obsoleted/Remediation/StellaOps.Remediation.WebService/Program.csNo live hostNoneFrozen 2026-08-19 (W3-14). Console remediation surfaces are AdvisoryAI’s in-module autofix; the marketplace design has no successor host.
Replay.WebServicesrc/Replay/StellaOps.Replay.WebService/Program.csYes/.well-known/openapiExisting custom provider; unchanged in this pass.
SbomServicesrc/SbomService/StellaOps.SbomService/Program.csYes/openapi/v1.jsonAdded the central Microsoft.AspNetCore.OpenApi package and default AddOpenApi/MapOpenApi; local no-dependency build passed.
Scanner.WebServicesrc/Scanner/StellaOps.Scanner.WebService/Program.csYes/openapi/v1.json when Microsoft.AspNetCore.OpenApi is presentExisting MapOpenApiIfAvailable reflection helper; unchanged in this pass.
Tester.WebServicesrc/Tester/StellaOps.Tester.WebService/Program.csNoNoneExplicitly forbidden for this worker; left for Tester-owned sprint.
Timeline.WebServicesrc/Timeline/StellaOps.Timeline.WebService/Program.csYes/openapi/v1.json, /swagger/v1/swagger.json, /swaggerActive matrix target timeline-web; includes the merged timeline-indexer endpoints and worker responsibilities.
TimelineIndexer.WebServicearchivedMerged into timeline-webNone as an active matrix targetThe 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.WebServicesrc/Unknowns/StellaOps.Unknowns.WebService/Program.csYes/openapi/v1.json, /swagger/v1/swagger.json, /swaggerAdded default AddOpenApi/MapOpenApi; un-gated existing Swagger middleware.
VexHub.WebService(tree deleted by a8472b6e10)No live hostNoneSuccessor is the Vulnerabilities hub, src/Vulnerabilities/StellaOps.Vulnerabilities.WebService/Program.cs, which serves /openapi/v1.json.
VexLens.WebService(tree deleted by a8472b6e10)No live hostNoneSuccessor is the Vulnerabilities hub, src/Vulnerabilities/StellaOps.Vulnerabilities.WebService/Program.cs, which serves /openapi/v1.json.
Workflow.WebServicesrc/Workflow/StellaOps.Workflow.WebService/Program.csYes/openapi/v1.jsonAdded explicit Microsoft.AspNetCore.OpenApi 10.0.1 because this project disables central package management; local no-dependency build passed.
Verdict.WebServicesrc/__Obsoleted/__Libraries/StellaOps.Verdict.WebService/Program.csNo live hostNoneNever activated; withdrawn by PCD-73 (2026-09-27, owner ruling). The Verdict library and CLI stay; there is no Verdict HTTP API.
AirGap.Controllersrc/__Obsoleted/AirGap/StellaOps.AirGap.Controller/Program.csNo live hostNoneSource retired 2026-09-10 (OK-5). Platform’s environment-state API owns custody; src/Platform/StellaOps.Platform.WebService/Program.cs serves /openapi/v1.json.
AirGap.Timesrc/__Obsoleted/AirGap/StellaOps.AirGap.Time/Program.csNo live hostNoneSource 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:

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:

StatusMeaning
okA valid OpenAPI document was fetched and parsed.
missingThe service responded but no candidate path returned a valid OpenAPI document.
timeoutThe service did not complete within the per-service timeout.
errorThe 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.

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

PropertyTypeDefaultDescription
Titlestring"StellaOps Gateway API"API title in OpenAPI info section
Descriptionstring"Unified API aggregating all connected microservices."API description
Versionstring"1.0.0"API version number
ServerUrlstring"/"Base server URL
CacheTtlSecondsint60Cache time-to-live in seconds
EnabledbooltrueEnable/disable OpenAPI aggregation
LicenseNamestring"BUSL-1.1"License name in OpenAPI info
ContactNamestring?nullContact name (optional)
ContactEmailstring?nullContact email (optional)
TokenUrlstring"/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:

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:


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.


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:

ServiceOriginal TypePrefixed Schema ID
billingCreateInvoiceRequestbilling_CreateInvoiceRequest
inventoryGetItemResponseinventory_GetItemResponse

Tag Generation

Tags are automatically generated from connected services:

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:

  1. A new microservice connects (HELLO received)
  2. 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:

// 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

  1. Check CacheTtlSeconds - may need to wait for TTL expiration
  2. Verify service connected successfully (check Gateway logs)
  3. Force refresh by restarting the Gateway

Missing Schemas

  1. Ensure [ValidateSchema] attribute is applied to endpoints
  2. Check for schema parsing errors in Gateway logs
  3. Verify endpoint implements IStellaEndpoint<TRequest, TResponse>

Security Schemes Not Appearing

  1. OAuth2 scheme only appears if endpoints have claim requirements
  2. Check RequiringClaims is populated on endpoint descriptors
  3. Verify claim types are being transmitted correctly

See Also