Files
DodoSSH/docs/adr/0002-minimal-apis.md
T
jaap-jan ce43f397a6 Add ADRs 0001-0006 and README (M0)
Records the decisions the milestone plan already made, with their costs stated rather
than only their benefits:

- 0001 e2ee-trust-model: key hierarchy, the AAD-to-row binding that stops the server
  moving ciphertext between rows, and the four-layer public-key trust story. States
  plainly that revocation is not retroactive, that Connect cannot be a security
  boundary, and that the IdP becomes a key-distribution trust root.
- 0002 minimal-apis: feature modules with explicit registration; capability negotiation
  instead of Asp.Versioning, since client and server upgrade independently when
  self-hosted.
- 0003 sync-protocol: single write path, revision cursors, and the bigserial
  pre-commit sequence gap that silently corrupts sync — plus the per-vault advisory
  lock that fixes it and the test that must prove it.
- 0004 relay-authorization: relay forwards bytes rather than terminating SSH, so
  zero-knowledge survives; server-resolved target IPs in the ticket to defeat DNS
  rebinding; why host addresses must be plaintext when relay is enabled.
- 0005 no-application-layer: why the usual Application/mediator layer earns nothing
  here, with the trigger that would make us revisit it.
- 0006 observability-stack: OTel plus built-in ILogger; liveness excludes dependencies
  so a database blip cannot restart the container and kill live SSH sessions.

Also adds a README covering layout, build, enforced conventions and milestones.
2026-07-28 12:28:44 +02:00

2.7 KiB

ADR 0002 — Minimal APIs with explicitly registered feature modules

  • Status: accepted
  • 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.