0027. API contracts are Go code; OpenAPI documents and clients are generated from them
Status: accepted, 2026-10-21
Context
About 700 routes in 18 services have no machine-readable contract. Handlers decode into anonymous
structs and answer views that embed database rows, panels type every API by hand in src/lib/api.ts,
MCP tools are written by hand in seven services, and the CLI compiles service command code it can't keep
current: services ship v2.x tags under paths without /v2, which go get refuses, so the CLI pins old
versions and drifts. The native apps (0018) need typed clients in
Swift and Kotlin, services describe their screens over their operations
(0019), and approvals gate operations by their risk
(0025): all three need one declaration of each operation. docs can render OpenAPI
references and has none of xvii's own.
The options were spec-first (OpenAPI or TypeSpec documents, generated Go servers), Protobuf with
Connect, a Go framework that derives OpenAPI (Huma), or a small package of our own on the route tables
the services already have.
Decision
- Go is the source of truth. Each service declares its API in a contract module beside its main one,
go.xvii.network/<service>/apiv1: operations api.Op[In, Out] (id, method and path, summary, scope,
risk, access, resource kind, audit action, MCP and CLI hints) and the wire types, separate from storage
types. The module depends only on kit, has its own semver (v0.x until a native app consumes it, then
v1.x with additive changes only) and its own tags; a breaking change is a new module apiv2 serving
/api/v2, with /api/v1 kept. It replaces the client/ convention: the contract is the Go client.
- kit/api serves it. The route table binds operations to handlers. kit checks access and scope from
the same declaration, decodes and validates input, encodes output, answers errors in the existing shape,
and calls the approvals gate. Server-sent events, WebSockets, uploads, multipart bodies and downloads
have explicit helpers and are part of the contract. Handlers stay
net/http functions; there is no
framework and no generated server code.
- One OpenAPI 3.1 document per service, generated from the Go types and their doc comments,
committed as
apiv1/openapi.json, embedded in the binary and served at /api/v1/openapi.json. Our
extensions say what OpenAPI can't: x-xvii-scope, x-xvii-risk (the levels of 0025, or
base/protected), x-xvii-access, x-xvii-resource, x-xvii-sensitive, and hints for streams,
offline queueing, audit, MCP and the CLI.
- Contract tests ride on the API tests. In test binaries kit/api validates every response against
the document, strictly (undocumented fields, nulls, statuses and error codes fail), and tracks which
operations no test reaches. CI checks that the document and the generated panel types are current, and
refuses breaking changes unless a commit announces them (
! or BREAKING CHANGE:); after the freeze
for native apps, it refuses them outright.
- Clients are generated from the document. TypeScript for panels and Kotlin (kotlinx.serialization)
for Android by kit's
apigen, which knows our subset and extensions; Swift by Apple's
swift-openapi-generator from a client copy apigen prepares (open enums, no WebSocket operations). Go
programs use the contract modules.
Consequences
- Every service migrates its route table to operations; tier 1 first (notify, then status, errors,
config, home, then apps, ai, id), wire-compatible, each with its existing tests turned into contract
tests. New services start with
apiv1.
- Handlers map storage rows to wire types, so a new column no longer leaks into an API; loose responses
(
map[string]any) have to be named or marked as loose.
- Each service repository gains a nested module, a second tag series (
apiv1/vX.Y.Z) and make api;
CI tests every module in the repository.
- The reflector, the emitters and the breaking-change diff are ours to maintain; they cover a fixed
subset of OpenAPI, with golden tests, and the documents stay standard, so other generators remain a
fallback.
- conventions/api.md, go.md, panel.md ("typed by hand" goes) and cli.md change in full when kit/api
ships; docs renders every installed service's reference.
- Generators and contract code are Go, so 0003's one stack holds;
Swift and Kotlin stay in the native apps' repositories.
- Design:
superpowers/specs/2026-10-21-api-contracts-design.md in this repository.