# Architecture

## Layers

DDD + clean architecture. Dependencies point inward. The domain knows nothing about
HTTP, SQL, git, or Topcoat.

```
web / ssh  (interface)   →  application (use cases)  →  domain (entities, ports)
                                    ↑
                         infrastructure (sqlite, git binary, argon2)
                         implements the ports the domain/application declare
```

| Layer | Holds | Must not import |
|---|---|---|
| `domain` | entities, value objects, typed IDs, repository traits, `DomainError` | anything infrastructural |
| `application` | use cases, ports (`GitStorage`, `GitProtocolServer`, `PasswordHasher`), `AppConfig` | concrete adapters |
| `infrastructure` | SQLite repos, `git` subprocess wrappers, Argon2, russh, web handlers | — |

**Every operation takes an `Actor`.** Authorization is checked in the use case, before
any side effect. Not in the handler, not in the adapter. The previous attempt got this
right and it paid off — `serve_push` could enforce `Role::Owner` in one place
regardless of transport.

## Conventions

These come from two prior attempts. They earned their place.

### Typed IDs — never raw strings for entity references

```rust
// CORRECT — the compiler catches a swapped argument
fn get_member(&self, org_id: &OrgId, user_id: &UserId) -> ...

// WRONG — silently compiles, fails at runtime
fn get_member(&self, org_id: &str, user_id: &str) -> ...
```

`UserId`, `OrgId`, `MembershipId`, `RepoId`, `InviteCodeId`, `SshKeyId`.

### `new()` validates, `from_trusted()` doesn't

`new()` is for user input and returns `Result`. `from_trusted()` is for rows loaded
out of the database, which were validated on the way in. Infrastructure repos should
always use `from_trusted()` — re-validating DB rows means a validation-rule change
turns old rows unreadable.

### `from_str` returns `Result`, never `Option`

A silently-defaulted enum parse is a bug that surfaces days later as wrong
permissions. Applies to `Role`, `Visibility`, `RegistrationPolicy`.

### Every repository port gets two implementations

An in-memory one and a SQLite one. The in-memory one is what makes use cases testable
without a database — attempt #2 reached 60 tests this way and they ran fast enough to
stay in the loop.

### Ports live where they're consumed

Repository traits in `domain/repository/`. Service ports the application needs
(`GitStorage`, `GitProtocolServer`, `PasswordHasher`) in `application/port.rs`. Use
cases take `&impl Port`, not a boxed trait object.

## DB-plus-filesystem writes

Creating a repo writes to two places that can't share a transaction: the
`repositories` row and the bare repo on disk. Neither prior attempt fully solved this.

Attempt #1 used a **compensating transaction** — create on disk first, and if the DB
insert fails, delete the directory:

```rust
let repo = self.repository.create(&id).await?;
if let Err(e) = self.repo_record_repo.create(&record).await {
    let _ = self.repository.delete(&id).await;  // compensate
    return Err(e.into());
}
```

That leaks an orphaned directory if the process dies between the two calls. Good
enough to ship; write down that it's a known hole rather than rediscovering it. The
durable fix is a reconciliation sweep on boot, or marking rows pending and committing
after the filesystem write lands.

## Open question: Topcoat's data access vs clean architecture

**This is the one genuinely unresolved design question, and it blocks Milestone 1.**

Topcoat's model is async components that query the database directly and check
permissions inline, with `#[memoize]` deduplicating calls per request. State comes
from `app_context::<T>(cx)`, and the framework's own guidance is to "prefer composable
`cx: &Cx` functions over middleware/extractors for auth and request-scoped data."

That is a view layer reaching straight for the data layer — the exact thing the layer
rules above forbid. The two models are in real tension and the resolution has to be
deliberate:

- **Hold the line.** Components call use cases; use cases hold the ports. Costs some
  of Topcoat's ergonomics and may fight the framework's grain.
- **Let components read, force writes through use cases.** Reads go direct (they're
  the ones that benefit from memoization and per-component fetching); every mutation
  and every authorization decision stays in a use case. Pragmatic middle.
- **Adopt Topcoat's model fully.** Fastest to build, and abandons the layering that
  made the previous attempt's authorization work uniformly across web *and* SSH.

The SSH transport is the thing that makes this non-obvious: `serve_push` has no `Cx`
and no request. Whatever authorization lives in a Topcoat component is unavailable to
it. Anything enforcing a permission needs to sit somewhere both transports can reach.

Decide before Milestone 1 and write it up as decision 0001 — see
[decisions/TEMPLATE.md](decisions/TEMPLATE.md).

## Safety

Safe Rust only. No `unsafe`.
