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.
56 lines
3.0 KiB
Markdown
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.
|