Swagger/OpenAPI Docs Gating¶
The pattern: filter by tag, unlock by app secret¶
Every public-facing Codevertex service's /v1/docs (Swagger UI) and /api/v1/openapi.json follow the same shape, implemented independently but identically in treasury-api, auth-api, and notifications-api (internal/http{,api}/handlers/swagger.go + swagger_filter.go in each):
- Classify every operation's tag as external or internal in a small
map[string]boolat the top ofswagger.go. External tags are the service's real product surface for outside developers (e.g. treasury'sExternal eTIMS API/Public Payments/Health, auth'sAuth/OAuth/Discovery/Health, notifications'Notifications/Health). Everything else — tenant-operational endpoints, platform-admin endpoints — is internal by default. filterSpecToTags(spec, allowed)(pure, unit-tested, identical in all three services) returns a shallow copy of the spec withpathsfiltered to only operations carrying an allowed tag. Both the full and external-only specs are computed once at startup and cached — every request is a map lookup, never a re-parse.- The docs page's own token bar — not Swagger UI's built-in Authorize dialog — is what actually unlocks anything. Paste an app secret (
bng_app_..., or a plainbng_...developer key), and the page's own JSfetch()s/api/v1/openapi.jsonwith it asX-API-Key, both for the initial spec load and viarequestInterceptoron every subsequent "Try it out" call. Swagger UI's own Authorize dialog only ever affectsTry it out, never the initial (unauthenticated) spec fetch — using a plainurl:loader there would never unlock anything. OpenAPIJSONresolves the secret server-side and is the sole source of truth. It reports its decision back via two response headers the docs page reads to drive its badges:X-Docs-View: internal|externalandX-Docs-Environment: sandbox|production|none. The client never decides trust on its own.
Why an app secret, not a JWT¶
Earlier versions of this pattern (treasury-api's first cut) let staff paste their session JWT to unlock the internal view. Replaced fleet-wide after user feedback: a JWT is too long to comfortably copy-paste into a docs page. An app secret is short, and — because App.app_type already distinguishes platform (cross-tenant S2S/staff) from tenant — it doubles as the credential for the sandbox/production badge too, so one paste box drives both unlocks instead of needing two different mechanisms.
The two independent unlocks, and what actually decides each¶
Both come from one call to shared-auth-client's APIKeyValidator.ValidateAPIKeyFull (already published, no per-service reimplementation needed) — or, for auth-api specifically, an in-process equivalent (APIKeyHandler.ResolveAnyToken), since auth-api validating a secret against itself over HTTP would be a pointless round-trip:
- Internal/full spec visibility —
result.Rolescontains"superuser", whichValidateAppToken/resolveAPIKeyTokenin auth-api only ever sets for a platform-type App (or a plain developer key created with a non-emptyservicefield). A tenant-scoped App's secret, however privileged within its own tenant, never unlocks this — its whole relationship to the API is the external surface. - Sandbox/production badge —
result.Environment, the App's ownenvironmentfield. This was silently dropped byshared-auth-client'sAPIKeyValidationResultuntilv0.12.0(2026-08-20) — if you're consuming this library andEnvironmentalways reads empty, check you're onv0.12.0+.
No literal second "sandbox server" URL is invented anywhere in servers[]. There's only ever been one real host per service; sandbox-vs-production has always been a credential-level distinction enforced by the handlers themselves (see treasury-api's internal/platform/sandbox package), not a routing one — the badge reflects that truthfully instead of adding a fake base URL a developer could point real tooling at by mistake.
Adding this to a new service¶
- Classify your spec's tags into
externalDocTags. - Copy
swagger_filter.goverbatim (it's pure and has no service-specific logic). - Copy
swagger.go'srenderDocsHTMLverbatim — it only takes atitlestring. - Wire
resolveAppSecretOptionalto either ashared-auth-clientAPIKeyValidatorpointed atAUTH_SERVICE_URL(the normal case) or, if you are auth-api, the in-process resolver. - Add unit tests mirroring
swagger_filter_test.go/swagger_privilege_test.goin any of the three existing services — anonymous → external tags only; invalid secret → external tags only; valid platform secret → full spec; valid tenant secret → external tags + production badge only ifenvironment=production.