OpenAPI and client generation
Every page on this site is written by hand. The document your code should build against is not: Gate generates one OpenAPI 3.1 description from the route definitions themselves, so it cannot drift from what the server actually accepts.
Where the spec lives
Both paths are relative to /api/v1.
/openapi.json/docs/openapi.json is the document. /docs is Swagger UI over it, also reachable at
/docs/api-reference so you can try calls from this site.
curl -sS https://evonia-gate.annatarhe.com/api/v1/openapi.json -o gate.jsonNeither route requires a credential, which makes the spec safe to fetch from CI or a build step without provisioning a key for it.
What it covers
One document describes both of Gate's surfaces.
| Prefix | Served by | Covers |
|---|---|---|
/api/v1 | Gate | Organizations, projects, service accounts, RBAC, configuration, usage, billing |
/api/auth | better-auth | Sessions, sign-in, organizations, and the OAuth 2.1 and OIDC protocol routes |
The two are merged, not concatenated, so the /api/auth half is namespaced to keep it apart from
the control plane. Its operations are tagged Auth / Default, Auth / Organization,
Auth / Oauth-provider, and Auth / Jwt — that casing is better-auth's — and its component
schemas are prefixed Auth…: AuthUser, AuthSession, AuthOrganization. The prefix is not
cosmetic; Organization means different things on the two surfaces and would otherwise collide.
Because one document spans both prefixes, its single servers entry is the origin and every path
key carries its prefix in full — /api/v1/authorize, /api/auth/oauth2/token. A generated client
needs https://evonia-gate.annatarhe.com as its base URL, not .../api/v1.
Security schemes
Three schemes are declared, and which one applies depends on who is calling.
| Scheme | Credential | Used by |
|---|---|---|
cookieAuth | better-auth.session_token cookie | Browsers — the Gate dashboard and your own web app |
apiKeyAuth | X-API-Key: gate_sk_… | Service accounts calling /api/v1 |
oauth2 | Authorization code or client credentials | Tokens Gate issues to other products |
oauth2 is the odd one out. It describes tokens your users and services carry to your APIs,
which then verify them against Gate's JWKS — see
Verifying tokens. It is not a way to call /api/v1; for that you
want a service API key. Both of its flows advertise the same eight
scopes, listed in Scopes and roles.
The vendored copy
The repository commits the generated document at openapi/gate.json, and a test regenerates and
compares it on every run, so it cannot fall behind the routes. After an intentional API change,
pnpm openapi:snapshot rewrites it.
That copy exists for you: a downstream repository can vendor or submodule it and pin an exact version, so builds do not need a reachable Gate and a deploy cannot silently change the types you compiled against.
It differs from a freshly fetched document in one place. The live servers entry reflects the
GATE_BASE_URL the instance runs with, so a spec pulled from a Gate you started locally says
http://localhost:3621; the committed copy is pinned to the production origin so it does not churn
with whoever regenerated it.
Generate a client
npx openapi-typescript https://evonia-gate.annatarhe.com/api/v1/openapi.json -o src/gate-api.d.tsgo tool oapi-codegen -package gate -generate types,client openapi/gate.json > gate/client.goTypeScript
openapi-typescript emits types only — no runtime, no client. You keep using fetch and index
into the generated paths for the request and response shapes.
import type { paths } from "./gate-api";
type Authorize = paths["/api/v1/authorize"]["post"];
// `requestBody` is optional in the generated type, hence NonNullable.
type AuthorizeBody = NonNullable<Authorize["requestBody"]>["content"]["application/json"];
type AuthorizeResult = Authorize["responses"][200]["content"]["application/json"];
export async function authorize(body: AuthorizeBody): Promise<AuthorizeResult> {
const response = await fetch("https://evonia-gate.annatarhe.com/api/v1/authorize", {
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": process.env.GATE_API_KEY ?? "",
},
body: JSON.stringify(body),
});
if (!response.ok) throw new Error(`authorize failed with ${response.status}`);
// The envelope is part of the type: read `.data`, not the object itself.
return (await response.json()) as AuthorizeResult;
}Point the generator at openapi/gate.json instead of the URL if you have vendored the spec.
Go
oapi-codegen generates both the types and a client whose methods come from each operation's
operationId. The go tool form above needs the generator in your go.mod tool directives; if it
is not there yet, run it directly:
go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest \
-package gate -generate types,client openapi/gate.json > gate/client.goGenerating from the committed openapi/gate.json rather than the live URL keeps go generate
hermetic, which matters more in Go than in TypeScript because the output is checked in.
Response conventions
Three things carry through from the API conventions into whatever the generator produces.
- Successes are wrapped. Every
2xxbody is{ "data": … }, with two deliberate exceptions that carry no resource:GET /api/v1/healthand the Stripe webhook receiver. The generated type includes the wrapper, so unwrap.dataat your client boundary rather than at every call site. - Errors are problem JSON.
type,title,status, andrequestId, witherrorsadded for validation failures. Match ontypeandstatus; the error catalog lists every one. Most generators type error bodies loosely, so validate them yourself. operationIdis stable and unique. It is what generators turn into method names —authorize,listOrganizations,putConfigEntry. A test enforces that every operation has one and that no two collide across the surfaces, and Gate treats renaming one as a breaking change, so a generated client survives a regeneration.