Getting Started
For a step-by-step explanation, start with Push your first repository. This page provides a complete host protected by one shared credential.
You need a Cloudflare account with Workers, Durable Objects, and R2 enabled. Cloudflare setup connects one.
Install
Section titled “Install”bun add alchemynpm install alchemypnpm add alchemyyarn add alchemyDefine the host
Section titled “Define the host”import * as Alchemy from "alchemy";import * as Cloudflare from "alchemy/Cloudflare";import * as Git from "alchemy/Git";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as HttpRouter from "effect/unstable/http/HttpRouter";import * as Http from "alchemy/Http";import * as Redacted from "effect/Redacted";import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder";import * as HttpApiSecurity from "effect/unstable/httpapi/HttpApiSecurity";
export const GitObjects = Cloudflare.R2.Bucket("GitObjects");export const GitSecret = Effect.gen(function* () { const Random = yield* Alchemy.Random; return yield* Random("GitSecret");});
/** One shared secret, sent as the password of HTTP Basic. */export const Authentication = HttpRouter.middleware( Effect.gen(function* () { const secret = yield* (yield* GitSecret).text; return (httpEffect) => Effect.gen(function* () { const { password } = yield* HttpApiBuilder.securityDecode(HttpApiSecurity.basic); if (Redacted.value(password) === Redacted.value(yield* secret)) { return yield* httpEffect; } return HttpServerResponse.empty({ status: 401, headers: { "www-authenticate": 'Basic realm="git"' }, }); }).pipe(Effect.provide(Alchemy.RuntimeContext.phantom)); }),);
const GitLive = Layer.mergeAll( Git.ApiLive.pipe(Layer.provide(Authentication.layer)), Git.InternalApiLive,).pipe( Layer.provide(Git.ApiHandlersLive), Layer.provide(Git.ReposDurableObject), Layer.provide(Git.RegistryDurableObject), Layer.provide(Git.HasherInline), Layer.provide(Git.BlobStoreR2(GitObjects)), Layer.provide(Http.Platform),);
export default class GitHost extends Cloudflare.Worker<GitHost>()( "Git", { main: import.meta.url, ...Git.GIT_WORKER_OPTIONS }, Effect.gen(function* () { const fetch = yield* HttpRouter.toHttpEffect(GitLive); return { fetch }; }),) {}Git.ApiLive registers the public Git routes on the application’s router.
Git.ApiHandlersLive supplies their shared implementation. Git.InternalApiLive
registers the internal hash route outside application authentication.
Your application chooses its server, middleware, platform, and CORS policy.
The engine holds no users and no credentials. Who may call a route is
decided by the middleware applied to its route layer, and Git.ApiLive
registers every git route: the REST plane, the git wire, the raw reads,
and the GitHub facade. The smallest thing that secures a fresh host is
one shared secret. A request that presents it may do anything; any
other request is refused with a 401 that makes git ask for a
password. The secret is an Alchemy.Random, minted on the first deploy
and stable after it. GIT_WORKER_OPTIONS sets the compatibility flags
and the CPU limit a push needs.
Add the host to your stack. The Random is a resource like any other,
so the stack can output its value:
import * as Alchemy from "alchemy";import * as Cloudflare from "alchemy/Cloudflare";import * as Output from "alchemy/Output";import * as Effect from "effect/Effect";import * as Redacted from "effect/Redacted";import GitHost, { GitSecret } from "./src/git.ts";
export default Alchemy.Stack( "GitService", { providers: Cloudflare.providers(), state: Cloudflare.state() }, Effect.gen(function* () { const git = yield* GitHost; const secret = yield* GitSecret; return { url: git.url.as<string>(), secret: Output.map(secret.text, Redacted.value), }; }),);Deploy
Section titled “Deploy”bun alchemy deploynpm run alchemy deploypnpm alchemy deployyarn alchemy deployThe stack prints the Worker’s URL and the secret. The rest of this page
calls them $HOST and $GIT_SECRET:
export HOST=...export GIT_SECRET=...Create a repository
Section titled “Create a repository”Repositories are named owner/name. The owner is a name you choose;
with one shared secret there are no users behind it:
curl -u "x:$GIT_SECRET" -X POST "$HOST/api/v1/repos" \ -H "Content-Type: application/json" \ -d '{"owner":"acme","name":"web"}'Use the full $HOST URL from the deployment output. When Git prompts, enter
x as the username and $GIT_SECRET as the password:
git remote add origin "$HOST/acme/web.git"git -c credential.helper= push -u origin mainClone it back to prove the round trip:
git -c credential.helper= clone "$HOST/acme/web.git" verifygit -C verify fsck --strictContinue with the tutorial
Section titled “Continue with the tutorial”The tutorial introduces one behavior per part. Each part includes a deploy and commands that verify what changed:
- Push your first repository — deploy Git, push a commit, and clone it back.
- Control access — require a shared credential and verify anonymous requests fail.
- Publish a repository — allow anonymous clones while keeping writes protected.
- Give users their own credentials — use accounts and individual API keys.
- Add your application’s API — serve
/mebeside Git with the same authenticated user. - Protect a branch — reject deletion of
mainwith a policy in your HTTP handler.
Building blocks is the reference for each line of
GitLive, and Recipes covers S3 bytes, Lambda hashing,
and how the host scales.