How We Built a Git Server in Go (Smart HTTP, SSH, gRPC)
gogitinternalsarchitecture

How We Built a Git Server in Go (Smart HTTP, SSH, gRPC)

Marouane AmanarAugust 14, 2026

Why we wrote our own git server in Go instead of running GitLab or Gitea — one binary speaking smart HTTP, SSH, and gRPC, with per-organization SSH key isolation and a pluggable auth layer.

The "Just Install Gitea" Question

When you decide your platform needs git hosting, the reasonable move is to install Gitea or GitLab and move on. We didn't — we wrote a git server in Go, from the transport layer up. This post is about why, and about the architecture that made writing it the right call.

The short version: Git hosting on a platform isn't a feature, it's a dependency. Every other subsystem — pipelines, code explorer, AI review, environment provisioning — needs to read and write repos, respond to pushes, and know the difference between authorized and unauthorized access in platform terms, not git terms. Gluing Gitea's semantics onto that is a constant fight. Owning the server means every capability composes.

One Binary, Three Transports

The server speaks three protocols from a single Go binary:

  • Smart HTTP — The info/refs?service=git-upload-pack / git-receive-pack dance that git clone https://... uses. This is what most of our users hit, because it's how the dashboard and CI authenticate without SSH keys.

  • SSHgit@git.monkeys.cloud:org/repo.git. Keys are registered per organization, and the SSH layer resolves the key to an identity before any git command runs.

  • gRPC — An internal transport for first-party callers (the API, the pipeline engine, the code explorer). gRPC lets our own services talk to the repo layer with typed requests and an internal token, without going through a public git protocol at all.

Three transports, one auth model: Every request — regardless of protocol — resolves to a user and an organization, then checks permission against the same policy. There is no "SSH users" list and "HTTP users" list drifting apart, because there's only one identity layer.

The Auth Model: Per-Organization SSH Key Isolation

The security property we cared most about is isolation. A key you add to Org A must not be able to read Org B's repos — even if both orgs contain a project with the same name. That sounds obvious, but a lot of git hosts model keys as global credentials and enforce isolation only at the authorization layer, which is one bug away from leaking.

Our model is structural: an SSH public key is owned by an organization. When a connection arrives, the server resolves the key to its org, then maps the requested path (org/repo) against it. A key from Org A asking for Org B's repo is rejected at the resolution layer, not politely denied later. The same rule applies over HTTP: credentials resolve to an identity, and the identity carries an org scope.

The Read/Write Split: go-git for Reads, exec git for Writes

The most pragmatic decision in the whole server is: we use two git engines.

EngineOperation TypeResponsibilitiesWhy We Use Itgo-gitReadsLog, diff, tree listings, blob lookups, ref enumerationPure-Go implementation. In-process access to repo internals without shelling out. No string parsing or subprocess overhead.exec gitWritesReceiving pushes (receive-pack)The actual git binary. Handles streaming objects and every edge-case/quirk of git's reference implementation safely.

Trying to reimplement push acceptance in a library is where compatibility bugs live. So: reads are in-process and fast; writes defer to the real thing. Each side uses the tool that's best at its job.

Why This Matters for the Platform

Owning the git layer pays off in every feature that touches a repo:

  • The code explorer needs trees, blobs, and diffs — served in-process by go-git, with server-side caching, instead of over NFS or a git-daemon sidecar.

  • The pipeline engine needs to know the moment a push lands — it gets a typed internal event, not a webhook poll.

  • MonkeysAI needs the diff for PR review — it asks the repo layer over gRPC, with the same auth context the user has.

  • Backups and migrations — we snapshot repos at the object level with the same layer that serves clones.

None of that is clean when git hosting is an external appliance with its own API. All of it is trivial when git is a library your platform owns.

The Costs (Honesty Section)

Writing your own git server isn't free. We paid in three ways:

  1. Protocol compliance. Smart HTTP and SSH have sharp edges (quirk handling, error framing, edge cases in the pkt-line protocol). Our test suite replays real client interactions.

  2. Concurrency. Handling many concurrent clones and pushes on bare repos needs careful locking — we hold ref locks only around actual mutations.

  3. Maintenance. Every git version bump is our concern now, not a vendor's.

The trade only makes sense because git's protocols are stable and well-documented, and because the platform's need for tight integration is genuinely unusual. For most teams, GitLab is the right answer. For a platform that needs git as a limb, owning it is the answer.

What You Can Take From This

You don't need to write your own git server to learn from ours:

  • Choose the right tool per operation, not per system. Our read/write split (go-git for reads, exec git for writes) is a pragmatic dual-engine approach you can apply to any complex dependency.

  • Make security structural. Isolation that lives in the data model beats isolation that lives in a permission check.

  • Own what you compose. Every integration you build on top of a dependency is a reason to consider owning the dependency.

Every MonkeysCloud project ships with 2 free servers and 2 free databases — git hosting, environments, CI/CD, and MonkeysAI included. Try it free

Attachments

How We Built a Git Server in Go (Smart HTTP, SSH, gRPC)