walgit

A git server that is one binary
in front of a bucket.

walgit hosts git repositories with no database, no leader and no local state that matters. Point it at S3 or GCS and you get smart HTTP fetch and push, clones served as static files, Git LFS, a web UI, a JSON API — and repositories larger than the machine that serves them.

# 1. a config
cat > walgit.toml <<'EOF'
[server]
listen = "0.0.0.0:8080"
public_url = "https://git.example.com"
auto_create_on_push = true
[server.auth]
mode = "token"
anonymous_read = false
tokens = [{ principal = "me", token_env = "WALGIT_TOKEN_ME", write = true }]
[store]
backend = "s3"
bucket = "my-walgit"
[store.s3]
endpoint = "https://s3.us-east-1.amazonaws.com"
region = "us-east-1"
EOF

# 2. run it
WALGIT_TOKEN_ME=$(openssl rand -hex 24) walgit serve --config walgit.toml

# 3. use it — a push to a new name creates the repository
git push https://git.example.com/acme/app.git main

Source on GitHub Run it in five minutes

Why a bucket

Hosting git is hard because of packfiles

Every repository is compressed into large binary packs laid out to be small, not to be read in order. Every git operation is a random walk over gigabytes. Fine on a laptop; catastrophic over a network filesystem. The designs that survived keep real repositories on local NVMe and replicate at the pack level with strict consistency — paid for with quorum protocols, a database that maps repositories to machines, and a fleet of pets.

Continuity's insight

Cursor's Git at any scale changes the economics: make a write-ahead log in object storage the source of truth, and make every on-disk repository a cache. A push is an immutable object; it becomes visible when a tiny manifest is rewritten with a compare-and-swap. That CAS is the consensus. Any instance may accept a push. A replica that has never seen a repository reads the log and has it.

What walgit adds

The same design on machines smaller than the repository: a remote reader that serves refs, the UI and the API from pack indexes plus HTTP range reads; a history pack that keeps commits and trees local while blobs stay in the bucket; and bundle-uri as the clone transport, so a 30 GB fresh clone touches the server for a few kilobytes.

How it works

git / browser / CIfetch · push · clone · API
→
walgitany number of disposable hosts, one binary
refs from the WAL · small packs local · big packs by range
→
the bucketS3 or GCS — the repository
immutable packs · WAL · manifest CAS · bundles · LFS
A push
index the pack, check connectivity and policy, PUT pack ∥ idx ∥ log, then CAS the manifest. On a 412, re-read and retry. The client sees ok only after the bucket does.
A read
one conditional GET of the manifest — 304 means serve from the local copy, 200 means apply the new entries. There is no "eventually".
A clone
git's bundle-uri: the newest weekly full plus the chain of dailies and hourlies above it, straight from the bucket or a CDN; upload-pack sends only the remainder.
A fetch
the catch-up list has no fulls: a days-stale client downloads exactly the slots it missed.
Maintenance
checkpoints, bundles, compaction, base rebuilds, fsck and repair are one loop: compute the desired state from (config, WAL), do one bounded unit, under a lease. Self-healing by construction.
Nothing waits silently
slow work is a task with a progress stream, narrated to git on sideband 2 and to the browser as SSE.

What you get

Run it

One box

just dev-store                    # rustfs in a container (S3-compatible)
walgit-server --config walgit.standalone.toml
open https://walgit.localhost:8080/

The standalone config terminates its own TLS (a self-signed certificate, pinned for git by the installer), runs every role and streams every byte. Nothing in front.

Get it

nix run github:tobi/walgit -- --config walgit.toml
# or
podman build -t walgit -f Containerfile . && podman run -p 8080:8080 walgit
# or
cargo build --release -p walgit-cli      # after `just web-build`

Roles (server.roles): serve, maintain, events. Empty = all. Any number of hosts may point at one bucket; give each repository one maintainer with placement globs.

Authentication

modewho gets inhow git authenticates
noneeveryone is anon — loopback experimentsnothing
tokenstatic tokens in the config, read from the environmentAuthorization: Bearer or the token as a Basic password
oidcany OpenID Connect issuer: Google, Entra, Okta, Auth0, Keycloak, Dex, GitLab…sign in once in the browser, create a walgit access token at /_auth/tokens, paste it into the installer. Stateless; rotating the secret revokes all.

Developer setup is one idempotent command — sh -c "$(curl -fsSL 'https://git.example.com/services/public/install.sh')" — which stores the token in a file only you can read, installs a tiny credential helper (on a real 401 it erases the token and tells you where a new one comes from) and turns on transfer.bundleURI.

Optional: an nginx in front

walgit needs no proxy. Put one in front when you want public TLS from your own ACME client, one cached auth verdict per credential, or byte offload: walgit still does auth, validators and 304s, but answers a bundle or LFS download with X-Accel-Redirect; nginx fetches the object from the bucket itself (S3 presigned, or GCS with walgit's bearer), slices ranges and caches it on disk. The second download of a 30 GB weekly costs the walgit process nothing.

nginx → walgit   X-Walgit-Capabilities: accel-redirect, client-authorization
walgit → nginx   200 + X-Accel-Redirect: /_store/
                 X-Walgit-Store-Url: https://…        X-Walgit-Store-Key: repos/o/r/bundles/…
                 X-Walgit-Store-Authorization: Bearer … (GCS only)

deploy/nginx.conf.example is the reference and documents the contract. Nothing is assumed from configuration: the edge announces what it took over, per request.

Read more