Skip to main content
Use this pattern when you give your users direct Git access. Your users clone and push with a remote on your domain, such as https://git.your-app.com/team/project-alpha.git. Your server authenticates each request with your own system and forwards it to Code Storage with a short-lived token. Your users never receive a Code Storage token or your private key. You control:
  • Authentication. Use your own sign-in, access tokens, or SSO.
  • Authorization. Decide which user can read or write each repository, and set ref policies for each user.
  • Revocation. When you remove access in your system, the next request fails.
  • Access logs. Record the user, IP address, repository, and operation of each request.

Choose an approach

A proxy gives you the most control, but it is also the most work: you run a server in the path of every clone and push. Two lighter options do not need a proxy:
  • Short-lived remote URLs. Your backend gives each user a remote URL with a short-lived token. See Git remote URLs on the SDK page.
  • A Git credential helper. You ship a credential helper that gets a short-lived token from your API each time Git needs credentials.
With these options, your users hold a Code Storage token until it expires, and their requests go directly to Code Storage. Use a proxy when your users must never hold a token, or when you must check, revoke, and log each request.

How it works

Your proxy must:
  • Forward only the Git endpoints below, and return 404 for every other path.
  • Return 401 when the user’s credentials are missing or incorrect, and 404 when the user cannot access the repository.
  • Sign a new token for each request, with the repository, the scopes that the request needs, your user ID as sub, and your ref policies.
  • Never forward the user’s Authorization header to Code Storage.
  • Stream the request body and the response body.

The endpoints that Git uses

Git over HTTPS uses four requests for each repository, under /{repo}.git/:
The example proxy does not support Git LFS. It returns 404 for LFS requests. To add LFS, see Support Git LFS.

Build the proxy

The proxy is a Fetch API handler: it takes a Request and returns a Response. This example runs it on Node.js with srvx and signs tokens with jose. Install the packages:
Set PIERRE_ORG and PIERRE_PRIVATE_KEY as shown in Authentication. Each step below adds code to proxy.ts, in order. The complete file is at the end of this section.

Set up

proxy.ts

Match the Git endpoints

Each route sets the access that its endpoint needs. A request that matches no route gets 404.
proxy.ts

Check the user

The handler authenticates the user, checks access to the repository, and writes one log line for each request. The line has the user, IP address, repository, endpoint, and response status.
proxy.ts

Connect your user system

Put your own logic in auth.ts. It exports two functions:
  • authenticate(request) returns the user, as an object with an id, or null. Git sends the user’s credentials as HTTP Basic auth in the Authorization header. For null, the proxy returns 401, and Git asks the user or its credential helper for credentials.
  • authorize(user, repo, access) returns true if the user can access the repository. access is 'read' for a clone or fetch, and 'write' for a push. For false, the proxy returns 404, so a user cannot find out which repositories exist.

Sign a token

The token covers one repository and the scopes that the request needs. It sets sub to your user ID, which Code Storage shows in its logs. The token expires after 300 seconds. It only needs to be valid when Code Storage receives the request, so a long push still completes.
proxy.ts
If you use a restricted key, its allowlist must include git:read and git:write. A restricted key also limits the token lifetime to 3,600 seconds.

Forward the request

The proxy forwards only the headers that Git needs. It does not forward the user’s Authorization header. A clone or push can transfer a large pack, so the proxy streams both bodies and does not buffer them.
proxy.ts

Serve the proxy

proxy.ts
handleGit() uses only the Fetch API. To run it on another server, pass it each Request and the client IP address, and return its Response. Run the proxy with Node.js 24 or later. Node.js runs TypeScript files directly. Set "type": "module" in package.json.

The complete proxy

proxy.ts

Use the remote

Give your users the URL of your proxy. Git sends their credentials with HTTP Basic auth, so they can use a Git credential helper as they do with any other host:
When Git asks for credentials, the user enters the credentials that your authenticate() accepts, for example a username and an access token from your product. The proxy matches repository paths that end in .git, so give your users URLs in this form.

Attribute pushes

Push webhooks identify the repository and ref, but not the user who pushed. Your proxy log identifies the user for each git-receive-pack request. Use your proxy log as the record of who pushed.

Test the proxy

Run these checks before you give the remote to users:
  1. Clone a repository through the proxy with a valid user.
  2. Push to a branch that the user can write.
  3. Clone with an incorrect token. The proxy returns 401.
  4. Clone a repository that the user cannot access. The proxy returns 404.
  5. Push to a ref that a ref policy blocks. Git shows push denied by policy.

Support Git LFS

The example proxy returns 404 for Git LFS requests. To support LFS, add the steps in this section to proxy.ts.
Do not forward LFS requests without changes. In the LFS batch response, the actions that point at Code Storage contain the Authorization header of the request. This header contains the Code Storage token of your proxy. Your users then get a Code Storage token, and their LFS transfers do not go through your proxy.

The endpoints that Git LFS uses

The Git LFS client finds the LFS server at your remote URL, under /{repo}.git/info/lfs/. It uses these requests: A batch request is a POST for downloads and for uploads. The operation field in the request body tells which. A download batch needs git:read. An upload batch needs git:read and git:write.

Match the LFS endpoints

Set PROXY_URL to the public URL of your proxy, for example https://git.your-app.com. The proxy uses it in the batch response.
proxy.ts

Check the user for LFS

The LFS handler is the same as handleGit(), with two changes. It reads the access of a batch request from the body. It also rewrites the batch response.
proxy.ts

Rewrite the batch response

Each action in the batch response has an href and a header. An action that contains the token of your proxy points at Code Storage. The proxy changes that action:
  • The href goes to your proxy, with the same path.
  • The Authorization header is the user’s own header from the batch request. The LFS client sends it with the transfer, so your proxy can authenticate the user again.
The proxy does not change the other actions. These are signed object storage URLs, or URLs of the upstream host for a synced repository. They do not contain your token.
proxy.ts

Pass redirects to the client

A download can get a 307 redirect to a signed object storage URL. Return the redirect to the LFS client, so that the client downloads the object directly. In forward(), add location to the response headers, and set redirect: 'manual':
proxy.ts

Serve LFS requests

Send LFS requests to handleLFS(), and all other requests to handleGit():
proxy.ts

Test LFS

Run these checks with a repository that tracks a file with LFS:
  1. Push a commit with an LFS file through the proxy.
  2. Clone the repository through the proxy. The LFS file has its full content, not a pointer.
  3. Clone with a user that can read but not write. The clone succeeds, and a push of an LFS file returns 404.
  4. Run GIT_TRACE=1 git lfs pull. No LFS request goes directly to your-org.code.storage.