No database, no leader node: a Git server that's a single binary in front of your bucket

No database, no leader node: a Git server that is a single binary in front of your bucket

Take inventory of what today sustains your self-hosted Git server. Almost certainly there’s a Postgres or MySQL holding the metadata. There’s a disk volume that can’t be lost, because the actual objects live there. There are backups of both, and there’s a restore procedure that nobody tested this year. And if you ever wanted to run two instances, the question appeared: who’s in charge — a leader node, a distributed lock, a Redis, something.

walgit erases that entire list. The README states it plainly:

“no database, no leader and no local state that matters”

And the principle that makes it possible fits in five words: “The bucket is the repository.” The disk and memory of the machine where it runs are cache. The source of truth is the object store.

It’s a Rust implementation of the architecture that Cursor described in Git at any scale, published by Tobi Lütke under the MIT license. It brings smart HTTP v0/v2 for fetch and push, clones via bundle-uri served as static files, Git LFS, a web UI for browsing, a JSON API with SDK, push policies per repository, webhooks and OIDC auth. And, according to the README, it serves repositories larger than the machine it runs on.

All consensus is a compare-and-swap

Here’s the mechanism, and it’s worth understanding before running a command, because it explains why the infrastructure list above disappears.

The README sums it up this way: the architecture consists of using “a write-ahead log in object storage as the source of truth, and making every on-disk repository a cache.”

When a push arrives, nothing is written to a database and no coordinator is asked. An entry is added to the WAL in the bucket. The commit point—the instant when that push exists for everyone—is a compare-and-swap operation on a tiny manifest. It’s the only coordination point in the entire system. Reads use conditional GET, so any instance reads the current state without asking anyone. And because any instance can reconstruct a repository from the WAL, you can point five machines at the same bucket and they don’t need to sync with each other: they sync against the object store, which is all that exists.

Cursor’s post describes the same mechanics for the system they call Continuity: “All updates to the write-ahead log are synchronized with an atomic compare-and-swap (CAS) operation on S3, so it’s always safe for any instance of a repository to receive a push.” And about disk: “We treat repositories like a warm cache on disk, but the source of truth is always the write-ahead log in S3… If a repository is missing from the local disk when accessed on a host, we just materialize it from the WAL.”

On the Hacker News thread an objection appeared that’s worth clarifying, because it’s the easiest misunderstanding to have here: “I’m confused by the word consensus in the explanation, to me it seems like a last-write-wins strategy, no consensus involved?”

It’s exactly the opposite. In last-write-wins the second writer overwrites the first and the first never finds out. In a compare-and-swap the second writer fails: the write carries the condition “only if the manifest is still the version I read”, and if another push came in between, the condition is not met, the operation is rejected and you have to reread and retry. Nothing is lost. It’s the same primitive used to build a lock, without the lock.

Why this is from 2024 and not always

This is the data that contextualizes everything else, and explains why suddenly several projects with the same shape appear.

That compare-and-swap over object storage didn’t exist for the first eighteen years of S3. AWS announced conditional writes—the If-Match that makes put-if-match possible—on November 26, 2024. Before that date, any system that needed an atomic commit point on S3 had to bring in an external coordinator: DynamoDB, ZooKeeper, etcd, a Postgres.

Ryan Dahl pointed it out publicly about walgit: “like celld, walgit depends only on s3 for storage and coordination. This has all become possible because of S3’s support for Compare And Swap since 2024. It’s just a practical pattern for reliability and costs.”

The HN thread converges on the same thing from another angle and names the neighbors: SlateDB, PicoMQ, celld, and Terraform 1.10, which since that version does state locking directly on S3 and no longer needs the DynamoDB table that was mandatory for years. walgit is the Git version of a pattern appearing across the industry at the same time. If you’ve been following the topic, the practical conclusion is broader than this repository: an S3 feature removed the coordination database from an entire class of systems, and it’s worth reviewing which of yours are still paying for it.

What it inherits from Cursor, and why it sounds familiar

If you read what we wrote about Origin, there’s a distinction in names worth keeping clear. Continuity is Cursor’s storage system; Origin is the code hosting product built on top. What walgit implements is Continuity, not Origin: the engine, not the forge.

And that’s where the calendar gets interesting. In that note, the closing was that a forge is the most expensive thing to change in your stack, that Origin didn’t offer self-hosting and there was no documented export route. Six days after Cursor published how their storage engine works, that architecture runs under MIT license against your bucket. It doesn’t replace Origin as a product—we’ll see everything walgit deliberately doesn’t do—but the hard part, the part that makes Git scale without a database, is now in a repository you can read.

A note to get the title right: Lütke wrote on X that he implemented it “over the weekend as an exercise” after reading Cursor’s post, with the context that he was frustrated with Shopify’s internal Git system. It’s a personal project, not a Shopify product nor something declared in production anywhere.

Running it against a bucket

What follows comes from the README and the project’s justfile. I didn’t run any of these commands: there’s no S3 bucket or Rust toolchain in the environment where I write, so the blocks are faithful to the documentation, not to my own run. Treat them as such.

The minimal deployment is three things: a bucket compatible with S3 (or GCS), a configuration file and a binary.

1. The configuration file. This is the README example, and it’s all a functional server needs:

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
```Pay attention to two keys. `auto_create_on_push = true` means that the first push to a route that doesn't exist creates the repository; convenient to get started, something you probably want set to `false` once there's more than one person pushing. And `tokens` takes the secret from an environment variable via `token_env`, not from the file — the config file can be versioned without carrying credentials inside it.

**2. Starting the server.** One command, with the token generated on the fly:

```sh
WALGIT_TOKEN_ME=$(openssl rand -hex 24) walgit serve --config walgit.toml

3. Push. Standard git against smart HTTP; no special client:

git -c http.extraHeader="Authorization: Bearer $WALGIT_TOKEN_ME" push https://git.example.com/acme/app.git main

That’s the whole system. To scale horizontally, you point another machine at the same bucket with the same configuration and you’re done: you don’t have to join it to a cluster or tell it who the leader is, because there is no cluster and no leader.

Building it

There are three ways, and the project ships all three:

just web-build && cargo build --release -p walgit-cli
nix build .#walgit
podman build -t walgit -f Containerfile .

To test it locally without a real bucket, the repository includes a development store and a standalone configuration:

just dev-store
./target/release/walgit-server --config walgit.standalone.toml

That’s the path I’d start with: just dev-store first, a test push against walgit.standalone.toml, and only then point to a real bucket.

Auth

Three modes, in order of increasing commitment: none, token, and oidc. The example one is token, with anonymous_read as a separate flag — you can have open reading and authenticated writing — and write permissions per principal. oidc is what you’ll want if you already have an identity provider and don’t want to manage tokens by hand.

On top of that there’s per-repository push policy and webhooks for ref events, which is what connects this to your existing CI: walgit doesn’t bring CI, it tells you when something changed.

Maintenance: the part that doesn’t show up in tutorials

A WAL that only grows needs someone to tidy it up, and this is where the project shows it thought beyond the demo. The maintenance commands are: checkpoint, bundles, compaction, base rebuilds, connectivity audits, and repairs.

It’s worth understanding what each family does. Checkpoints and compaction keep the WAL bounded, so materializing a repository from scratch doesn’t mean re-reading months of history. Bundles are what feed the bundle-uri: static packages that a clone can download directly from the bucket, so the heavy part of a clone is served by the object store and not your process. Base rebuilds redo the base that incremental entries stack on. And connectivity audits and repairs are the equivalent of git fsck for this architecture: verifying that all referenced objects are actually there.

That last pair is what I’d put in a cron before anything else. When the source of truth is a bucket, the kind of error you care about stops being “the disk died” and becomes “there’s a reference to an object that isn’t there”. A scheduled audit is the difference between you finding out and finding out from a developer who can’t clone.

On the testing side, the repository brings just test, just e2e, just test-s3 to run against a real store, and a failure simulation suite:

cargo test -p walgit-server --test sim

That a project of this size includes failure simulation says a lot about how seriously it took the distributed part.

What walgit won’t do

The repository has a GOAL.md file that’s more useful than the README for deciding if this serves you, because it spells out the non-goals explicitly. The declared goal is “A share-nothing git host, fast for monorepos, with an object store as the only source of truth.”

And the exclusions are deliberate: no code review, merge queues, or CI. It’s also not optimized for millions of small repositories, and doesn’t aim to fork git or invent object formats — it uses upstream git, gix, Rust with tokio and axum, and standard object stores.

Read it straight: walgit is not a replacement for GitHub or GitLab. It’s the storage and transport layer for Git, built for large monorepos, and everything else a forge gives you beyond that is still your problem. If what hurts is that your Git server chokes on the monorepo, this points right there. If what hurts is that your team needs pull requests, this is not the tool.

The same file declares concrete performance goals: repositories of several GB serving fast from machines with few GiB of RAM, cold ref lookups under a second, and CI clones in seconds. Important: these are design goals written by the author, not published measured results. There’s no benchmark in the repository backing them, so treat them as the bar the project set for itself, and measure them yourself against your own repository before you believe them.

What kind of project this is

Here it’s worth being precise, because the signals point in two directions at once.

In favor: the artifact is complete in a way that’s not usual. Containerfile and compose.yaml for containers, flake.nix for Nix, justfile with recipes for test, e2e and CI, walgit.example.toml and walgit.standalone.toml, OIDC auth, LFS, SDK, and the failure simulation suite already mentioned. None of that is what you find in an experiment published in a hurry.

Against: a single commit — the history comes squashed — and no release or tag. There’s no version you can pin in a production Containerfile; today you can only point to a branch. And the repository doesn’t declare itself experimental anywhere: neither the README nor GOAL.md have a status section or warning. The only signal of „this is an exercise