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
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
refs from the WAL · small packs local · big packs by range
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 seesokonly 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
- Smart HTTP v0/v2 ls-refs with prefixes, fetch with filter/shallow/deepen, receive-pack with atomic pushes, deletes, tags, push options, report-status-v2. sha1 and sha256.
- bundle-uri weekly full, chained dailies, hourlies — cut on calendar slots as a pure function of the WAL. Blobless families for
--filter=blob:none. - Git LFS batch + basic transfer into the bucket; optional read-through from an upstream LFS server for imported history.
- Web UI + API + SDK tree, blob, commits, diffs, the WAL's own health page; sha-addressed answers are immutable and cached everywhere;
repos.jsfor pages, agents and scripts. - Push policy protected refs, groups, fast-forward only, bypass lists — a small rule language per repository.
- Webhooks a bridge tails the WAL and POSTs ref events with a durable cursor; exactly once per
(repo, seq, ref), signed. - Per-repo settings bundle schedules, compaction, upstream follow — published into the WAL with history.
- S3 and GCS, first class AWS, MinIO, Ceph, R2, rustfs or GCS; server-side compose for the weekly bundle on both.
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
| mode | who gets in | how git authenticates |
|---|---|---|
none | everyone is anon — loopback experiments | nothing |
token | static tokens in the config, read from the environment | Authorization: Bearer or the token as a Basic password |
oidc | any 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
- README — the introduction, running it, the invariants.
- AGENTS.md — the architecture and every design decision with its reasoning.
- Bundle design — slots, chains, the two lists, the numbers.
- The announcement post.
- Git at any scale — the design walgit descends from.