openapi
Every HTTP-serving daemon exposes its API at GET /openapi.json. The document is generated from resreg.Resource.RowType reflection — one walk over the registry produces components/schemas from struct fields and paths./v1/<name> for the four CRUD verbs. Drift between handler and doc is structurally impossible because both read the same struct.
What you get
OpenAPI 3.1 JSON. Public — no auth gate. Cached for the process lifetime; reflection runs once per daemon. Paste a URL into any conformant tool (Swagger UI, Redoc, Stoplight Elements, an SDK generator) and the daemon's surface is browsable, callable, and codegen-ready.
Endpoints by daemon
| Daemon | URL | Owned resources |
|---|---|---|
routd |
https://<host>/openapi.json via proxyd, or http://routd:8080/openapi.json intra-container |
groups, acl, acl_membership, routes, web_routes, scheduled_tasks, secrets, network_rules |
proxyd |
http://proxyd:8080/openapi.json |
proxyd_routes |
onbod |
http://onbod:8080/openapi.json |
onboarding_gates |
webd |
http://webd:8080/openapi.json |
none — forwards routes CRUD to proxyd; doc is informational |
dashd |
http://dashd:8080/openapi.json |
none — HTMX operator UI; cold-tier CRUD lives in routd |
timed |
http://timed:8080/openapi.json |
none — reads scheduled_tasks; routd owns the table |
Daemons that own no resources still emit a valid OpenAPI document so external tooling can introspect them uniformly. The schemas + paths blocks are empty but info, servers, and the standard components/responses (400, 401, 403, 404, 409, 500) are present.
How it works
Each cold-tier resource is one Go struct with db:, json:, and yaml: tags — the registry lives in routd. resreg.OpenAPI(daemon, baseURL, resources) walks the registered resources and emits:
- One
components.schemas.<Name>perRowType. Property name from thejson:tag; type inferred from the Go kind (string,integer,boolean, array, object).omitemptyremoves the field fromrequired. - Four operations per resource:
GET /v1/<name>(list),POST /v1/<name>(create),PATCH /v1/<name>/{pk}(update),DELETE /v1/<name>/{pk}(delete). Composite PKs collapse to one URL parameter; the description flags the encoding so clients know to URL-encode separators. - Standard error responses (400, 401, 403, 404, 409, 500) defined once in
components.responsesand$ref-d from every operation. - One
servers[]entry frombaseURL.
Spec: specs/5/36-yaml-manifests.md §"OpenAPI emission". Subsumes the former specs/5/4-openapi-discoverable.md (folded into this spec).
Browsing the spec
The JSON is a regular text file — curl + jq works fine for most lookups:
curl -s https://<host>/openapi.json | jq '.paths | keys'
curl -s https://<host>/openapi.json | jq '.components.schemas.Routes'
For an interactive renderer, point Swagger UI or Redoc at the URL. Neither is bundled with arizuko; pick the tool your operator workflow already uses.
SDK generation
Any OpenAPI 3.1 codegen tool (openapi-generator, swagger-codegen, oapi-codegen) can consume the spec to produce a typed client. Run the generator against the live daemon's URL or save the JSON and run offline. Re-run on every release — the spec moves with the struct, so clients stay current by re-generating.
Caveats
- The endpoint advertises the shape of the surface, not whether a given caller has permission to use it. Authorization happens in
auth.Authorizeper grants; the OpenAPI doc doesn't reflect per-caller scope. - Descriptions are minimal in v1 — types and paths are correct, but the prose is sparse. Iterate as needed.
- Composite-PK URL parameters collapse to one segment; the description explains the encoding. If your client library can't handle this, fall back to
POSTwith a body that names the PK fields.