1---
2title: "Part 2: Control access"
3description: Protect the existing Git host with one shared credential, then verify both accepted and rejected requests.
4---
5
6import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
7
8The host from [Part 1](/git/tutorial/part-1) accepts requests from anyone.
9Add a single shared credential so only people who have it can use the host.
10This part changes access to the existing repositories; it does not recreate them.
11
12## Generate the credential
13
14Create `src/secret.ts`:
15
16```typescript
17// src/secret.ts
18import * as Alchemy from "alchemy";
19import * as Effect from "effect/Effect";
20
21export const GitSecret = Effect.gen(function* () {
22 const Random = yield* Alchemy.Random;
23 return yield* Random("GitSecret");
24});
25```
26
27Alchemy creates this random value on the first deploy and keeps it stable across
28updates. It is the password Git clients will send.
29
30## Check the password on each request
31
32Create `src/middleware.ts`:
33
34```typescript
35// src/middleware.ts
36import { RuntimeContext } from "alchemy";
37import * as Effect from "effect/Effect";
38import * as Redacted from "effect/Redacted";
39import * as HttpRouter from "effect/http/HttpRouter";
40import * as HttpServerResponse from "effect/http/HttpServerResponse";
41import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder";
42import * as HttpApiSecurity from "effect/http-api/HttpApiSecurity";
43import { GitSecret } from "./secret.ts";
44
45export const Authentication = HttpRouter.middleware(
46 Effect.gen(function* () {
47 const secret = yield* (yield* GitSecret).text;
48 return (httpEffect) =>
49 Effect.gen(function* () {
50 const { password } = yield* HttpApiBuilder.securityDecode(
51 HttpApiSecurity.basic,
52 );
53 if (Redacted.value(password) === Redacted.value(yield* secret)) {
54 return yield* httpEffect;
55 }
56 return HttpServerResponse.empty({
57 status: 401,
58 headers: { "www-authenticate": 'Basic realm="git"' },
59 });
60 }).pipe(Effect.provide(RuntimeContext.phantom));
61 }),
62);
63```
64
65HTTP Basic has a username and password. This policy ignores the username and
66checks the password. A matching credential continues to the Git handler; other
67requests receive `401`. `WWW-Authenticate` tells Git to prompt for a credential.
68The Worker supplies `RuntimeContext` per request; the phantom layer accounts for
69that service in the middleware's type.
70
71## Apply the check to Git's routes
72
73```diff lang="typescript"
74// src/git.ts
75import * as Layer from "effect/Layer";
76+import { Authentication } from "./middleware.ts";
77```
78
79Replace the start of the route assembly:
80
81```diff lang="typescript"
82// src/git.ts
83-export const GitLive = Git.ApiLive.pipe(
84+const PublicRoutes = Git.ApiLive.pipe(
85+ Layer.provide(Authentication.layer),
86+);
87+
88+export const GitLive = PublicRoutes.pipe(
89 Layer.provide(Git.ApiHandlersLive),
90```
91
92`PublicRoutes` means the routes exposed to Git and API clients. They now all pass
93through your middleware, including repository management, pushes, and clones.
94
95## Output the credential
96
97Add these imports to `alchemy.run.ts`:
98
99```diff lang="typescript"
100// alchemy.run.ts
101import * as Effect from "effect/Effect";
102+import * as Output from "alchemy/Output";
103+import * as Redacted from "effect/Redacted";
104+import { GitSecret } from "./src/secret.ts";
105```
106
107Then include the generated value in the stack's outputs:
108
109```diff lang="typescript"
110// alchemy.run.ts
111 const host = yield* GitHost;
112- return { url: host.url.as<string>() };
113+ const secret = yield* GitSecret;
114+ return {
115+ url: host.url.as<string>(),
116+ secret: Output.map(secret.text, Redacted.value),
117+ };
118```
119
120This exposes the shared credential in the deployment output so you can copy it
121for the tutorial. Anyone with this credential has full access to the host.
122
123## Deploy the access policy
124
125<Tabs syncKey="pkgManager">
126 <TabItem label="bun" icon="bun">
127 <Code code={`bun alchemy deploy`} lang="sh" />
128 </TabItem>
129 <TabItem label="npm" icon="npm">
130 <Code code={`npx alchemy deploy`} lang="sh" />
131 </TabItem>
132 <TabItem label="pnpm" icon="pnpm">
133 <Code code={`pnpm alchemy deploy`} lang="sh" />
134 </TabItem>
135 <TabItem label="yarn" icon="seti:yarn">
136 <Code code={`yarn alchemy deploy`} lang="sh" />
137 </TabItem>
138</Tabs>
139
140Copy the printed credential:
141
142```sh
143export GIT_SECRET="paste-the-secret-output"
144```
145
146Keep `$HOST` from Part 1. The Worker URL and `acme/web` repository are unchanged.
147
148## Verify that anonymous requests fail
149
150```sh
151curl -i "$HOST/api/v1/repos"
152```
153
154Expect `401` and a `WWW-Authenticate: Basic realm="git"` header. If you still see
155the old `200` response immediately after deploying, repeat this check while the
156update propagates; access is protected only once the new policy is serving.
157The same request
158with the credential succeeds:
159
160```sh
161curl --fail-with-body -u "x:$GIT_SECRET" "$HOST/api/v1/repos"
162```
163
164You should see the repository created in Part 1.
165
166## Push with the credential
167
168```sh
169printf 'Access is now protected.\n' >> work/README.md
170git -C work commit -am "Require a credential"
171git -c credential.helper= -C work push origin main
172```
173
174When prompted, enter `x` as the username and the value of `$GIT_SECRET` as the
175password. The push should succeed. Clone with the same credentials to verify
176that authenticated reads work too:
177
178```sh
179git -c credential.helper= clone "$HOST/acme/web.git" private-copy
180git -C private-copy fsck --strict
181```
182
183Continue to [Part 3: Publish a repository](/git/tutorial/part-3), where reads of
184public repositories become available without the shared credential.