# internal/api/CLAUDE.md — bex-api composition root

`internal/api` is the **composition root**. It owns no business logic: it wires the feature services (`apps`, `logs`, `metrics`, `apikeys`, `postgres`) behind one auth gate and assembles the three surfaces. The domain lives in the feature packages; this package is presentation + wiring only ([docs/ADR006-bex-api.md](../../../../docs/ADR006-bex-api.md)).

**The design guarantee: one service per feature, three thin adapter fragments, and the transports stay SINGLE artifacts.** `server.go` builds exactly one REST router, one GraphQL schema (`newSchema` merges every feature's `GraphQLQuery`/`GraphQLMutation` into the single root Query/Mutation), and one MCP registry (`MCPServer` calls every feature's `RegisterMCP`) — all behind one auth gate. A feature's `rest.go`/`graphql.go`/`mcp.go` are **registration fragments** that _contribute_ to those roots; they are never standalone servers. This is what keeps the three surfaces from drifting.

Two rules keep it that way:

- **New verbs go in a feature's `service.go` only.** An adapter fragment never gets a second implementation of a verb; it maps the shared service method to its wire format. **Every service verb starts with `s.Authorize(ctx, core.Rel…)`** (the gate on `core.Base`, mapped to the Render-shaped relations `RelCanView`/`RelCanViewLogs`/`RelCanOperate`/`RelCanCreate`/`RelCanViewSensitive`/`RelCanManageKeys`) — enforcement at the service layer is what keeps the surfaces authorization-identical. `TestAuthzGuardsEveryVerb` (server_test.go) sweeps every feature service by reflection and fails a verb that forgets its guard. Pod logs / metrics reach past the generic client only through injected source funcs (`logs.PodLogSource`, `metrics.ResourceMetricsSource`, …), kept out of the domain so services stay clientset-free and testable.
- **Three-adapter parity — a change to one fragment must consider the other two.** When you add/rename/reshape a verb, argument, or field in REST, GraphQL, _or_ MCP, apply the equivalent change to the other two (or write down why one legitimately differs). `TestSurfaceParityAndWiring` asserts every feature registers into the single schema/router/registry — a dropped registration fails it.

**Adding a feature:** create `internal/<feature>/` with a `Service` embedding `*core.Base`, its verbs, and the fragments it needs (`RegisterREST` / `GraphQLQuery`+`GraphQLMutation` / `RegisterMCP` — implement only the ones it has). Add it to `Server`'s fields, `NewServer`, and `features()`. The root's type-assertions pick up whatever fragments it implements.

**Render.com consistency is mandatory, per surface** — each fragment mirrors its Render counterpart and is verified against the real Render artifact (REST → public OpenAPI spec; GraphQL → dashboard operation names; MCP → `render-oss/render-mcp-server` tool/arg names). Omit Render fields bex can't honor rather than faking them — the returned object stays a safe superset.

**Transports & auth.** Every HTTP route (REST, GraphQL, MCP streamable-HTTP at `/mcp`) sits behind the same gate in `auth.go` ([docs/ADR012-auth.md](../../../../docs/ADR012-auth.md)): OAuth2 API-key tokens via Hydra introspection, Kratos sessions for humans, no shared static token. The gate attaches the caller `core.Identity` to the request context, which the services' `Authorize` reads. Missing `BEX_HYDRA_ADMIN_URL` refuses to start; Ory outages fail closed. MCP's stdio transport (`RunStdio`) is the exception — its trust boundary is the subprocess itself, so no auth gate applies.
