0020. Forges are reached through one service, git
Status: accepted, 2026-10-21
Context
The owner decided deploys from a repository: connect GitHub and GitLab, pick a repository and a branch in
a panel, build from a server-side clone, deploy on push and preview every merge or pull request. apps,
pages and docs all need it, and docs already carries a fetcher and a webhook endpoint of its own (a token
per space, a static hook secret, the whole repository downloaded as a zip). Forge credentials would be the
most valuable secrets the platform holds: they read every connected project's code, and with statuses and
comments they write to it. Webhooks get lost, arrive late and out of order, and GitLab's older ones carry
only a static token. Builds run on nodes, which a team may own and which see everything delivered to
them (ADR 0008).
Decision
- A service,
git (gitd at git.<d>), is the only place that holds forge credentials or speaks a
forge's API. apps, pages and docs reach repositories through it; none of them keeps a forge token, a
webhook endpoint or a forge client.
- GitHub is connected through one GitHub App per instance, registered by GitHub's manifest flow (a
superuser's few clicks; its key and secrets sealed in gitd). Teams install it on their accounts; gitd
binds an installation to a team only after the GitHub user's own authorization shows they can reach
it. gitd works with installation tokens narrowed, per call, to one repository and the permissions that
call needs. GitHub personal access tokens are not used.
- GitLab (gitlab.com or self-managed) is connected through an OAuth application of that GitLab, or
through a group, project or personal access token a team admin pastes. gitd refreshes OAuth tokens and
rotates access tokens that may rotate themselves.
- Code leaves a forge only through gitd. Builders and services fetch with clone tokens: random,
stored hashed, read-only, for one repository and one commit, minutes long, accepted only by gitd's git
proxy and archive endpoint, which add the forge credential upstream. Forge credentials never reach a
node.
- Webhooks are hints. gitd checks a delivery's signature, then reads the state that matters (a
branch's head, a merge request) from the forge's API before anything happens, so a forged, replayed,
duplicated or late delivery can't make the platform build what isn't there. Polling covers webhooks
that never come, and GitHub's failed deliveries are asked for again.
- Connections belong to teams. A connection reaches what its credential reaches (an installation's
repositories, a token's group or project), and every developer of the team may build and deploy those
repositories. A person who links a GitHub installation they didn't just make, on an account that isn't
theirs, connects only the repositories they reach themselves.
- Repository hosting is not built: GitLab and GitHub stay the forges; Forgejo and Gitea remain apps
templates.
Consequences
- One more binary on the control plane (about 25 MB of memory), and a GitHub App per instance that its
owner creates; GitLab works without it.
- Clones pass through the control plane, streamed and bounded. A builder never holds more than one
commit of one repository, for a quarter of an hour.
- docs drops its fetcher and webhook endpoint; id's later "linked identities" no longer has to carry
repository access.
- Consumers learn of pushes and merge requests through watches (ADR 0021).