Files
DodoSSH/docs/adr/0005-no-application-layer.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

56 lines
3.0 KiB
Markdown

# ADR 0005 — No separate Application layer
- Status: accepted
- Date: 2026-07-28
## Context
A conventional layering for a .NET service of this size would be Domain → Application →
Infrastructure → Api, with a mediator and one handler per use case.
DodoSSH's server is, in substance, CRUD plus one interesting write path
([ADR 0003](0003-sync-protocol.md)) plus a byte relay ([ADR 0004](0004-relay-authorization.md)).
It performs **no cryptography on secrets and no business logic over item contents**, because it
cannot read them. The genuinely hard logic — merge, key wrapping, trust decisions — lives on
the client.
## Decision
Three projects: `DodoSSH.Domain` (entities, enums, invariants, no EF), `DodoSSH.Infrastructure`
(DbContext, configurations, migrations, queries), `DodoSSH.Api` (host, with vertical slices
under `Features/`). Plus `DodoSSH.Contracts` for shared DTOs and `DodoSSH.Crypto` for the
envelope format.
No `Application` project, no mediator.
## Consequences
- A feature is one folder under `Features/`: endpoint, request/response mapping, and the query
or command inline. Reading an endpoint means reading one file, not tracing a request through
a handler, a validator, a behaviour pipeline and a repository interface.
- The testability argument for an Application layer does not apply here. The tests that matter
for this server are authorization tests and sync-protocol tests, and both must run against a
real PostgreSQL to be meaningful — an in-memory handler test would pass while the EF query
filter, the advisory lock and the CHECK constraints all went unexercised. Integration tests
with Testcontainers are the primary suite, so an extra seam buys nothing.
- Business invariants that are genuinely invariant (permission algebra, relay target
validation, cursor encoding) live in `Domain` as pure functions and are unit-tested there.
That is where the fast tests belong.
- **Accepted risk:** if the server later grows real domain logic — server-side policy
evaluation, workflow, notifications with side effects — endpoints will start to get long.
The mitigation is a rule, not a layer: when an endpoint file exceeds ~80 lines, extract a
named service into `Features/<Area>/`. Revisit this ADR if that happens three times in one
area.
- `DodoSSH.Crypto` is referenced by the API but only for format and fingerprint constants. It
must stay free of any code path that could decrypt a payload server-side, so that the
dependency cannot quietly become a capability.
### Rejected
- **Application layer with MediatR.** Adds a hop and a handler per feature. Its usual payoffs —
transaction/validation/logging behaviours, and decoupling from the web framework — are either
already provided (endpoint filters, ProblemDetails) or irrelevant (there is no second host).
- **Repository interfaces over EF Core.** `DbContext` is already a unit of work and EF's global
query filters *are* the authorization backstop ([ADR 0002](0002-minimal-apis.md)). Wrapping
it would hide the mechanism that keeps the API failing closed.