Skip to content

OpenAPI spec for the WaveHouse API + machine-readable discovery (/.well-known/api-catalog) #302

Description

@EricAndrechek

Goal

Publish a machine-readable OpenAPI 3.1 spec for the WaveHouse HTTP API, serve it for discovery, and use it to close the agent/crawler API-discovery gaps that #301 deliberately deferred.

#301 ("improve discoverability for AI agents and crawlers") handled the docs-site discovery surface (sitemap, Link headers, content-signals; llms.txt + markdown content-negotiation already shipped). It explicitly skipped RFC 9727 /.well-known/api-catalog because we have no OpenAPI spec yet — that's this issue.

Why

  • Agents/SDKs can discover and call the API programmatically (service-desc).
  • Unlocks /.well-known/api-catalog (RFC 9727) → flips the Cloudflare agent-readiness "API catalog" finding that fix(docs): real per-page Last-updated dates + open AI content signals #301 couldn't.
  • A self-hosted WaveHouse that serves its own /openapi.json is genuinely self-describing per deployment.
  • We can regenerate the SDK's hand-written request/response types (clients/ts/src/types.ts) from one source of truth.

What we're starting from

  • Router: chi v5, centralized route table in internal/api/router.go (~17 routes under /v1 + health/version).
  • All request/response bodies are typed Go structs with json tags (StructuredQuery, Aggregation/Filter/OrderClause/TimeRange, NamedQuery/ParamDef, Policy/TablePolicy/RolePermissions, batchResult/recordResult, queryRequest).
  • Consistent error shape {"error": "..."} (internal/api/errors.go).
  • Auth: JWT Bearer header + ?token= query fallback (middleware on all /v1; admin gate on /v1/admin/*). → securitySchemes: http/bearer/JWT + an apiKey-in-query token.
  • Existing machine-readable contract: the TS SDK codegen introspects the live /v1/schema endpoint for ClickHouse table/column types (clients/ts/src/cli/codegen.ts). SDK request/response types are hand-written (clients/ts/src/types.ts).
  • No OpenAPI/Swagger anywhere today.

Because the API already exists and is well-typed, the textbook spec-first (spec → generated server) path is out — we'd be regenerating handlers we already have.

Options

Option How Effort Output Drift risk
A. Hand-write openapi.yaml + CI contract test Author the 3.1 doc; a Go test loads it (kin-openapi/pb33f/libopenapi) and validates real handler responses against it Low–med OpenAPI 3.1, full control Low if the contract test is real; otherwise high
B. Annotations → swag // @Summary/@Param/@Success on handlers, swag init via go:generate Medium OpenAPI 2.0 (3.x only via lossy convert) Low
C. Reflect existing structs → spec (small generator) A ~150–250-line Go generator walks the known routes + reflects the structs (invopop/jsonschema) into openapi.yaml; structs stay the schema source of truth Medium OpenAPI 3.1 Very low (schemas auto-derived)
D. Type-first framework huma (sits on top of chi, emits 3.1 + Swagger UI + request validation for free) or ogen (replaces handler layer, reflection-free) High (refactor) OpenAPI 3.1, always in sync None

Recommendation

  • A or C — both give clean OpenAPI 3.1, keep the Go structs as the schema source of truth, and need no framework change. C has the least drift (schemas auto-derived) at the cost of writing the generator; A is fastest to stand up but leans on the CI contract test to prevent drift.
  • Avoid B — OpenAPI 2.0 + annotation clutter fights this repo's minimal-comment ethos (AGENTS.md: "comment the why, not the what").
  • D (huma-on-chi) is the strategic pick if we expect the API to grow a lot and want validation + a docs UI for free — a deliberate refactor, not a side quest. huma is the gentlest (layers onto the existing chi router).
  • Either way, the any-typed fields (Filter.Value, Columns as string | []string) need explicit hand-written schema fragments.

Wiring once the spec exists

  1. Serve from the binary via go:embed at /openapi.json (each self-hosted deployment self-describes at its own origin). servers: can be relative/placeholder for a self-hosted product.
  2. Mirror at wavehouse.dev/openapi.yaml (static) + render it in the docs.
  3. Publish /.well-known/api-catalog (RFC 9727, application/linkset+json): service-desc → the OpenAPI, service-doc → /api, status → /healthz.
  4. (Optional) Regenerate clients/ts/src/types.ts from the spec (openapi-typescript), unifying with the existing /v1/schema table-type codegen.

Acceptance

  • Decide approach (A / C / D)
  • openapi.yaml (3.1) covering the /v1 routes + health/version, with securitySchemes (Bearer + token query)
  • Drift guard (CI contract test for A; the generator for C)
  • Serve /openapi.json from the binary (go:embed)
  • Mirror at wavehouse.dev + docs render
  • /.well-known/api-catalog (RFC 9727)
  • (optional) SDK type regen from the spec

Refs: #301 (deferred this), RFC 9727, RFC 9264 linkset.

Activity

  1. added
    enhancementNew feature or request
    area/apiHTTP handlers, routing, middleware
    area/sdkTypeScript SDK (clients/ts/)
    area/docsDocumentation, site/, README
    on Jun 8, 2026
  2. coderabbitai commented on Jun 8, 2026

    @coderabbitai
    🔗 Related PRs

    #123 - fix(api): drop CORS credentials + skip same-origin decoration [merged]
    #137 - feat(deploy): local dev o11y stack (#121) [merged]


    📝 Issue Planner

    Check the box below or use the @coderabbitai plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.

    • Create Plan

    🧪 Issue enrichment is currently in open beta.

    You can configure auto-planning by selecting labels in the issue_enrichment configuration.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  3. EricAndrechek commented on Aug 13, 2026

    @EricAndrechek
    MemberAuthor

    One consumer worth designing for explicitly, because it adds a few requirements the current acceptance list doesn't cover: a gateway or reverse proxy fronting several WaveHouse deployments of differing versions, deriving its own behavior from each deployment's spec.

    That's the generalization of the "self-hosted WaveHouse self-describes at its own origin" bullet already in the Why — but a fronting system needs more from the document than a human or a one-deployment SDK does. Everything below is a gap I hit reading the current router and docs, and each one is the difference between "the proxy derives this" and "the proxy hardcodes a table that silently goes stale on upgrade."

    Stable operationIds, treated as API surface. Anything mapping operations onto its own handlers, permissions, or tools needs a stable key — path+method is workable but brittle when a path changes shape. Worth adding to the acceptance list, and worth saying in the docs that renaming one is a breaking change, since downstream mappings are invisible from here.

    Express the admin/non-admin split in the spec. Right now that gate is a router fact: RequireAdmin wraps /v1/admin/*, and also /v1/schema, /v1/schema/refresh, and /v1/dlq/stats. So the /v1/admin/ prefix is not a reliable signal for whether an operation is privileged — which is exactly the trap a fronting proxy falls into, because path-shape is the only thing it can see today. Encoding the gate (a distinct security requirement, a scope, or an x- extension) lets a proxy derive authorization tiers instead of guessing, and means a new privileged endpoint landing outside the prefix doesn't silently widen what that proxy allows. This is the single most valuable addition on this list.

    Mark the operational endpoints. The reverse-proxy guide already recommends keeping /livez//readyz//healthz internal — a public /readyz turns each hit into a ClickHouse Ping — and exposing only /v1/health. If the spec marks which operations are operational rather than API surface, a fronting proxy can exclude them automatically instead of maintaining its own denylist, which is the thing that goes stale precisely when the deployment is upgraded.

    info.version tied to the build. So a consumer can cache the derived surface per engine version and invalidate on upgrade. Without it, cache invalidation is guesswork against a moving target.

    State whether discovery itself is authenticated. /openapi.json and /.well-known/api-catalog should be explicitly unauthenticated (or explicitly not). If learning the surface requires a credential, the discovery story doesn't work for a fronting system — and arguably not for the agent/crawler cases either, which is the motivating use here.

    Document the two non-obvious response shapes on /v1/admin/query. It returns ClickHouse's FORMAT JSON envelope unwrapped — just the data array — rather than the envelope itself; and an inline FORMAT override (SELECT 1 FORMAT CSV) bypasses default_format=JSON entirely and returns a non-JSON body with the upstream Content-Type. Both are correct and deliberate, and both are the kind of thing a spec-generated client gets wrong by assuming a uniform JSON response. Worth pinning in the schema rather than leaving to the handler comments.

    Decide how /v1/stream is represented. SSE doesn't model as an ordinary response, and a consumer generating a typed method from the spec will produce something broken unless the document says text/event-stream and marks it as streaming. Also relevant: it accepts the ?token= query credential the other endpoints don't need, so its security differs.

    None of these change the recommendation between A / C / D — they're requirements on the resulting document whichever way it's produced. If it helps, the ones I'd put in acceptance are the operationIds, the admin-gate encoding, and the operational-endpoint marking; the rest are documentation quality.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/apiHTTP handlers, routing, middlewarearea/docsDocumentation, site/, READMEarea/sdkTypeScript SDK (clients/ts/)enhancementNew feature or request

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions