git LFS server implemented on Cloudflare Workers + R2
cloudflare git lfs workers r2
TypeScript 100.0%
Other 0.1%
2 1 0

Clone this repository

https://tangled.org/mahardi.me/lfs-proxy https://tangled.org/did:plc:pha4rqcvhdc3us4n3y3tu6dr/lfs-proxy
git@knot.tangled.wizardry.systems:mahardi.me/lfs-proxy git@knot.tangled.wizardry.systems:did:plc:pha4rqcvhdc3us4n3y3tu6dr/lfs-proxy

For self-hosted knots, clone URLs may differ based on your setup.

Download tar.gz
README.md

lfs-proxy#

Git LFS server on Cloudflare Workers + R2. No egress fees, no file size limit.

The worker never touches file data — it authenticates requests, then hands back presigned S3 URLs so clients upload/download straight to R2.

How it works#

          UPLOAD (git lfs push)                          DOWNLOAD (git lfs pull)

 Client        Worker         R2              Client        Worker         R2
   │              │            │                │              │            │
   │ POST batch   │            │                │ POST batch   │            │
   │ op:"upload"  │            │                │ op:"download"│            │
   ├─────────────►│            │                ├─────────────►│            │
   │              │ HEAD obj   │                │              │ HEAD obj   │
   │              ├───────────►│                │              ├───────────►│
   │              │◄───────────┤                │              │◄───────────┤
   │              │            │                │              │            │
   │◄─────────────┤            │                │◄─────────────┤            │
   │ presigned PUT│            │                │ presigned GET│            │
   │ + verify url │            │                │              │            │
   │              │            │                │  GET ────────────────────►│
   │  PUT ────────────────────►│                │◄──────────────── bytes ──┤
   │  x-amz-meta-sha256: oid  │                │              │            │
   │◄──────────────── 200 OK ─┤
   │              │            │
   │ POST verify  │            │
   │ {oid, size}  │            │
   ├─────────────►│            │
   │              │ HEAD obj   │
   │              │ check meta │
   │              ├───────────►│
   │              │◄───────────┤
   │◄─────────────┤            │
   │    200 OK    │            │

R2 storage layout#

lfs-storage/                        ← R2 bucket
  my-repo/                          ← per-repo prefix
    ab3f7c...  (64 hex chars)       ← object data
      customMetadata:
        sha256 = "ab3f7c..."        ← integrity tag (set via x-amz-meta-sha256)

Auth#

R2 S3 API credentials double as Basic Auth credentials. The worker compares them with timingSafeEqual, then uses the same key pair to sign presigned URLs.

Git remote URL format:

https://{R2_ACCESS_KEY_ID}:{R2_SECRET_ACCESS_KEY}@{worker-host}/{repo}.git/info/lfs

Prerequisites#

  • Cloudflare account with Workers and R2 enabled
  • An R2 bucket (e.g. lfs-storage)
  • An R2 S3 API token with read + write permissions

Deploy#

# set secrets
npx wrangler secret put R2_ACCOUNT_ID
npx wrangler secret put R2_ACCESS_KEY_ID
npx wrangler secret put R2_SECRET_ACCESS_KEY

# deploy
npx wrangler deploy

R2_BUCKET_NAME is set in wrangler.jsonc as a plain-text var.

Client config#

Add a .lfsconfig to your repo:

[lfs]
  url = https://{key}:{secret}@{worker-host}/{repo}.git/info/lfs
  access = basic

Development#

bun install
npx wrangler dev     # local server on :8787
bun run test         # vitest with Cloudflare Workers pool

API routes#

Route Method Description
/:repo[.git]/info/lfs/objects/batch POST Batch API — returns presigned URLs for upload/download
/:repo[.git]/info/lfs/objects/verify POST Verify — confirms object exists with correct size + metadata
Any other path * 404
Non-POST to valid route * 405

All responses use application/vnd.git-lfs+json. Failed auth returns 401 with WWW-Authenticate: Basic.