Cloudsteading
Product image still needed. This listing has source documentation, but no reviewed screenshot yet.

dgit

Host Git repositories and browse their history on Workers and SQLite Durable Objects.

dgit is a self-hosted GitHub/GitLab alternative built on Cloudflare (Durable Objects, R2, Workers). Paid services required. Inspect the source and license in the linked repository.

Source & license

Upstream license: MIT

License TL;DR

You can use it, change it, self-host it and sell it. Keep the original copyright and license notice with copies of the code. You don’t have to publish your changes. The authors don’t promise it will work.

Explain MIT in plain English →

Summary of the main license. Separate packages and assets can have different terms.

Inspect repository ↗Read this project’s actual license ↗

Repository owner

@littledivy

See the upstream repository for the original creator and contributors.

Maintain this project? Maintainer verification →

Cloudflare hosting

Paid services required

dgit has a documented low-volume paid Cloudflare path beginning with Workers Paid ($5 USD/account/month), with separately metered usage. The supplied high CPU limit requires Paid; repository sizes and active Durable Object duration drive costs.

Hosting requirements
  • Use Workers Paid for the configured 300,000 ms CPU limit; $5 USD/account/month is a baseline, not a fixed all-inclusive price.
  • Workers Paid starts at $5 USD/account/month and includes 10 million requests/month plus 30 million CPU milliseconds/month; higher CPU limits do not make execution unmetered.
  • Use R2 Standard storage, at most 10 GB-month, 1 million Class A operations and 10 million Class B operations/month; provision an eligible billing-enabled R2 account.
  • Paid SQLite Durable Objects include 1 million requests/month, 400,000 GB-s duration/month and 5 GB-month SQL storage; active duration and excess database operations are metered.
  • Use a small personal or team workload; domain registration and optional third-party providers are separate costs. Provision your own IDs, secrets and migrations.
Check current pricing ↗
Sources checked 01/10/2026

Repository snapshot: d5eba04. Hosting eligibility reflects the deployment documentation and listed assumptions.

  • github ↗

    dgit is a git server for Cloudflare Workers and for your own machines with [celld](https://celld.dev). Each repository is a Durable Object: a small server with a name and a private SQLite database that holds the repository's objects and refs, speaks the git smart HTTP protocol to a stock git client, and renders a cgit-style web interface. There is no origin server, no filesystem, and no GitHub in the critical path. A repository nobody touches costs almost nothing, and applications shard by construction: one hot repository cannot slow another. Reads are public; pushes authenticate; pushing to a name that does not exist creates the repository.

  • gitlab ↗

    dgit is a git server for Cloudflare Workers and for your own machines with [celld](https://celld.dev). Each repository is a Durable Object: a small server with a name and a private SQLite database that holds the repository's objects and refs, speaks the git smart HTTP protocol to a stock git client, and renders a cgit-style web interface. There is no origin server, no filesystem, and no GitHub in the critical path. A repository nobody touches costs almost nothing, and applications shard by construction: one hot repository cannot slow another. Reads are public; pushes authenticate; pushing to a name that does not exist creates the repository.

  • workers ↗

    script name stays "git-cells" — Durable Object data is bound // to it, and renaming would orphan every hosted repository. "name": "git-cells", "main": "src/index.ts", "compatibility_date": "2026-08-01", "routes": [ { "pattern": "git.littledivy.com", "custom_domain": true } ], "limits": { // pack indexing of a large push is CPU-bound; 5 minutes is the paid-plan max "cpu_ms": 300000 }, "durable_objects": { "bindings": [ { "name": "REPO", "class_name": "RepoCell"

  • r2 ↗

    "class_name": "Registry" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["RepoCell", "Registry"] } ], // Optional: with PACK_CACHE bound, pushed pack bytes are stored in R2 (off the // DO's SQLite) and full clones are served straight from R2 by the Worker. // Remove this binding to fall back to the SQLite-only path (as on celld). "r2_buckets": [ { "binding": "PACK_CACHE", "bucket_name": "dgit-pack-cache" } ], "vars": { "SITE_NAME": "git.littledivy.com",

  • durable-objects ↗

    xing of a large push is CPU-bound; 5 minutes is the paid-plan max "cpu_ms": 300000 }, "durable_objects": { "bindings": [ { "name": "REPO", "class_name": "RepoCell" }, { "name": "REGISTRY", "class_name": "Registry" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["RepoCell", "Registry"] } ], // Optional: with PACK_CACHE bound, pushed pack bytes are stored in R2 (off the // DO's SQLite) and full clones are served straight from R2 by the Worker. /

  • paid ↗

    script name stays "git-cells" — Durable Object data is bound // to it, and renaming would orphan every hosted repository. "name": "git-cells", "main": "src/index.ts", "compatibility_date": "2026-08-01", "routes": [ { "pattern": "git.littledivy.com", "custom_domain": true } ], "limits": { // pack indexing of a large push is CPU-bound; 5 minutes is the paid-plan max "cpu_ms": 300000 }, "durable_objects": { "bindings": [ { "name": "REPO", "class_name": "RepoCell"

  • paid ↗

    "class_name": "Registry" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["RepoCell", "Registry"] } ], // Optional: with PACK_CACHE bound, pushed pack bytes are stored in R2 (off the // DO's SQLite) and full clones are served straight from R2 by the Worker. // Remove this binding to fall back to the SQLite-only path (as on celld). "r2_buckets": [ { "binding": "PACK_CACHE", "bucket_name": "dgit-pack-cache" } ], "vars": { "SITE_NAME": "git.littledivy.com",

  • paid ↗

    xing of a large push is CPU-bound; 5 minutes is the paid-plan max "cpu_ms": 300000 }, "durable_objects": { "bindings": [ { "name": "REPO", "class_name": "RepoCell" }, { "name": "REGISTRY", "class_name": "Registry" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["RepoCell", "Registry"] } ], // Optional: with PACK_CACHE bound, pushed pack bytes are stored in R2 (off the // DO's SQLite) and full clones are served straight from R2 by the Worker. /

  • paid ↗

    up>1, 2, 3, 4</sup> | Duration | CPU time | | --- | --- | --- | --- | | **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation | | **Standard** | 10 million included per month <br> +$0.30 per additional million | No charge or limit for duration | 30 million CPU milliseconds included per month<br> +$0.02 per additional million CPU milliseconds<br><br> Max of [5 minutes of CPU time](https://developers.cloudflare.com/workers/platform/limits/#account-plan-limits) per invocation (default: 30 seconds)<br> Max of 15 minutes of CPU time per [Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/) or [Queue Consumer](https://developers.cloudflare.co

  • paid ↗

    infrequent access storage) for 1.1 GB, you will be billed for 2 GB. ### Free tier You can use the following amount of storage and operations each month for free. | | Free | | --- | --- | | Storage | 10 GB-month / month | | Class A Operations | 1 million requests / month | | Class B Operations | 10 million requests / month | | Egress (data transfer to Internet) | Free <sup>[1](#user-content-fn-1)</sup> | Caution The free tier only applies to Standard storage, and does not apply to Infrequent Access storage. ### Storage usage Storage is billed using gigabyte-month (GB-month) as the billing metric. A GB-month is calculated by averaging the *peak* storage per day over a billing period (30 days). For examp

  • paid ↗

    jects are available both on Workers Free and Workers Paid plans. - **Workers Free plan**: Only Durable Objects with [SQLite storage backend](https://developers.cloudflare.com/durable-objects/best-practices/access-durable-objects-storage/#create-sqlite-backed-durable-object-class) are available. - **Workers Paid plan**: Durable Objects with the SQLite storage backend are available. The [key-value storage backend](https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/#storage-backends) is only available to accounts that already have a key-value-backed namespace. If you wish to downgrade from a Workers Paid plan to a Workers Free plan, you must first ensure that you have deleted all Durable Object namespaces with the key-value storage backend. On Workers Free plan: - If you exceed any one of the free tier limits, further operations of that type will fail with an error. - Daily free limits reset at 00:00 UTC. ## Compute billing Durable Objects are billed for compute duration (wall-clock time) while the Durable Object is actively running or is idle in memory but unable to [hibernate](https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/). Durable Objects that are idle and eligible for hibernation are not billed for duration, even before the runtime has hibernated them. Requests to a D

  • paid ↗

    billed accordingly. | | Free plan | Paid plan | | --- | --- | --- | | Requests | 100,000 / day | 1 million / month, + $0.15/million<br> Includes HTTP requests, RPC sessions<sup>1</sup>, WebSocket messages<sup>2</sup>, and alarm invocations | | Duration<sup>3</sup> | 13,000 GB-s / day | 400,000 GB-s / month, + $12.50/million GB-s<sup>4,5</sup> | <details> <summary> Footnotes </summary> <sup>1</sup> Each <a href="https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/">RPC session</a> is billed as one request to your Durable Object. Every <a href="https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/">RPC method call</a> on a <a href="https://developers.cloudflare.com/durable-objects/">Durable Objects stub</a> is its own RPC session and therefore a single billed request. RPC method calls can return objects (stubs) extending <a href="https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/#lifetimes-memory-and-resource-management"><code>RpcTarget</code></a> and invo

  • paid ↗

    /). | | Workers Free plan | Workers Paid plan | | --- | --- | --- | | Rows reads <sup>1,2</sup> | 5 million / day | First 25 billion / month included + $0.001 / million rows | | Rows written <sup>1,2,3,4</sup> | 100,000 / day | First 50 million / month included + $1.00 / million rows | | SQL Stored data <sup>5</sup> | 5 GB (total) | 5 GB-month, + $0.20/ GB-month | <details> <summary> Footnotes </summary> <sup>1</sup> Rows read and rows written included limits and rates match <a href="https://developers.cloudflare.com/d1/platform/pricing/">D1 pricing</a>, Cloudflare's serverless SQL database. <sup>2</sup> Key-value methods like <code>get()</code>, <code>put()</code>, <code>delete()</code>, or <code>list(

  • paid ↗

    es Functions, Workers KV, Hyperdrive, and Durable Objects usage for a minimum charge of $5 USD per month for an account. The plan includes increased initial usage allotments, with clear charges for usage that exceeds the base plan. There are no additional charges for data transfer (egress) or throughput (bandwidth). All included usage is on a monthly basis. Pages Functions billing All [Pages Functions](https://developers.cloudflare.com/pages/functions/) are billed as Workers. All pricing and inclusions in this document apply to Pages Functions. Refer to [Functions Pricing](https://developers.cloudflare.com/pages/functions/pricing/) for more information on Pages Functions pricing. ## Workers Users on the Wo

  • MIT ↗

    MIT License Copyright (c) 2026 Divy Srivastava Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONI

  • architecture ↗

    script name stays "git-cells" — Durable Object data is bound // to it, and renaming would orphan every hosted repository. "name": "git-cells", "main": "src/index.ts", "compatibility_date": "2026-08-01", "routes": [ { "pattern": "git.littledivy.com", "custom_domain": true } ], "limits": { // pack indexing of a large push is CPU-bound; 5 minutes is the paid-plan max "cpu_ms": 300000 }, "durable_objects": { "bindings": [ { "name": "REPO", "class_name": "RepoCell"

  • architecture ↗

    "class_name": "Registry" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["RepoCell", "Registry"] } ], // Optional: with PACK_CACHE bound, pushed pack bytes are stored in R2 (off the // DO's SQLite) and full clones are served straight from R2 by the Worker. // Remove this binding to fall back to the SQLite-only path (as on celld). "r2_buckets": [ { "binding": "PACK_CACHE", "bucket_name": "dgit-pack-cache" } ], "vars": { "SITE_NAME": "git.littledivy.com",

  • architecture ↗

    xing of a large push is CPU-bound; 5 minutes is the paid-plan max "cpu_ms": 300000 }, "durable_objects": { "bindings": [ { "name": "REPO", "class_name": "RepoCell" }, { "name": "REGISTRY", "class_name": "Registry" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["RepoCell", "Registry"] } ], // Optional: with PACK_CACHE bound, pushed pack bytes are stored in R2 (off the // DO's SQLite) and full clones are served straight from R2 by the Worker. /

What it can replace

Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.

GitHub logoGitHub ↗

Git repository hosting over Smart HTTP with a web history browser; issues, pull requests, CI runners and full software-forge features are excluded.

See supporting source ↗
GitLab logoGitLab ↗

Git repository hosting over Smart HTTP with a web history browser; issues, pull requests, CI runners and full software-forge features are excluded.

See supporting source ↗
external SaaS target
varies
external SaaS target
varies

How it works

The shape of dgit on Cloudflare, and how it stacks up against the rented tools it replaces.

Architecture

Diagram of deployment declarations at the reviewed commit. Each app has its own entrypoint; declared resources do not prove runtime calls. Follow file and line sources below.

View upstream source ↗
Public interface
Configured entry points2
git-cells
wrangler.jsonc
git-cells
wrangler.celld.jsonc
↓
App
git-cells
entry
Cloudflare Workers
Entrypoint: src/index.ts
git-cells
entry
Cloudflare Workers
Entrypoint: src/index.ts
↓

Configuration and workflow sources

Reviewed commit d5eba04eda58. Files were read as data; upstream applications and CI jobs were not executed.

Deployment configuration · 2 files
wrangler.jsonc ↗

Cloudflare Workers · compatibility 2026-08-01

git-cells · default

Entrypoint: src/index.ts

Configured route patterns: git.littledivy.com

  • PACK_CACHE → R2
  • REPO → Durable Objects · class RepoCell
  • REGISTRY → Durable Objects · class Registry
wrangler.celld.jsonc ↗

Cloudflare Workers · compatibility 2026-08-01

git-cells · default

Entrypoint: src/index.ts

  • REPO → Durable Objects · class RepoCell
  • REGISTRY → Durable Objects · class Registry

Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.

Runtime source · handlers, binding usage and workflow steps

Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.

src/mod.ts ↗
  • L101 · tokens calls (conditional paths may differ): split, filter, list.map, t.trim
  • L113 · tooManyRequests calls (conditional paths may differ): String, Math.ceil
  • L120 · digest calls (conditional paths may differ): crypto.subtle.digest, encode
  • L125 · digestsEqual calls (conditional paths may differ): Math.max
  • L135 · secretsEqual calls (conditional paths may differ): digestsEqual, digest
  • L139 · clientIp calls (conditional paths may differ): req.headers.get
  • L147 · parseBasic calls (conditional paths may differ): req.headers.get, header.startsWith, atob, header.slice, decoded.indexOf, decoded.slice
  • L165 · pruneAuthFailures calls (conditional paths may differ): authFailures.delete, next, authFailures.keys
  • L177 · rateLimited calls (conditional paths may differ): authFailures.get, Date.now
  • L183 · recordAuthFailure calls (conditional paths may differ): Date.now, authFailures.get, authFailures.set, pruneAuthFailures
  • L195 · tokenAuthorize calls (conditional paths may differ): tokens, ctx.request.headers.get, header.startsWith, unauthorized, clientIp, rateLimited, tooManyRequests, recordAuthFailure, digest, digestsEqual
  • L235 · indexPage calls (conditional paths may differ): env.REGISTRY.getByName, registry.list, repoUrl, esc, age, Math.floor, htmlResponse, layout, siteBase
  • L267 · pruneInfoMemo calls (conditional paths may differ): next, infoMemo.keys, infoMemo.delete
  • L275 · repoInfo calls (conditional paths may differ): infoMemo.get, Date.now, get, env.REGISTRY.getByName, infoMemo.set, pruneInfoMemo
  • L307 · serveCloneFromR2 calls (conditional paths may differ): parseUploadRequest, req.wants.every, req.caps.has, stub.currentPackKey, bucket.get, parseInt, Number.isFinite, streamingCloneResponse
  • L344 · withCors calls (conditional paths may differ): res.headers.set
  • L361 · parseRefUpdates calls (conditional paths may differ): res.headers.get, JSON.parse, decodeURIComponent, Array.isArray
  • L383 · repoPath calls (conditional paths may differ): pattern.exec, decodeURIComponent, REPO_NAME.test, segs.push, RESERVED.has, segs.join
  • L408 · classify calls (conditional paths may differ): url.searchParams.get, sub.startsWith
  • L427 · createDurableGit calls (conditional paths may differ): path.startsWith, corsPreflight, withCors, Response.json, url.searchParams.get, list, env.REGISTRY.getByName, json, repos.map, pageCache, cache.match, indexPage, res.clone, toCache.headers.set, ctx.waitUntil, cache.put, repoPath, errorPage, siteBase, classify

Environment references: env.GIT_TOKEN · env.GIT_TOKENS · env.SITE_NAME · env.SITE_DESC · env.REGISTRY · env.SITE_OWNER · env.PACK_CACHE · env.REPO

src/ui/html.ts ↗
  • L5 · esc calls (conditional paths may differ): s.replace
  • L10 · repoUrl calls (conditional paths may differ): join, map, repo.split
  • L15 · repoBase calls (conditional paths may differ): repo.slice, repo.lastIndexOf
  • L20 · age calls (conditional paths may differ): Math.max, Math.floor, Date.now, unit.endsWith, fmt
  • L33 · fmtDate calls (conditional paths may differ): padStart, String, d.getUTCFullYear, pad, d.getUTCMonth, d.getUTCDate, d.getUTCHours, d.getUTCMinutes, d.getUTCSeconds
  • L46 · fmtDate2822 calls (conditional paths may differ): parseInt, tz.slice, padStart, String, d.getUTCDay, d.getUTCDate, d.getUTCMonth, d.getUTCFullYear, pad, d.getUTCHours, d.getUTCMinutes, d.getUTCSeconds
  • L79 · layout calls (conditional paths may differ): repoUrl, encodeURIComponent, join, tabs.map, esc, o.branches.map
  • L155 · errorPage calls (conditional paths may differ): htmlResponse, layout, esc
src/git/protocol.ts ↗
  • L39 · advertisement calls (conditional paths may differ): concat, pkt, join, store.head, store.resolveHead, refs.push, store.refs, lines.push, refs.forEach
  • L85 · advertisedOids calls (conditional paths may differ): store.resolveHead, oids.add, store.refs, r.name.startsWith, store.typeAndSize, peelToCommitOid
  • L106 · wantsAreAllTips calls (conditional paths may differ): store.refs, tips.add, wantSet.has
  • L116 · sidebandFrames calls (conditional paths may differ): payload.subarray, framed.set, frames.push, pkt
  • L137 · streamingCloneResponse calls (conditional paths may differ): body.getReader, ctrl.enqueue, pkt, sidebandFrames, te.encode, reader.read, ctrl.close, ctrl.error, reader.cancel
  • L189 · parseUploadRequest calls (conditional paths may differ): parser.read, req.caps.add, line.startsWith, req.wants.push, line.slice, split, trim, req.haves.push, req.clientShallows.push, parseInt
  • L224 · lsRefs calls (conditional paths may differ): store.resolveHead, lines.push, pkt, req.caps.has, store.head, store.refs, ref.name.startsWith, store.typeAndSize, peelToCommitOid, concat
  • L240 · peelToCommitOid calls (conditional paths may differ): store.get, parseTag
  • L260 · latestCommitTime calls (conditional paths may differ): store.resolveHead, tips.add, store.refs, peelToCommitOid, store.get, parseCommit, Date.now
  • L285 · computeDepthSet calls (conditional paths may differ): peelToCommitOid, commits.has, commits.add, queue.push, queue.shift, store.get, parseCommit, boundary.add, boundary.delete, store.has
  • L332 · interestingCommits calls (conditional paths may differ): peelToCommitOid, haveCommits.has, set.addHex, stack.push, stack.pop, store.get, yieldMaybe, parseCommit, store.has
  • L374 · excludedObjects calls (conditional paths may differ): Math.max, setTimeout, store.has, peelToCommitOid, haveCommits.add, interestingCommits, excluded.addHex, commitStack.push, commitStack.pop, store.get, yieldMaybe, parseTag, parseCommit, shallowStops.has, boundary.push, interesting.atHex, excluded.hasHex, trees.push, trees.pop, parseTree
  • L477 · collectPackOids calls (conditional paths may differ): walk.addHex, walk.atHex, setTimeout, store.typeAndSize, commitLimit.has, excluded.hasHex, store.get, parseCommit, walk.markHex, childOids
  • L519 · uploadPack calls (conditional paths may differ): parseUploadRequest, release, lsRefs, req.wants.some, isOid, pkt, advertisedOids, allowed.has, store.has, preamble.push, computeDepthSet, clientShallowSet.has, commits.has, boundary.has, te.encode, req.haves.filter, common.map, req.caps.has, concat, wantsAreAllTips
  • L908 · warmFullClone calls (conditional paths may differ): store.reachableCount, store.reachablePackedAfter, store.reachableHas, w.rawDelta, store.packs.readRaw, store.get, w.object, w.rawFull, store.reachableLooseAfter, objects, concat, pending.push, sidebandFrames, packCache.beginMultipart, capture.push, buffered.push, flushBuffered, te.encode, writer.header, ctrl.enqueue
  • L926 · objects calls (conditional paths may differ): store.reachablePackedAfter, store.reachableHas, w.rawDelta, store.packs.readRaw, store.get, w.object, w.rawFull, store.reachableLooseAfter
  • L1055 · isAncestor calls (conditional paths may differ): store.get, stack.pop, parseCommit, seen.has, seen.add, stack.push
  • L1087 · parsePushCommands calls (conditional paths may differ): parser.read, line.indexOf, split, trim, line.slice, line.match, commands.push
  • L1118 · reachableComplete calls (conditional paths may differ): known.addHex, known.atHex, store.typeAndSize, store.get, childOids
  • L1165 · childOids calls (conditional paths may differ): parseCommit, parseTag, parseTree, isGitlinkMode, out.push
  • L1194 · packComplete calls (conditional paths may differ): store.resetPushEdges, store.addPushEdges, store.firstMissingPushEdge, store.packNonBlobOidsAfter, store.get, childOids, pending.push, flush
  • L1225 · validatePush calls (conditional paths may differ): store.getMeta, store.reachableVersion, packComplete, store.refs, known.addHex, peelToCommitOid, store.resolveHead, decisions.push, cmd.ref.startsWith, cmd.ref.includes, BAD_REF.test, cmd.ref.endsWith, some, cmd.ref.split, c.startsWith, store.getRef, reachableComplete, store.has, isAncestor
  • L1298 · commitPush calls (conditional paths may differ): results.push, store.getRef, store.delRef, store.setRef, store.head, filter, store.refs, r.name.startsWith, branches.find, store.setHead, store.extendReachable, store.invalidateReachable
  • L1350 · renderStatus calls (conditional paths may differ): pkt, lines.push, concat, sidebandFrames
src/git/util.ts ↗
  • L4 · concat calls (conditional paths may differ): out.set
  • L16 · toHex calls (conditional paths may differ): padStart, toString
  • L22 · fromHex calls (conditional paths may differ): parseInt, s.slice
  • L30 · isOid calls (conditional paths may differ): test
  • L34 · sha1hex calls (conditional paths may differ): toHex, sha1
src/git/zlib.ts ↗
  • L5 · deflate calls (conditional paths may differ): pako.deflate
  • L9 · inflate calls (conditional paths may differ): pako.inflate
  • L20 · gunzipLimited calls (conditional paths may differ): parts.push, inf.push, concat
src/repo.ts ↗
  • L1924 · overDeclaredLength calls (conditional paths may differ): parseInt, req.headers.get, Number.isFinite
  • L1929 · tooLargeResponse calls (conditional paths may differ): Math.round
  • L1937 · renderHunk calls (conditional paths may differ): esc
  • L1947 · rawBlobResponse calls (conditional paths may differ): toLowerCase, name.slice, name.lastIndexOf, Object.hasOwn, isBinary
  • L1977 · decodePath calls (conditional paths may differ): filter, map, p.split, decodeURIComponent

Environment references: env.PACK_CACHE · env.MAX_PUSH_MB · env.INGEST_CACHE_MB · env.SHA1DC · env.SITE_NAME · env.SITE_DESC

src/git/objects.ts ↗
  • L16 · objectHeader calls (conditional paths may differ): te.encode
  • L20 · hashObject calls (conditional paths may differ): sha1hex, concat, objectHeader
  • L56 · parsePerson calls (conditional paths may differ): line.indexOf, line.slice, split, trim, parseInt, test, Number.isFinite
  • L71 · splitHeaders calls (conditional paths may differ): text.indexOf, text.slice, head.split, line.startsWith, push, line.slice, line.indexOf, headers.push, cont.push, join
  • L98 · parseCommit calls (conditional paths may differ): splitHeaders, td.decode, message.split, isOid, c.parents.push, parsePerson
  • L127 · parseTag calls (conditional paths may differ): splitHeaders, td.decode, isOid, parsePerson
  • L145 · parseTree calls (conditional paths may differ): td.decode, data.subarray, toHex, entries.push
  • L173 · modeString calls (conditional paths may differ): parseInt, isTreeMode, isGitlinkMode, bits
src/git/pktline.ts ↗
  • L6 · pkt calls (conditional paths may differ): te.encode, concat, padStart, toString
  • L12 · pktLines calls (conditional paths may differ): concat, payloads.map
src/git/pack.ts ↗
  • L5 · applyDelta calls (conditional paths may differ): next, varint, out.set, base.subarray, delta.subarray
  • L62 · encodeTypeSizeNum calls (conditional paths may differ): Math.floor, bytes.push
  • L75 · encodeTypeSize calls (conditional paths may differ): encodeTypeSizeNum
src/git/multipart.ts ↗
  • L85 · beginRawMultipart calls (conditional paths may differ): bucket.createMultipartUpload, parts.push, mp.uploadPart, mp.complete, mp.abort
src/git/sha1.ts ↗
  • L116 · sha1 calls (conditional paths may differ): digest, update
  • L192 · recompress calls (conditional paths may differ): scratch.set, stepBackward, ihvin.set, stepForward
src/git/packstore.ts ↗
  • L111 · finishOid calls (conditional paths may differ): toHex, h.digest
  • L124 · hashObjectAsync calls (conditional paths may differ): finishOid, update, scratchSha.reset, objectHeader, buf.set, toHex, crypto.subtle.digest
src/git/diff.ts ↗
  • L20 · diffLines calls (conditional paths may differ): aText.split, bText.split, a.pop, b.pop, ops.push, Math.min, Math.max, Math.floor, trace.push, v.slice, ops.reverse
  • L93 · toHunks calls (conditional paths may differ): ops.forEach, changes.push, changes.slice, ranges.push, Math.max, Math.min, hunk.ops.push, hunks.push
  • L145 · isBinary calls (conditional paths may differ): Math.min
src/git/blame.ts ↗
  • L27 · blame calls (conditional paths may differ): td.decode, tipText.split, tipLines.pop, tipLines.map, diffLines, eqMap.set, eqMap.get
src/git/snapshot.ts ↗
  • L13 · safePath calls (conditional paths may differ): p.startsWith, p.includes, every, p.split
  • L18 · safeTarget calls (conditional paths may differ): t.startsWith, every, t.split
  • L23 · safeEntry calls (conditional paths may differ): safePath, safeTarget, decode
  • L29 · octal calls (conditional paths may differ): padStart, n.toString, te.encode
  • L34 · tarHeader calls (conditional paths may differ): te.encode, lastIndexOf, path.slice, h.set, subarray, octal, padStart, sum.toString
  • L65 · tarGz calls (conditional paths may differ): safeEntry, decode, parts.push, tarHeader, pako.gzip, concat
  • L101 · dosDateTime calls (conditional paths may differ): d.getUTCFullYear, d.getUTCMonth, d.getUTCDate, d.getUTCHours, d.getUTCMinutes, d.getUTCSeconds
  • L108 · zip calls (conditional paths may differ): dosDateTime, safeEntry, te.encode, crc32, pako.deflateRaw, lv.setUint32, lv.setUint16, local.set, parts.push, cv.setUint32, cv.setUint16, cd.set, central.push, ev.setUint32, ev.setUint16, concat
src/ui/markdown.ts ↗
  • L38 · safeUrl calls (conditional paths may differ): url.trim, test, u.startsWith
  • L47 · rewriteUrl calls (conditional paths may differ): safeUrl, test, u.replace, rel.indexOf, rel.slice, encodeURIComponent, join, map, rel.split, filter
  • L67 · rewriteSrcset calls (conditional paths may differ): map, val.split, p.trim, seg.search, seg.slice, rewriteUrl, parts.some, join, parts.filter
  • L81 · parseAttrs calls (conditional paths may differ): re.exec, toLowerCase, ALLOWED_ATTRS.has, val.slice, URL_ATTRS.has, rewriteSrcset, rewriteUrl, out.push, esc, out.join
  • L107 · sanitizeTag calls (conditional paths may differ): raw.match, esc, toLowerCase, ALLOWED_TAGS.has, parseAttrs, VOID_TAGS.has
  • L120 · inline calls (conditional paths may differ): stash.push, text.replace, keep, esc, m.slice, sanitizeTag, s.replace, rewriteUrl
  • L154 · splitRow calls (conditional paths may differ): s.trim, t.startsWith, t.slice, t.endsWith, map, t.split, replace, c.trim
  • L161 · alignOf calls (conditional paths may differ): startsWith, cell.trim, endsWith
  • L170 · renderMarkdown calls (conditional paths may differ): split, src.replaceAll, out.push, inline, para.join, listStack.pop, flushPara, closeLists, closeQuote, raw.match, resetBlocks, test, code.push, esc, code.join, replace, raw.includes, DELIM_RE.test, includes, splitRow
src/ui/highlight.ts ↗
  • L64 · tokenize calls (conditional paths may differ): tokens.push, test, src.startsWith, flushPlain, src.indexOf, src.slice, Math.min, isWord, lang.keywords.has
  • L158 · highlightLines calls (conditional paths may differ): filename.toLowerCase, base.includes, base.slice, base.lastIndexOf, src.split, plainLines.pop, plainLines.map, esc, tokenize, t.text.split, lines.push, lines.pop, lines.slice
Build and deployment pipeline · 1 GitHub Actions workflows

Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.

publish · .github/workflows/publish.yml ↗

Triggers: push

publish · no job dependencies declared

  1. actions/checkout@v4actions/checkout@v4
  2. actions/setup-node@v4actions/setup-node@v4
  3. Shell commandnpm ci
  4. Shell commandnpm run typecheck
  5. tag matches package.jsontest "v$(node -p "require('./package.json').version")" = "$GITHUB_REF_NAME"
  6. publish, unless the version is already on the registryv=$(node -p "require('./package.json').version") if npm view "durable-git@$v" version >/dev/null 2>&1; then echo "durable-git@$v already published, skipping" else npm publish fi
package.json ↗
  • deploy: wrangler deploy

Full upstream document by @littledivy · README.md · snapshot d5eba04

dgit

Durable git.

dgit is a git server for Cloudflare Workers and for your own machines with celld. Each repository is a Durable Object: a small server with a name and a private SQLite database that holds the repository's objects and refs, speaks the git smart HTTP protocol to a stock git client, and renders a cgit-style web interface. There is no origin server, no filesystem, and no GitHub in the critical path. A repository nobody touches costs almost nothing, and applications shard by construction: one hot repository cannot slow another. Reads are public; pushes authenticate; pushing to a name that does not exist creates the repository.

How it works

dgit implements git in TypeScript: pkt-line framing, packfile parsing with ofs- and ref-delta resolution, pack generation over a streaming SHA-1, commit/tree/tag codecs, and a Myers diff. The one dependency is pako, for zlib.

A push streams into the repository's cell and is stored as the packfile the client sent; an index maps each object id to its pack, offset, and delta base, so the client's compression is preserved rather than re-derived. When an R2 bucket is bound the pack bytes are written to R2 and only the index stays in the cell's SQLite, so pack storage is no longer capped by the per-cell database. A clone walks the closure of the requested refs and copies the stored compressed bytes verbatim into the outgoing pack; a full clone, once built, is cached in R2 and afterwards streamed straight from the Worker, so repeat clones never load the cell. Fetch negotiation excludes the closure of the client's haves, cut correctly at shallow boundaries, so an incremental fetch downloads only what is missing. Shallow clones (--depth, deepening, --unshallow), thin packs, side-band progress, forced updates, and ref deletion behave as they do against any git server.

The web interface is the cgit surface: summary, refs, log with search and per-path history, tree, blob with syntax highlighting, blame, commit and arbitrary-range diffs, format-patch output that applies cleanly with git am, tar.gz and zip snapshots of any ref, about pages rendered from the README, atom feeds, and commit-activity statistics. Repositories carry a description, an owner, a section on the index page, and a private flag that hides them and gates every read behind the push token.

Deploy to Cloudflare

npm install
npx wrangler r2 bucket create dgit-pack-cache   # optional: R2 pack offload + clone cache
npx wrangler deploy
npx wrangler secret put GIT_TOKEN   # the push password

The PACK_CACHE R2 binding in wrangler.jsonc is optional: with it, pushed pack bytes live in R2 (off the cell's SQLite) and full clones are served straight from R2 by the Worker without loading the cell. Remove the binding and everything falls back to the SQLite-only path.

Then push anything:

git remote add origin https://<your-host>/myrepo.git
git push -u origin main

A Workers request is bounded at 128MB of memory and five minutes of CPU, so a very large history lands as a series of smaller pushes rather than one; day-to-day pushes, clones, and fetches fit comfortably. Building a full-history clone of a repository with millions of objects can exceed the CPU bound the first time — once such a clone is cached in R2 it streams from the Worker without rebuilding, and shallow and incremental fetches of the same repository are fine regardless. The largest repositories belong on celld.

Self-host on celld

celld runs the same Worker against a bucket you own, with none of the managed platform's request bounds. Set a real GIT_TOKEN var in wrangler.celld.jsonc first:

celld deploy wrangler.celld.jsonc --bucket s3://my-cells --endpoint https://...
CELLD_V8_HEAP_LIMIT_MB=4096 CELLD_LTX_DURABILITY_TIMEOUT_SECS=180 \
celld --bucket s3://my-cells --endpoint https://... \
  --listen 0.0.0.0:8080 --internal-listen 10.0.0.1:8081 --advertise 10.0.0.1:8081

Each repository's SQLite database replicates to the bucket; nodes are disposable, and a killed node's repositories come back bit-identical. The heap and durability-deadline variables give large single-cell ingests the room the defaults do not.

Build on dgit

dgit is also a library, published as durable-git. The deployed Worker above is three lines around it:

import { createDurableGit, secretsEqual } from "durable-git";
export { RepoCell, Registry } from "durable-git";

export default createDurableGit({
  async authorize({ repo, op, private: priv, credentials, env }) {
    if (op === "read" && !priv) return true;
    return secretsEqual(credentials?.pass ?? "", await lookupDeployKey(env, repo));
  },
  onPush(event, env) {
    return fetch("https://ci.example.com/hook", { method: "POST", body: JSON.stringify(event) });
  },
});

The package ships TypeScript source, which wrangler bundles directly; bind the Durable Object classes as wrangler.jsonc does and re-export them from your entry module.

authorize gates every repository-scoped request and replaces the stock GIT_TOKEN policy entirely (the private flag is yours to consult); onPush fires with the ref updates a push applied. Every repository also serves a JSON content API, gated like its pages, and the same surface as typed RPC on the cell. ui: false runs it headless behind your own frontend, cors opens the API cross-origin, and namespaces: true routes GitHub-style owner/name repositories. See docs at docs/api.md.

Operate

curl -X PUT  -u x:$GIT_TOKEN -d '{"description":"...","section":"tools","private":false}' \
  https://<host>/myrepo/config                       # describe and place a repository
curl -X POST -u x:$GIT_TOKEN https://<host>/myrepo/gc    # prune unreachable objects
curl -X DELETE -u x:$GIT_TOKEN https://<host>/myrepo     # delete a repository

Garbage collection also runs by itself, from a Durable Object alarm, after a forced update or a ref deletion. GIT_TOKENS holds additional comma-separated tokens; MAX_PUSH_MB caps a single push. Setting SHA1DC=1 screens every pushed object for a SHA-1 collision attack on ingest; by default objects are hashed with native SHA-1 — the same object ids, without the check.

Contributions

Pull requests are disabled. Send a git format-patch attachment to me@littledivy.com.

Frequently asked about dgit

What is dgit?+

dgit is a self-hosted GitHub/GitLab alternative built on the Cloudflare developer platform. Host Git repositories and browse their history on Workers and SQLite Durable Objects.

What does dgit replace?+

dgit is listed as an alternative to GitHub, GitLab. Compare the features and tradeoffs before migrating.

What Cloudflare primitives does dgit use?+

dgit is built on Durable Objects, R2, Workers.

How much does dgit cost to run?+

dgit has a documented low-volume paid Cloudflare path beginning with Workers Paid ($5 USD/account/month), with separately metered usage. The supplied high CPU limit requires Paid; repository sizes and active Durable Object duration drive costs. Use Workers Paid for the configured 300,000 ms CPU limit; $5 USD/account/month is a baseline, not a fixed all-inclusive price. Workers Paid starts at $5 USD/account/month and includes 10 million requests/month plus 30 million CPU milliseconds/month; higher CPU limits do not make execution unmetered. Use R2 Standard storage, at most 10 GB-month, 1 million Class A operations and 10 million Class B operations/month; provision an eligible billing-enabled R2 account. Paid SQLite Durable Objects include 1 million requests/month, 400,000 GB-s duration/month and 5 GB-month SQL storage; active duration and excess database operations are metered. Use a small personal or team workload; domain registration and optional third-party providers are separate costs. Provision your own IDs, secrets and migrations. Check current Cloudflare pricing before deploying.

Is dgit open source?+

The upstream repository declares the MIT license. Read its terms at https://raw.githubusercontent.com/littledivy/durable-git/d5eba04eda584b855a1bde1f86a836b751c378c0/LICENSE. Source code and contributor credit are available at https://github.com/littledivy/durable-git.

Discussion · 0

sign in to comment →
No comments yet — be the first.