Public Access
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.
2.7 KiB
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 enumeratesEndpointDataSourceand 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-knownalso is the onboarding story: the user types one server URL and the client discovers the rest.Asp.Versioninggets 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 theDodoSSH.Contractsassembly, 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.