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.
| 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. |
| OpsMemory.WebService | src/AdvisoryAI/StellaOps.OpsMemory.WebService/Program.cs | Yes | /openapi/v1.json, /swagger/v1/swagger.json, /swagger | Added default AddOpenApi/MapOpenApi; un-gated existing Swagger middleware. |
| Attestor.WebService | src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Program.cs | Yes | /openapi/v1.json | Added default AddOpenApi/MapOpenApi; no new package was required. |
| 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. |
| IssuerDirectory.WebService | src/Authority/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Program.cs | Yes | /openapi/v1.json | Added default AddOpenApi/MapOpenApi; no new package was required. |
| BinaryIndex.WebService | src/BinaryIndex/StellaOps.BinaryIndex.WebService/Program.cs | Yes | /openapi/v1.json, /swagger/v1/swagger.json, /swagger | Added the central Microsoft.AspNetCore.OpenApi package and default AddOpenApi/MapOpenApi; retained un-gated Swagger middleware. |
| Symbols.Server | src/BinaryIndex/StellaOps.Symbols.Server/Program.cs | Yes | /openapi/v1.json | Removed Environment.IsDevelopment() gate. |
| Concelier.WebService | src/Concelier/StellaOps.Concelier.WebService/Program.cs | Yes | /.well-known/openapi, /swagger/v1/swagger.json when Swagger is enabled | Existing custom provider; unchanged in this pass. |
| Excititor.WebService | src/Concelier/StellaOps.Excititor.WebService/Program.cs | Yes | /.well-known/openapi, /openapi/excititor.json | Existing custom provider; unchanged in this pass. |
| Doctor.WebService | src/Doctor/StellaOps.Doctor.WebService/Program.cs | Yes | /openapi/v1.json | Removed Environment.IsDevelopment() gate. |
| EvidenceLocker.WebService | src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.WebService/Program.cs | Yes | /openapi/v1.json | Removed Environment.IsDevelopment() gate. Local-only restore/build now passes with the existing package cache. |
| ExportCenter.WebService | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Program.cs | Yes | /openapi/v1.json, /.well-known/openapi, /openapi/export-center.json, /openapi/export-center.yaml | Removed default MapOpenApi gate; existing discovery endpoints unchanged. |
| Findings.Ledger.WebService | src/Findings/StellaOps.Findings.Ledger.WebService/Program.cs | Yes | /.well-known/openapi | Restricted by OAPI-059-001 worker scope; existing static provider unchanged. |
| RiskEngine.WebService | src/Findings/StellaOps.RiskEngine.WebService/Program.cs | Yes | /openapi/v1.json | Removed Environment.IsDevelopment() gate. |
| 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. |
| PacksRegistry.WebService | src/JobEngine/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService/Program.cs | Yes | /openapi/v1.json | Removed Environment.IsDevelopment() gate. |
| Scheduler.WebService | src/JobEngine/StellaOps.Scheduler.WebService/Program.cs | Yes | /openapi/v1.json | Added the central Microsoft.AspNetCore.OpenApi package and default AddOpenApi/MapOpenApi; local no-dependency build passed. |
| Notifier.WebService | src/Notifier/StellaOps.Notifier/StellaOps.Notifier.WebService/Program.cs | No active web target | None in the active matrix | Historical source remains in the solution, but services-matrix.env marks notifier-web as merged into notify-web; only notifier-worker is an active matrix target. |
| 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. |
| ReachGraph.WebService | src/ReachGraph/StellaOps.ReachGraph.WebService/Program.cs | Yes | /openapi/v1.json, /swagger/v1/swagger.json, /swagger | Added default AddOpenApi/MapOpenApi; un-gated existing Swagger middleware. |
| Remediation.WebService | src/Remediation/StellaOps.Remediation.WebService/Program.cs | Source wired; not active matrix/publish target | /openapi/v1.json if separately hosted | The project exists and was wired earlier in Sprint 059, but there is no active remediation-web entry in services-matrix.env or build-service-publish.sh; do not count it in live rebuild proof until a service owner reactivates it. |
| 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 | src/Timeline/StellaOps.TimelineIndexer.WebService/Program.cs | Merged into timeline-web | None as an active matrix target | Historical project/source remains, but services-matrix.env comments out timeline-indexer-web and timeline-indexer-worker as merged into timeline-web; rebuild and live proof should 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. |
| VexHub.WebService | src/VexHub/StellaOps.VexHub.WebService/Program.cs | Yes | /openapi/v1.json | Removed Environment.IsDevelopment() gate. |
| VexLens.WebService | src/VexLens/StellaOps.VexLens.WebService/Program.cs | Yes | /openapi/v1.json | Removed Environment.IsDevelopment() gate. |
| 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. |
| Verdict.WebService | src/__Libraries/StellaOps.Verdict.WebService/Program.cs | Source wired; not active matrix/publish target | /openapi/v1.json if separately hosted | The library-host source exists and was wired earlier in Sprint 059, but there is no active verdict-web entry in services-matrix.env or build-service-publish.sh; do not count it in live rebuild proof until a service owner reactivates it. |
| AirGap.Controller | src/AirGap/StellaOps.AirGap.Controller/Program.cs | Yes | /openapi/v1.json | Added the central Microsoft.AspNetCore.OpenApi package and default AddOpenApi/MapOpenApi; local no-dependency build passed. |
| AirGap.Time | src/AirGap/StellaOps.AirGap.Time/Program.cs | Yes | /openapi/v1.json | Added the central Microsoft.AspNetCore.OpenApi package and default AddOpenApi/MapOpenApi; local no-dependency build passed. |
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: Policy.Gateway, SmRemote.Service, and Gateway.WebService. 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; custom-provider services use their published paths first (AuthorityandConcelier:/.well-known/openapi;Excititor:/openapi/excititor.json; legacy static providers such as Notifier, Replay, and Findings Ledger use/.well-known/openapi). - 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.
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:
powershell -ExecutionPolicy Bypass -File .\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
