# ADR 0002 — Minimal APIs with explicitly registered feature modules

- Status: accepted; the endpoint-framework decision is superseded by [ADR 0008](0008-fastendpoints.md)
- Date: 2026-07-28

## Context

The API is roughly 60 endpoints: identity and enrollment, a public-key directory, teams,
vaults and grants, sync, read-only item queries, relay, audit and admin. It is consumed by
one first-party desktop client.

## Decision

Minimal APIs, grouped into feature modules under `Features/`, registered **explicitly** from
`Setup/EndpointRegistration.cs`. Typed results (`Results<Ok<T>, ForbidHttpResult,
ProblemHttpResult>`) throughout, one endpoint per file.

Version with a hard-coded `/api/v1` prefix and **no `Asp.Versioning` package**. Instead ship
capability negotiation:

- `GET /api/v1/meta` — server version, sync protocol version, crypto spec version, feature
  flags, `minClientVersion`, and the push caps.
- `GET /.well-known/dodossh-configuration` — OIDC authority, client id, scopes, relay URL.

## Consequences

- Per-endpoint filters and metadata compose cleanly, and typed results give an accurate
  OpenAPI document without attribute noise.
- Explicit registration over reflection scanning: predictable startup cost, trimming-friendly,
  and every route is greppable. The cost is one line per module, which is worth paying.
- Minimal APIs' real weakness is that a cross-cutting concern is easy to forget. Two
  mitigations, both required: group-level `RequireAuthorization`, and an **endpoint-inventory
  test** that enumerates `EndpointDataSource` and fails the build if any endpoint is absent
  from an explicit authorization expectations table. A new endpoint therefore cannot be added
  without making an authorization decision.
- Capability negotiation over version routing is the right trade for a self-hosted product,
  where client and server upgrade independently and skew is normal rather than exceptional.
  `.well-known` also *is* the onboarding story: the user types one server URL and the client
  discovers the rest. `Asp.Versioning` gets added when a v2 actually exists, not before.
- The generated OpenAPI document is for third parties and a future CLI. It is emitted at build
  time to `artifacts/openapi/v1.json`, committed and CI-diffed. **The client's real contract is
  the `DodoSSH.Contracts` assembly**, guarded by PublicApiAnalyzers so an accidental DTO change
  is a build error rather than a runtime deserialisation failure on a user's laptop.

### Rejected

- **MVC controllers.** Would work, but bring filter/model-binding machinery this API does not
  need and blur the one-endpoint-one-file structure that keeps the authz surface reviewable.
- **Reflection-based endpoint discovery.** Convenient until a route silently disappears
  because an assembly was not loaded, or trimming removes it.

## Superseded in part

[ADR 0008](0008-fastendpoints.md) replaces minimal APIs with FastEndpoints. Everything else on this
page still stands and is still the reason those endpoints look the way they do: the hard-coded
`/api/v1` prefix, no `Asp.Versioning`, capability negotiation through `/api/v1/meta` and
`/.well-known/dodossh-configuration`, and `DodoSSH.Contracts` as the client's real contract.

The two mitigations above survive the change with their reasoning intact. Registration is still
explicit rather than a reflection scan — 0008 feeds FastEndpoints a literal type list for the reason
this page gives, and then some. The endpoint-inventory test is no longer a plan: it exists as of
0008, and it matters more under a hand-maintained list than it did here, because a forgotten line is
now the way a route goes missing.
