# Plansmith self-hosted Git broker

Let Plansmith read a Git server inside your network, without opening a firewall port and without giving us your credentials.



{/*
  PSM-584 — the managed broker setup guide.

  ⚠ THE READER OF THIS PAGE IS NOT A PLANSMITH USER. It is the customer's infra
  or security engineer: someone handed a URL and a token by a colleague, who
  will never log in, and who has to decide whether to run our container inside
  their network. So it leads with what it does and what it can reach, answers
  the security review in their vocabulary, and only then gives the command.

  ⚠ ONE BLOCK ON THIS PAGE IS PINNED TO CODE AND MUST NOT BE HAND-EDITED: the
  image reference in the `docker run` command, pinned to `BROKER_IMAGE`
  (`src/lib/marketing/broker-image.ts`) by `broker-image.test.ts`, which reads
  THIS FILE. To change it, change the constant and re-run that test.

  ⚠ THE ENDPOINT TABLE IS GONE ON PURPOSE, AND SO IS ITS GUARD. This page used
  to publish `renderAllowlistTable()` verbatim under an "The allowlist" heading,
  pinned by `broker-allowlist-drift.test.ts`. Readers found a table of GitLab
  REST paths confusing — it reads as a thing they have to evaluate, when the
  bound they actually control is the service account they create. The security
  section now says that, and mentions the container's own narrowing in a
  sentence. Do not restore the table without restoring the drift guard with it:
  a published table nothing pins is how the last one came to promise a narrower
  surface than the container enforced (PSM-593).

  ⚠ THE SETUP TABS CARRY NO MARKDOWN HEADINGS, DELIBERATELY. The table of
  contents is built from the file, not from what is on screen, so a `##` inside
  a panel lists all three products' steps at once and two thirds of them scroll
  to hidden content. Steps inside a panel are `<Steps>` with a bold first line.

  ⚠ ONLY THE GITLAB TAB DESCRIBES A BROKER. The container's target is GitLab
  (`TARGET_GITLAB_BASE_URL`) and `broker-agent/src/allowlist.ts` exports
  `TARGETS = { gitlab }` — nothing else. The other two tabs describe the DIRECT
  path, which is what actually works for them today. Do not write broker steps
  for GHES or Bitbucket DC before the allowlist has rules for them.
  */}

## When you need one [#when-you-need-one]

If your GitLab, GitHub Enterprise or Bitbucket server is reachable from the internet, you do not
need this. Register it directly and skip this page. The managed broker exists for the other case: a
server that only answers inside your network, behind a VPN or a corporate firewall.

<Callout>
  Self-hosted Git servers, including the broker, are available on the Team and Enterprise plans.
</Callout>

## How it works [#how-it-works]

You run a small container inside your network. It dials **out** to Plansmith over TLS and asks for
work. When a planning session needs to read your repository, we put a request on a queue; your
broker picks it up, adds your own Git credential, calls your server, and posts the response back.

<Mermaid
  alt="Every connection starts at the broker. Inside your network, the plansmith-broker container opens an outbound HTTPS connection to app.plansmith.co and asks for work; Plansmith answers on that connection with one allowlisted read; the broker calls your Git server with your own credential, gets the file, and posts the response back. Nothing connects inward."
  chart="`sequenceDiagram
box Your network
participant broker as plansmith-broker
participant git as Your Git server
end
participant plansmith as app.plansmith.co
broker->>plansmith: outbound HTTPS: any work for me?
plansmith-->>broker: read this-file.tsx
broker->>git: the read, with your credential
git-->>broker: the this-file.tsx contents
broker->>plansmith: the response`"
/>

**One broker reaches one server.** The container is configured with a single target URL, so if you
have two internal Git servers you run two brokers, each with its own token. Plansmith enforces this:
a broker that already reaches a server cannot be chosen for a second one.

## What your security team will ask [#what-your-security-team-will-ask]

* **Inbound ports: none.** The broker makes outbound connections only. There is no firewall rule to
  add, no inbound route, and no VPN access for us.
* **Where credentials live: only in your network.** Your Git token is an environment variable on
  your container. Plansmith never receives it, never stores it, and cannot read it. The broker
  overwrites the authorization header on every request with your token, so nothing we send can
  substitute a credential of ours.
* **What can be reached: whatever the service account or API token you create allows.** You set the
  reach in your own Git server, and Plansmith cannot exceed it: the broker holds no credential of
  its own, so an account with read access to two projects is an integration with read access to two
  projects. The container narrows it further, to the handful of read-only API calls Plansmith
  actually makes; a request outside that set is refused before it reaches your server, and the
  refusal is logged. No write endpoint is reachable through the broker at all.
* **Audit: one JSON line per relayed request** to the container log, recording timestamp, method,
  path, decision, status, duration and byte counts. Never request bodies, never file contents, never
  tokens.
* **What we store, and for how long.** What your broker returns is the context the planning agent
  works from, so it lives in that plan's chat for as long as the chat does. Getting it there uses a
  queue in our database, and those rows are deleted as they are read; anything a failed request
  leaves behind is scrubbed within the hour, and revoking a broker deletes its queued rows on the
  spot. Archive a plan and it is deleted 30 days later, its chats and everything read into them
  included.

## Setting up your server [#setting-up-your-server]

Select the git server you run.

<UrlTabs param="server" defaultValue="gitlab">
  <UrlTab value="github" label="GitHub">
    **The broker does not relay GitHub Enterprise Server yet.** The container's target is GitLab and its
    allowlist holds GitLab endpoints, so there is nothing to run for GHES today. What you do depends on
    one thing: whether your server answers from the internet.

    **If it does**, you do not need a broker at all: register it directly and skip the rest of this
    page.

    <Steps>
      <Step>
        **In GHES: create a GitHub App.** In the organization that owns the repositories, go to **Settings →
        Developer settings → GitHub Apps → New GitHub App**. Set the callback URL to
        `https://app.plansmith.co/api/code-context/oauth/github/callback`, untick **Webhook → Active**, and
        give it two read-only repository permissions: **Contents: Read-only** and **Metadata: Read-only**.
        Nothing else is used.
      </Step>

      <Step>
        **Collect four values** from the App's page: the **App ID**, its **slug** (the last segment of the
        App's URL), a **client secret**, and a **private key**. The last two are generated on that page, and
        the key downloads as a `.pem` file.
      </Step>

      <Step>
        **Install the App** on the organizations whose repositories your team plans against. An App that
        exists but is installed nowhere connects successfully and then lists no repositories.
      </Step>

      <Step>
        **In Plansmith: register the server.** Open **Settings → Connectors** as an owner on the Team or
        Enterprise plan, and under **Self-hosted** select **Add instance → GitHub Enterprise Server**.
        Answer that the server is **reachable from the internet**, enter its URL and the four values, save,
        then select **Connect** to authorize as yourself.
      </Step>
    </Steps>

    **If it only answers inside your network**, there is nothing to run yet. Broker relay for GHES is
    planned as this same container with a GitHub target and its own allowlist, on the terms above:
    outbound only, your credential never leaving your network, reads only. It is prioritized by who
    asks, so tell us you need it.

    <Callout>
      None of this applies to github.com, which connects in one click under **Settings → Connectors**.
    </Callout>
  </UrlTab>

  <UrlTab value="gitlab" label="GitLab">
    The setup takes about fifteen minutes across three places, and each step names where you are. You will need:

    * Owner access to the GitLab group or project you want to plan against, for steps 1 to 3
    * A Plansmith owner on the Team or Enterprise plan for step 4
    * A host inside your network with Docker, able to reach the GitLab server and to make outbound HTTPS to `app.plansmith.co`, for step 5.

    <Steps>
      <Step>
        **In GitLab: create a service account.** A service account is GitLab's non-human account for
        exactly this: it has no password and no inbox, nobody can sign in as it, its reads appear in your
        audit logs under its own name, and it holds exactly the access Plansmith needs and nothing else.

        1. Open the **group** whose projects your team plans against. For a single repository, open that
           **project** instead.
        2. In the left sidebar, go to **Settings → Service accounts**.
        3. Select **Add service account** and name it `plansmith`.

        Create it on the group when several projects should be available; a project-level service account
        covers that one project. Creating one takes an owner of the group or project.

        <Callout>
          If **Settings → Service accounts** is not in your sidebar, your GitLab is older or on the Free tier.
          The fallback works the same way: an administrator creates a dedicated regular user in the **Admin
          area** (**Overview → Users → New user**), signs in as it once, and mints the token under **Edit
          profile → Access tokens**. Steps 2 and 3 are unchanged.
        </Callout>
      </Step>

      <Step>
        **In GitLab: give it read access to the right projects.** A service account starts with access
        to nothing, and its token can only see what the account can see. Membership is where you decide
        which code Plansmith can read; nothing outside it is reachable, whatever the token says.

        1. In the group (or project), go to **Manage → Members** and select **Invite members**.
        2. Invite the service account. Search for the name you gave it; it appears with a generated username
           like `@service_account_…`.
        3. Give it the role **Reporter**.
        4. Repeat for any other group you want available. Subgroups and their projects inherit the
           membership.

        <Callout>
          **Reporter is the floor.** On a private project the Guest role can see that the project exists but
          cannot read its files, so a Guest membership passes every screen here and then produces empty file
          lists in step 6.
        </Callout>
      </Step>

      <Step>
        **In GitLab: create the access token.** Tokens for a service account are managed from the same
        page you created it on. You never sign in as the account.

        1. Back in **Settings → Service accounts*&#x2A;, open the &#x2A;*⋮** menu on the `plansmith` row and select
           **Manage access tokens**.
        2. Select **Add new token**.
        3. Name it `plansmith-broker`, pick an expiry your policy allows, and tick **both** scopes:
           **read\_api** and **read\_repository**.
        4. Create it and copy the `glpat-` value straight away. GitLab shows it once.

        <Callout>
          **Both scopes, deliberately.** The broker talks to the GitLab REST API, and the scope that opens it
          is `read_api`. A token with only `read_repository` looks right, is accepted here, and then fails
          step 6 with 403: that scope covers Git operations, not the API. Both scopes are read-only.
        </Callout>
      </Step>

      <Step>
        **In Plansmith: add the server and get a pairing token.**

        1. In Plansmith, open **Settings → Connectors**. You need to be an owner, on the Team or Enterprise
           plan.
        2. Under **Self-hosted**, select **Add instance** and pick GitLab.
        3. Where it asks whether the server is reachable from the internet, answer **behind a firewall or
           VPN**.
        4. Enter the server URL, for example `https://gitlab.example.internal`. This is how the server
           appears in Plansmith; traffic goes wherever your container is configured to reach, so use the
           same URL here and in `TARGET_GITLAB_BASE_URL` in step 5. Private addresses and internal hostnames
           are fine.
        5. Select **Add managed broker** and name it after where it will run, for example `dc-frankfurt`.
           The `psbk_` pairing token is shown **once**, with the command for step 5 pre-filled.

        Keep this tab open: the broker flips to **Online** here the moment the container makes its first
        poll.
      </Step>

      <Step>
        **On your host: run the container.** On the Docker host inside your network, run the pre-filled
        command from step 4. It looks like this:

        ```bash
        docker run -d --name plansmith-broker --restart on-failure:5 \
          -e PLANSMITH_URL=https://app.plansmith.co \
          -e PLANSMITH_BROKER_TOKEN=psbk_… \
          -e TARGET_GITLAB_BASE_URL=https://gitlab.internal \
          -e TARGET_GITLAB_TOKEN=glpat-… \
          ghcr.io/braingridai/plansmith-broker:0.3.0
        ```

        `PLANSMITH_URL`: always `https://app.plansmith.co`. Not the API or MCP host, which serve a different
        surface and will not answer a broker poll (the container refuses to start if you point it at one).

        `PLANSMITH_BROKER_TOKEN`: the `psbk_` token from step 4.

        `TARGET_GITLAB_BASE_URL`: your GitLab as reachable **from this container**, the same URL you entered
        in step 4.

        `TARGET_GITLAB_TOKEN`: the `glpat-` token from step 3. It never leaves your network.

        `HTTPS_PROXY` / `NO_PROXY`: optional, honoured if your egress goes through a proxy.
        `NODE_EXTRA_CA_CERTS`: optional, point it at your CA bundle if a proxy terminates TLS or your server
        uses a private CA.

        `AUDIT_LOG_PATH`: optional. The audit log always goes to stdout; this also writes it to a file.
      </Step>

      <Step>
        **Verify end to end.** Within a few seconds of the container starting, the broker shows
        **Online** in Plansmith. Finish saving the instance: Plansmith verifies it by asking your broker to
        call `GET /user` on your GitLab, with your token, from inside your network. When that succeeds, pick
        the repositories your team should plan against. The list you are choosing from is the service
        account's membership from step 2.

        For a final check, attach one of those repositories to a plan and ask the planning agent something
        about the code. The answer arrives through the broker, and `docker logs plansmith-broker` shows one
        JSON audit line per file read.
      </Step>
    </Steps>
  </UrlTab>

  <UrlTab value="bitbucket" label="Bitbucket">
    **The broker does not relay Bitbucket Data Center yet.** The container's target is GitLab and its
    allowlist holds GitLab endpoints, so there is nothing to run for Bitbucket today. What you do
    depends on one thing: whether your server answers from the internet.

    **If it does**, you do not need a broker at all: register it directly and skip the rest of this
    page.

    <Steps>
      <Step>
        **In Bitbucket: create an incoming application link.** As an administrator, go to **Administration →
        Applications → Application links**, select **Create link**, and choose **External application →
        Incoming**.
      </Step>

      <Step>
        **Set the redirect URL** to `https://app.plansmith.co/api/code-context/oauth/bitbucket-dc/callback`
        and grant the **Repositories: Read** permission, which is the whole of what Plansmith asks for. Save
        the link, then copy its **client ID** and **client secret**.
      </Step>

      <Step>
        **In Plansmith: register the server.** Open **Settings → Connectors** as an owner on the Team or
        Enterprise plan, and under **Self-hosted** select **Add instance → Bitbucket Data Center**. Answer
        that the server is **reachable from the internet**, enter its URL and the two values, save, then
        select **Connect** to authorize as yourself.
      </Step>
    </Steps>

    **If it only answers inside your network**, there is nothing to run yet. Broker relay for Bitbucket
    Data Center is planned as this same container with a Bitbucket target and its own allowlist, on the
    terms above: outbound only, your credential never leaving your network, reads only. It is
    prioritized by who asks, so tell us you need it.

    <Callout>
      None of this applies to Bitbucket Cloud, which connects in one click under **Settings →
      Connectors**.
    </Callout>
  </UrlTab>
</UrlTabs>

## If something is wrong [#if-something-is-wrong]

Everything below is about a running broker, so it is the git server's path.

* **A startup error naming the host.** `PLANSMITH_URL` points at the API or MCP host. Use
  `app.plansmith.co`.
* **Polls return 404.** The pairing token was revoked, or mistyped. Add a new managed broker (step
  4\) and restart the container with its token.
* **It stays Offline.** Outbound HTTPS to `app.plansmith.co` is blocked, or your proxy variables are
  not set on the container.
* **Verification fails with 403 on a fresh token.** Two different halves fail identically from
  outside: the token is missing the scope that opens your server's REST API, or the account it
  belongs to is not a member of the group or project. Check both; your product's tab above names
  the scope and the role.
* **Repositories are missing from the picker, or file lists come back empty.** The account's
  membership does not cover them, or covers them at a level that can see a project exists but not
  read its files. Widen the membership; a broader token will not fix it, because the token can only
  ever see what the account can.
* **`allowlist_denied` in the log.** Working as designed: the container relays a fixed set of
  read-only API calls, and something outside it was requested and refused before reaching your
  server. Send us the line if you think that call should be allowed.
* **TLS errors.** Set `NODE_EXTRA_CA_CERTS` to your CA bundle.
* **Your monitoring shows requests with no response status.** Expected. An idle broker holds a poll
  open for up to 25 seconds waiting for work, and some tools log the open request with an empty
  status. It completes as an ordinary 200.

## Upgrading and shutting down [#upgrading-and-shutting-down]

Pin a version tag or an image digest and redeploy to upgrade. To stop a broker for good, revoke it
in Plansmith: its token stops working immediately, whatever version the container is running, and
anything still queued for it is deleted.

**To move a server to a different broker**, start the new broker, then add the server again in
Plansmith with the same URL and pick the new broker. Same URL means it updates the server you
already have rather than creating a second one, and the repositories you have already linked keep
working, so there is nothing to re-link.
