1---
2title: Repository
3description: One Durable Object per repository, behind the Git.RepoStore service. Provide it, call it from your own routes, compose operations through Git.Engine, or implement its shape over another store.
4---
5
6The examples use `Authentication` from
7[Getting Started](/git/getting-started): the application's request middleware.
8
9`Git.ReposDurableObject` hosts one Durable Object per repository and
10provides `Git.RepoStore`, the service every route reaches it through:
11
12```typescript
13const GitLive = Git.ApiLive.pipe(
14 Layer.provide(Git.ApiHandlersLive),
15 Layer.provide(Authentication.layer),
16 Layer.provide(Git.ReposDurableObject),
17 // ReposDurableObject requires:
18 Layer.provide(Git.HasherInline), // push verification
19 Layer.provide(Git.BlobStoreR2(GitObjects)), // packs, bundles, spilled pushes
20 Layer.provide(Git.RegistryDurableObject), // owner/name → repoId
21);
22```
23
24The object is keyed by the repository's id, a ULID the
25[Registry](/git/blocks/registry) hands out, so a rename never moves
26data. Its SQLite holds everything that has to be consistent: refs, the
27object index, hot object bytes, the commit graph, and pull requests. A
28push is a compare-and-swap on a ref inside one transaction.
29
30## The service
31
32```typescript
33const repos = yield* Git.RepoStore;
34const repo = repos.getByName(repoId); // a typed RPC stub over the Durable Object
35```
36
37The stub is `Git.GitRepoShape`. It asks no questions about who is
38calling; that was decided in front of the route:
39
40```typescript
41interface GitRepoShape {
42 // lifecycle
43 initRepo(input): Effect<InitRepoResult, StoreError>;
44 startImport(input): Effect<InitRepoResult, StoreError>;
45 startFork(input): Effect<InitRepoResult, StoreError>;
46 startCompact(): Effect<void, RepoNotFound | StoreError>;
47 startPurge(): Effect<void, RepoNotFound | StoreError>;
48
49 // metadata
50 getRepoMeta(): Effect<RepoMetaData, RepoNotFound | StoreError>;
51 readMeta(): Effect<RepoMetaData, RepoNotFound | StoreError>;
52 updateRepoMeta(input): Effect<RepoMetaData, RepoNotFound | RefNotFound | StoreError>;
53
54 // refs
55 listRefs(input): Effect<RefsPage, RepoNotFound | StoreError>;
56 getRef(name): Effect<RefData, RepoNotFound | RefNotFound | StoreError>;
57 updateRef(input): Effect<RefData, RefConflict | ObjectNotFound | ReadOnlyRepo | …>;
58 removeRef(input): Effect<void, RefNotFound | RefConflict | ReadOnlyRepo | …>;
59
60 // objects
61 readObject(input): Effect<ObjectData, ObjectNotFound | WrongObjectType | …>;
62 readCommitLog(input): Effect<CommitLogPage, RefNotFound | …>;
63 readCommitDiff(input): Effect<CommitDiffData, ObjectNotFound | …>;
64```
65
66Every error is a tagged class the stub reconstructs across the RPC
67boundary, so `Effect.catchTag("RefConflict", …)` works in the Worker.
68
69## Call it from a route of your own
70
71Resolve the id through the registry, then talk to the object. A route
72that answers the default branch's tip:
73
74```typescript
75import * as HttpApi from "effect/http-api/HttpApi";
76import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder";
77import * as HttpApiEndpoint from "effect/http-api/HttpApiEndpoint";
78import * as HttpApiGroup from "effect/http-api/HttpApiGroup";
79
80export const Tip = HttpApiEndpoint.get("tip", "/api/v1/repos/:owner/:repo/tip", {
81 params: Git.RepoPath,
82 success: Git.Ref,
83 error: [Git.RepoNotFound, Git.RefNotFound],
84});
85
86export class AppRoutes extends HttpApiGroup.make("app").add(Tip) {}
87export class AppApi extends HttpApi.make("app").add(AppRoutes) {}
88
89export const AppRoutesLive = HttpApiBuilder.group(AppApi, "app", (h) =>
90 Effect.gen(function* () {
91 const registry = yield* Git.RegistryStore;
92 const repos = yield* Git.RepoStore;
93 return h.handle("tip", Effect.fn(function* ({ params }) {
94 const entry = yield* registry
95 .resolve(params.owner, params.repo)
96 .pipe(Effect.catchTag("StoreError", (error) => Effect.die(error)));
97 if (entry === undefined) return yield* new Git.RepoNotFound(params);
98 const repo = repos.getByName(entry.repoId);
99 const meta = yield* repo
100 .getRepoMeta()
101 .pipe(Effect.catchTag("StoreError", (error) => Effect.die(error)));
102 const ref = yield* repo
103 .getRef(`refs/heads/${meta.defaultBranch}`)
104 .pipe(Effect.catchTag("StoreError", (error) => Effect.die(error)));
105 return new Git.Ref({ name: ref.name, oid: ref.oid as Git.Oid });
106 }));
107 }),
108);
109```
110
111Provide `AppRoutesLive` to `HttpApiBuilder.layer(AppApi)`, then merge that route layer beside `Git.ApiLive`.
112`Git.ApiLive` registers the Git groups.
113[HTTP routes](/git/blocks/server#add-git-to-your-application) shows the full assembly. The registry
114and repository clients are resolved when the group builds; the handler
115runs once per request behind the route middleware.
116
117## Compose application operations
118
119Use `Git.Engine` to work with repository data from your own handler:
120
121```typescript
122const git = yield* Git.Engine;
123const repo = yield* git.repositories.get({ owner: "alice", repo: "demo" });
124const prepared = yield* git.prepareRefRemoval(repo, { ref: "refs/heads/feature" });
125yield* checkRefChanges(repo, prepared.updates);
126yield* prepared.commit;
127yield* Effect.log("branch removed");
128```
129
130Run this inside `Effect.scoped`. `checkRefChanges` is your application function;
131it can use your request user and database. The prepared operation pins the ref
132value and can commit only once before the scope closes. Use the same policy from
133push and merge handlers. [Engine operations](/git/blocks/engine) describes the APIs.
134
135`RepoStore` remains the lower storage contract for implementing another backend.
136Its raw RPC methods are infrastructure primitives, not an authorization boundary.
137
138## Implement your own
139
140The contract is `Git.GitRepoShape` behind `Git.RepoStore`. A different
141store provides the same service, and nothing else in the graph changes:
142
143```typescript
144export const ReposMine = Layer.effect(
145 Git.RepoStore,
146 Effect.gen(function* () {
147 // return { getByName: (repoId) => GitRepoShape }
148 }),
149);
150```
151
152The registry and the blob store are the seams it can reuse. The wire protocol (`fetch`) and the push pipeline are the parts
153that assume the object's transactional storage, so a store without a
154per-repository serialization point starts there.
155
156## Where the bytes are
157
158| Objects | Live in | Because |
159| --- | --- | --- |
160| commits, trees, tags | SQLite rows, deflated | the tree walk behind every fetch runs on SQLite |
161| fresh blobs | SQLite rows, deflated | a push commits as one transaction |
162| oversize blobs | the blob store, one index row here | never buffered in the object |
163| compacted blobs | immutable packs in the blob store | a busy repository's object stays small |
164| clone bundles | the blob store, keyed by a refs hash | the Worker streams them, no pack byte transits the object |
165
166A full clone whose refs have not moved is one object in the blob
167store; the Worker pipes it. Otherwise the clone is assembled from rows
168and packs, and a fresh bundle is scheduled. Objects are stored
169deflated, so a pack is concatenation plus a checksum.
170
171## Jobs
172
173Maintenance runs on the object's alarms, armed by the work itself.
174Every job is idempotent and resumable:
175
176| Job | Armed by | Does |
177| --- | --- | --- |
178| compaction | a push crossing a size threshold, or `startCompact` | rewrites loose blobs into a pack |
179| bundling | refs moving | cuts a fresh clone bundle |
180| purge | `startPurge` | drains rows and blobs, then frees the name |
181| fork, import | `startFork`, `startImport` | copies from a sibling object or fetches from a remote |
182
183```sh
184curl -u "x:$GIT_SECRET" -X POST "$HOST/api/v1/repos/acme/web/compact"
185```
186
187## What it reports
188
189```typescript
190const repo = yield* client.repos.get({ params: { owner: "acme", repo: "web" } });
191
192repo.objects; // { loose, resident, packed, r2, bytes }
193repo.lastPush; // { ingestMs, stageMs, connectivityMs, finalizeMs, totalMs }
194```
195
196Listings report zeros for both, so listing a thousand repositories does
197not wake a thousand objects. [Scaling](/git/recipes/scaling) has the
198throughput numbers behind the layout above.