Part 1: Push your first repository
Start with a Git remote you can push to. This part deploys a Worker backed by Durable Objects and R2, then sends a real commit through it and clones it back.
Create the project
Section titled “Create the project”You need Bun or Node.js 22+, Git, and a Cloudflare account with Workers, Durable Objects, and R2. Complete Cloudflare setup to connect your account.
mkdir git-tutorialcd git-tutorialbun init -ybun add alchemy effectmkdir srcmkdir git-tutorialcd git-tutorialnpm init -ynpm install alchemy effectmkdir srcmkdir git-tutorialcd git-tutorialpnpm initpnpm add alchemy effectmkdir srcmkdir git-tutorialcd git-tutorialyarn init -yyarn add alchemy effectmkdir srcRun the remaining commands from this project directory. Keep the same directory, stack name, and deployment stage throughout the tutorial so each deploy updates this host.
Add the Git routes
Section titled “Add the Git routes”Create src/git.ts:
import * as Git from "alchemy/Git";import * as Layer from "effect/Layer";
export const GitLive = Git.ApiLive.pipe( Layer.provide(Git.ApiHandlersLive),);Git.ApiLive registers the Git HTTP endpoints, including the protocol used by
git push and git clone. Git.ApiHandlersLive implements those endpoints.
Its remaining dependencies describe where repositories live and how incoming
objects are checked. Supply each one below before deploying.
Store repository data
Section titled “Store repository data”export const GitLive = Git.ApiLive.pipe( Layer.provide(Git.ApiHandlersLive), Layer.provide(Git.ReposDurableObject),);Git.ReposDurableObject gives each repository a Durable Object. It stores the
repository’s refs and object metadata and coordinates writes to that repository.
Look up repositories by name
Section titled “Look up repositories by name” Layer.provide(Git.ReposDurableObject), Layer.provide(Git.RegistryDurableObject),);The registry maps an owner/name, such as acme/web, to its repository. This
lets an incoming request find the right Durable Object.
Verify incoming Git objects
Section titled “Verify incoming Git objects” Layer.provide(Git.RegistryDurableObject), Layer.provide(Git.HasherInline),);Git objects are identified by hashes of their contents. Git.HasherInline
verifies incoming objects in the Worker during a push.
Store packs and large objects
Section titled “Store packs and large objects”import * as Cloudflare from "alchemy/Cloudflare";import * as Git from "alchemy/Git";import * as Layer from "effect/Layer";
const GitObjects = Cloudflare.R2.Bucket("GitObjects", { forceDestroy: true });
export const GitLive = Git.ApiLive.pipe(This bucket holds packs, clone bundles, and large objects. forceDestroy: true
lets you remove the tutorial stack even when the bucket contains data; destroying
this stack also deletes those objects.
Connect the bucket to Git:
Layer.provide(Git.HasherInline), Layer.provide(Git.BlobStoreR2(GitObjects)),);The R2 layer supplies the blob-storage operations used by the Worker and the repository Durable Objects.
Serve Git from a Worker
Section titled “Serve Git from a Worker”Create src/host.ts:
import * as Cloudflare from "alchemy/Cloudflare";import * as Git from "alchemy/Git";import * as Http from "alchemy/Http";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as HttpRouter from "effect/unstable/http/HttpRouter";import { GitLive } from "./git.ts";
export default class GitHost extends Cloudflare.Worker<GitHost>()( "GitHost", { main: import.meta.url, ...Git.GIT_WORKER_OPTIONS }, Effect.gen(function* () { const fetch = yield* HttpRouter.toHttpEffect( GitLive.pipe(Layer.provide(Http.Platform)), ); return { fetch }; }),) {}HttpRouter.toHttpEffect turns the route layer into the Worker’s fetch handler.
Http.Platform provides its HTTP platform dependencies. GIT_WORKER_OPTIONS
sets the Worker compatibility flags and CPU limit used by Git.
Declare the stack
Section titled “Declare the stack”Create alchemy.run.ts:
import * as Alchemy from "alchemy";import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import GitHost from "./src/host.ts";
export default Alchemy.Stack( "GitTutorial", { providers: Cloudflare.providers(), state: Cloudflare.state() }, Effect.gen(function* () { const host = yield* GitHost; return { url: host.url.as<string>() }; }),);Yielding the Worker includes its bucket and Durable Object bindings in the stack. The output gives you the URL clients will use.
Deploy
Section titled “Deploy”bun alchemy deploynpx alchemy deploypnpm alchemy deployyarn alchemy deployCopy the printed URL, including https://, without a trailing slash:
export HOST="https://your-worker.workers.dev"Verify that the Git API is reachable:
curl --fail-with-body "$HOST/api/v1/repos"It returns a JSON repository list. On a new host the list is empty. A new
workers.dev deployment can briefly return a Cloudflare 404 while it propagates;
repeat this read check until the API responds before creating the repository.
Updates in later parts can also briefly serve the previous version.
Create a repository
Section titled “Create a repository”curl --fail-with-body -X POST "$HOST/api/v1/repos" \ -H "Content-Type: application/json" \ -d '{"owner":"acme","name":"web"}'The repository is named acme/web. At this stage acme is just a namespace;
there are no user accounts. Creating a repository reserves that name before you
push any commits.
Push a commit
Section titled “Push a commit”git init -b main workgit -C work config user.name "Tutorial"git -C work config user.email "tutorial@example.com"printf '# My repository\n' > work/README.mdgit -C work add README.mdgit -C work commit -m "First commit"git -C work remote add origin "$HOST/acme/web.git"git -C work push -u origin mainThe push uploads the commit and its objects, then creates main on your host.
Clone it back
Section titled “Clone it back”git clone "$HOST/acme/web.git" verifygit -C verify fsck --strictgit -C work rev-parse HEADgit -C verify rev-parse HEADThe two commit IDs should match, and fsck should report no corrupt or missing
objects. You now have a working Git remote.
Continue to Part 2: Control access. If you stop here, remove the open learning deployment:
bun alchemy destroynpx alchemy destroypnpm alchemy destroyyarn alchemy destroy