# 0001 — Serve git over smart HTTP, not SSH

**Status:** accepted · **Date:** 2026-07-31

## Context

The previous attempt served git over an embedded russh server and reached working
clone, push, and public-key authentication. Carrying that forward raised three
problems at once:

1. **Unknown coexistence risk.** Topcoat's `topcoat dev` builds, watches sources, and
   restarts the app. Whether a long-lived embedded SSH listener survives that loop was
   untested, and it sat under four milestones of planned work.
2. **A layering question with no cheap answer.** Topcoat's grain is async components
   that query the database and check permissions inline. An SSH channel handler has no
   `Cx`, no request, and no cookies, so any authorization expressed in a component is
   invisible to `serve_push`. Reconciling the two needed a deliberate architectural
   position before any domain code could be written.
3. **A whole subsystem for key management.** SSH public keys need a table, SHA256
   fingerprinting, canonical re-encoding, dedup, and revocation UI — roughly a
   milestone of work serving one transport.

Git's smart HTTP protocol is an alternative transport with the same capabilities.
Notably, the original roadmap called for HTTP first; SSH was an unplanned detour.

## Decision

Serve git over smart HTTP as the primary transport, delegating the protocol to
`git http-backend` — the CGI that ships with git and implements `info/refs`,
`upload-pack`, `receive-pack`, and dumb-protocol fallback.

Authenticate with personal access tokens over HTTP Basic. SSH may return later as an
optional transport; it is not load-bearing.

Verified before accepting: `topcoat_router::Body` is a boxed `http_body::Body` used
for both requests and responses, with `into_data_stream()` for reading and
`Body::new()` wrapping any stream for writing — so pack data streams in and out
without buffering.

## Alternatives considered

- **Embedded SSH (russh), as attempt #2 did.** Better developer experience — key-based
  auth with no credential prompts. Rejected for the three costs above; the deciding
  factor was that it was the only context-less transport, and removing it dissolves an
  architectural question rather than answering it.
- **Implement smart HTTP directly.** More control, no subprocess. Rejected for the same
  reason gitoxide was rejected: protocol details we would own and get wrong, against a
  binary that already handles every case correctly.
- **Both transports from the start.** Twice the surface before the product exists.

## Consequences

- **The layering question disappears.** Every transport now has a `Cx`. There is no
  context-less caller, so a transport-neutral use-case layer is a matter of taste
  rather than necessity. See [architecture.md](../architecture.md#data-access).
- **No russh, no host keys, no coexistence risk.** One protocol, one port, and TLS
  terminates the way it does for any web app.
- **SSH key management collapses into personal access tokens** — smaller, and needed
  for `/api` regardless, so the work is shared rather than additional.
- **Worse developer experience, chosen knowingly.** `git clone https://…` prompts for
  credentials unless a credential helper is configured. SSH keys are nicer. GitHub ran
  HTTPS-plus-token as its primary path for years, so the path is well-trodden, but this
  is a real regression against attempt #2.
- **`git` remains a runtime dependency**, now including `git-http-backend`.
- **Body size limits will bite.** `topcoat-router` has a `body_limit` layer; a large
  push will be rejected until the cap is raised on the git routes. Expect to hit this
  as a confusing failure rather than a clear one.
- Reversible. `GitProtocolServer` stays a port, so adding SSH later means an adapter
  plus a key-management subsystem — not a rewrite.
