1---
2title: Store Git Repos with Artifacts
3description: Use Cloudflare Artifacts to create Git repos at runtime and hand out clone tokens, all from a tiny Worker.
4sidebar:
5 order: 15
6---
7
8**Cloudflare Artifacts** is a Git server you can drive from a
9Worker — create repos, then `git clone`/`push`/`pull` against them.
10This tutorial keeps it deliberately small: one Worker that creates a
11repo and hands back a clone URL plus a token.
12
13## Declare the namespace
14
15An Artifacts namespace is the container for your repos. It's
16*implicit* — nothing to provision — so the resource is just a
17binding marker. Repos are created at runtime through the binding.
18
19```typescript
20// src/Repos.ts
21import * as Cloudflare from "alchemy/Cloudflare";
22
23export const Repos = Cloudflare.Artifacts.Namespace("Repos");
24```
25
26## Create a repo from the Worker
27
28Bind the namespace in the Worker's Construction phase, then call
29`artifacts.create(name)` inside `fetch`. It returns the repo's
30`remote` URL and a clone `token`:
31
32```typescript
33// src/Worker.ts
34import * as Cloudflare from "alchemy/Cloudflare";
35import * as Effect from "effect/Effect";
36import { HttpServerRequest } from "effect/http/HttpServerRequest";
37import * as HttpServerResponse from "effect/http/HttpServerResponse";
38import { Repos } from "./Repos.ts";
39
40export default class Worker extends Cloudflare.Worker<Worker>()(
41 "Api",
42 {
43 main: import.meta.url,
44 compatibility: { flags: ["nodejs_compat"], date: "2026-03-17" },
45 },
46 Effect.gen(function* () {
47 const artifacts = yield* Cloudflare.Artifacts.ReadWriteNamespace(Repos);
48
49 return {
50 fetch: Effect.gen(function* () {
51 const request = yield* HttpServerRequest;
52 const name = new URL(request.url).pathname.slice(1);
53
54 const repo = yield* artifacts.create(name, { setDefaultBranch: "main" });
55
56 return yield* HttpServerResponse.json({
57 remote: repo.remote,
58 token: repo.token,
59 });
60 }).pipe(Effect.orDie),
61 };
62 }).pipe(Effect.provide(Cloudflare.Artifacts.ReadWriteNamespaceBinding)),
63) {}
64```
65
66`Cloudflare.Artifacts.ReadWriteNamespace(Repos)` resolves an Effect-native client
67whose methods (`create`, `get`, `list`, `delete`) each return an
68Effect. `Effect.orDie` turns any `ArtifactsError` into a 500 — fine
69for this demo.
70
71## Mint a fresh clone token
72
73The token from `create` expires. To hand out a new one without
74recreating the repo, look it up with `artifacts.get(name)` and call
75`createToken`:
76
77```diff lang="typescript"
78 fetch: Effect.gen(function* () {
79 const request = yield* HttpServerRequest;
80 const name = new URL(request.url).pathname.slice(1);
81
82+ if (request.method === "GET") {
83+ const repo = yield* artifacts.get(name);
84+ const token = yield* repo.createToken("read", 3600);
85+ return yield* HttpServerResponse.json({ token: token.plaintext });
86+ }
87
88 const repo = yield* artifacts.create(name, { setDefaultBranch: "main" });
89
90 return yield* HttpServerResponse.json({
91 remote: repo.remote,
92 token: repo.token,
93 });
94 }).pipe(Effect.orDie),
95```
96
97`POST /my-repo` creates the repo; `GET /my-repo` mints a fresh,
98short-lived (1 hour) read token for it.
99
100## Test it
101
102Deploy the Stack once and hit both routes over HTTP:
103
104```typescript
105// test/integ.test.ts
106import * as Cloudflare from "alchemy/Cloudflare";
107import * as Test from "alchemy/Test/Bun";
108import { expect } from "bun:test";
109import * as Effect from "effect/Effect";
110import * as HttpClient from "effect/http/HttpClient";
111import Stack from "../alchemy.run.ts";
112
113const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
114 providers: Cloudflare.providers(),
115 state: Cloudflare.state(),
116});
117
118const stack = beforeAll(deploy(Stack));
119afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack));
120
121const repoName = `tutorial-${Date.now().toString(36)}`;
122
123test(
124 "create repo and mint token",
125 Effect.gen(function* () {
126 const { url } = yield* stack;
127 const client = yield* HttpClient.HttpClient;
128
129 const created = yield* (yield* client.post(`${url}/${repoName}`)).json;
130 expect((created as { remote: string }).remote).toBeString();
131
132 const minted = yield* (yield* client.get(`${url}/${repoName}`)).json;
133 expect((minted as { token: string }).token).toBeString();
134 }),
135 { timeout: 120_000 },
136);
137```
138
139```sh
140bun test
141```
142
143Alchemy deploys the Worker, `POST /<name>` creates the repo, and
144`GET /<name>` mints a clone token. With the `remote` and `token` you
145can `git clone` against the repo right away.
146
147## Where to go next
148
149From here you can layer on the things a real Git host needs:
150
151- Front the Worker with a schema-typed [HttpApi](/cloudflare/apis/effect-http-api)
152 so every route is validated end-to-end.
153- Add a [Durable Object](/cloudflare/compute/durable-objects) per
154 repo to hold metadata that doesn't live in Git — description,
155 topics, stars.