PushStack

A self-hosted Git repository platform built from scratch — real Git smart-HTTP protocol server on Cloudflare R2, issues, pull requests, and two extracted npm packages for running isomorphic-git off a disk.

GitHub hosts your code. But how does it actually work?

When you run git push, what happens on the other end? What does the server receive? How does it store the objects? How do you turn a raw Git object store into a file tree you can browse in a browser? I had used GitHub for years without knowing. The only way to actually find out was to build the same thing.

There’s no native git binary anywhere in the stack, including the tests — isomorphic-git handles every git operation, and the object store behind it is Cloudflare R2, not a disk. That single constraint — no durable filesystem — is what the rest of the architecture is a response to.

The Git layer

isomorphic-git expects a filesystem: reads, writes, stats, directory listings. R2 gives you none of that for free. The fix is an fs-shaped adapter that maps the paths isomorphic-git actually touches onto R2 keys — repos/{owner}/{repo}/git/objects/..., refs/heads/..., and so on — stacked with an LRU cache (request coalescing, negative-result and directory-listing caching) and a retry layer with a circuit breaker in front of the network.

On top of that adapter sits a git smart-HTTP implementation — info/refs, upload-pack, receive-pack — handling pkt-line framing, side-band packfile chunking, CAS-checked ref updates, and pack consolidation once a repo’s loose packs cross a threshold. Every client-supplied ref name is validated before it reaches a single filesystem call, since git.commit/git.merge/git.deleteBranch don’t validate ref names internally the way git.branch does — an unchecked ref name is a path-traversal vector on shared storage.

Reads (clone, fetch, browsing) go straight against R2 through that adapter. Writes (push, in-browser file edits) hydrate the affected repo to a scratch directory, since isomorphic-git’s own merge and checkout still want a real worktree, then sync the result back to R2.

The code browser

Files render through Shiki, syntax-highlighted off the main thread in a web worker so a large file doesn’t block the UI. Diffs are computed with the diff package and rendered in a custom side-by-side/unified viewer built for this app rather than a pre-packaged diff component.

Heavy client-only dependencies — Markdown rendering, syntax highlighting — are lazy-loaded per route instead of bundled eagerly, since the tree view and blob pages are hot paths most visits hit.

The rest of the platform

The test suite took longer than anything else. Git operations have more edge cases than you’d expect — empty repositories, force pushes, binary files, concurrent pushes to the same repo. The Playwright suite covers user-visible flows end to end; Vitest covers the git-adapter and access-control logic in isolation, against real repos rather than mocked isomorphic-git calls.

Pulling the git layer out

Once the R2 adapter and the edge-safe git operations were solid — stress-tested against real clones, pushes, and merges, not just “works on my machine” — they moved out of the app entirely into two published packages that pushstack now depends on instead of carrying that logic inline:

They compose but neither imports the other — both just agree on isomorphic-git’s plain { fs, gitdir, cache } shape, so either is usable on its own.

Key decisions

Built with