# 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, 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.