| 1 | # 0001 — Serve git over smart HTTP, not SSH |
| 2 | |
| 3 | **Status:** accepted · **Date:** 2026-07-31 |
| 4 | |
| 5 | ## Context |
| 6 | |
| 7 | The previous attempt served git over an embedded russh server and reached working |
| 8 | clone, push, and public-key authentication. Carrying that forward raised three |
| 9 | problems at once: |
| 10 | |
| 11 | 1. **Unknown coexistence risk.** Topcoat's `topcoat dev` builds, watches sources, and |
| 12 | restarts the app. Whether a long-lived embedded SSH listener survives that loop was |
| 13 | untested, and it sat under four milestones of planned work. |
| 14 | 2. **A layering question with no cheap answer.** Topcoat's grain is async components |
| 15 | that query the database and check permissions inline. An SSH channel handler has no |
| 16 | `Cx`, no request, and no cookies, so any authorization expressed in a component is |
| 17 | invisible to `serve_push`. Reconciling the two needed a deliberate architectural |
| 18 | position before any domain code could be written. |
| 19 | 3. **A whole subsystem for key management.** SSH public keys need a table, SHA256 |
| 20 | fingerprinting, canonical re-encoding, dedup, and revocation UI — roughly a |
| 21 | milestone of work serving one transport. |
| 22 | |
| 23 | Git's smart HTTP protocol is an alternative transport with the same capabilities. |
| 24 | Notably, the original roadmap called for HTTP first; SSH was an unplanned detour. |
| 25 | |
| 26 | ## Decision |
| 27 | |
| 28 | Serve git over smart HTTP as the primary transport, delegating the protocol to |
| 29 | `git http-backend` — the CGI that ships with git and implements `info/refs`, |
| 30 | `upload-pack`, `receive-pack`, and dumb-protocol fallback. |
| 31 | |
| 32 | Authenticate with personal access tokens over HTTP Basic. SSH may return later as an |
| 33 | optional transport; it is not load-bearing. |
| 34 | |
| 35 | Verified before accepting: `topcoat_router::Body` is a boxed `http_body::Body` used |
| 36 | for both requests and responses, with `into_data_stream()` for reading and |
| 37 | `Body::new()` wrapping any stream for writing — so pack data streams in and out |
| 38 | without buffering. |
| 39 | |
| 40 | ## Alternatives considered |
| 41 | |
| 42 | - **Embedded SSH (russh), as attempt #2 did.** Better developer experience — key-based |
| 43 | auth with no credential prompts. Rejected for the three costs above; the deciding |
| 44 | factor was that it was the only context-less transport, and removing it dissolves an |
| 45 | architectural question rather than answering it. |
| 46 | - **Implement smart HTTP directly.** More control, no subprocess. Rejected for the same |
| 47 | reason gitoxide was rejected: protocol details we would own and get wrong, against a |
| 48 | binary that already handles every case correctly. |
| 49 | - **Both transports from the start.** Twice the surface before the product exists. |
| 50 | |
| 51 | ## Consequences |
| 52 | |
| 53 | - **The layering question disappears.** Every transport now has a `Cx`. There is no |
| 54 | context-less caller, so a transport-neutral use-case layer is a matter of taste |
| 55 | rather than necessity. See [architecture.md](../architecture.md#data-access). |
| 56 | - **No russh, no host keys, no coexistence risk.** One protocol, one port, and TLS |
| 57 | terminates the way it does for any web app. |
| 58 | - **SSH key management collapses into personal access tokens** — smaller, and needed |
| 59 | for `/api` regardless, so the work is shared rather than additional. |
| 60 | - **Worse developer experience, chosen knowingly.** `git clone https://…` prompts for |
| 61 | credentials unless a credential helper is configured. SSH keys are nicer. GitHub ran |
| 62 | HTTPS-plus-token as its primary path for years, so the path is well-trodden, but this |
| 63 | is a real regression against attempt #2. |
| 64 | - **`git` remains a runtime dependency**, now including `git-http-backend`. |
| 65 | - **Body size limits will bite.** `topcoat-router` has a `body_limit` layer; a large |
| 66 | push will be rejected until the cap is raised on the git routes. Expect to hit this |
| 67 | as a confusing failure rather than a clear one. |
| 68 | - Reversible. `GitProtocolServer` stays a port, so adding SSH later means an adapter |
| 69 | plus a key-management subsystem — not a rewrite. |