← Documentation index · ← BaaS overview · ← Inbound webhooks
Audience. Org admins who want to wire a GitLab/GitHub/Forgejo repo to a bot (e.g. Revi on merge-request open) without the manual PAT→secret→binding→webhook→forge-hook chain.
This is the outbound complement of inbound webhooks:
inbound (pkg/webhooks) authenticates deliveries the forge sends to
iterion; forge integrations (pkg/forge) hold the admin credential that
lets iterion call out to the forge to register that delivery — and the
bot-secret binding — in one action.
In the studio, /teams/<id> → Integrations:
GET /user), reads the identity, and stores the credential
sealed.forge_token secret from the
connection (created once per connection);iwh_ secret it holds on both ends, the events the bots need,
and a per-webhook SecretOverrides pin to the managed token;iwh_ secret;RepoIntegration join row.Disable is the inverse, one click: delete the forge hook, the webhook config, and the join row (the managed secret survives — it is shared by the connection’s other repos; it is removed when the connection is deleted).
forge: in its manifest)A bot opts into auto-provisioning with a forge: block in its
manifest.yaml — advisory, discovery-time metadata (like dispatch_vars),
read by the orchestrator, not the runtime:
forge:
events: [pull_request, pull_request_comment] # normalized; mapped per provider
token_scopes:
pull_requests: write
repository: read
secret: forge_token # the workflow-secret name the bot consumes
webhook:
launch_vars: { pr_review_mode: summary }
min_replier_role: developer
rationale: |
Shown in the enable dialog so the operator sees why each scope is asked.
Normalized event vocabulary (mapped to the forge’s native names by pkg/forge/event_map.go):
| normalized | GitLab | GitHub | Forgejo |
|---|---|---|---|
pull_request |
merge_requests_events |
pull_request |
pull_request |
pull_request_comment |
note_events |
issue_comment |
issue_comment |
Unknown events / scope keys / levels fail manifest parsing (pkg/bundle/manifest.go:validateForgeRequirements).
The connection’s admin token (OAuth user token / installation token / PAT)
is used only to manage iterion’s footprint on the forge — create hooks,
list repos. It is never what a bot posts with. Instead the orchestrator
derives a managed, team-scoped forge_token generic secret from it and pins
it on the webhook via SecretOverrides (Tier-0 in
ResolveGenericWithBindings). So the entire existing run path —
forge_token → RunBundle → /run/iterion/secrets/forge_token →
glab/gh — is unchanged. OAuth/App tokens are kept fresh by a
background worker (pkg/forge/refresh.go) that
re-seals the connection blob, then rewrites the managed secret’s plaintext;
PAT connections never refresh.
Note on identity (vs envelope): “never what a bot posts with” means the sealing envelope differs (
forge_conn:<id>→generic_secret:<id>), not the credential — the managed secret holds the same token value, so a bot acts as the connection’s identity on the forge. Who that is, how it relates to the iterion user who launched the bot (it doesn’t — different planes), least-privilege, and GitHub vs GitLab vs Forgejo are documented in forge-permissions.md.
The Mongo stores (forge_connections, repo_integrations) and the routes
are wired automatically in iterion server cloud mode. OAuth apps are
opt-in per provider via env (a provider with no client id offers only the
PAT path):
ITERION_FORGE_GITLAB_OAUTH_CLIENT_ID / _CLIENT_SECRET
ITERION_FORGE_GITHUB_OAUTH_CLIENT_ID / _CLIENT_SECRET (Phase 3)
ITERION_FORGE_FORGEJO_OAUTH_CLIENT_ID / _CLIENT_SECRET (Phase 4)
PublicURL must be set (the OAuth redirect + the forge hook URL are built
from it).
forge_conn:<id>; the managed
secret keeps the existing generic_secret:<id> AAD; the GitHub-App
private key (Phase 2) lives in deployment config, never in Mongo. No token
is ever logged or placed in a URL.Connection.ForgeBaseURL is threaded onto webhooks.Config.ForgeBaseURL
so the existing inbound SSRF host-pin keeps applying; the global
ITERION_WEBHOOK_FORGE_HOSTS allowlist still wins.iwh_ webhook secret is minted server-side and never shown to the
operator — iterion holds both ends. A fresh one is minted on every
mutating provision so the forge hook secret and the iterion config hash
stay in lockstep without needing the prior plaintext.insufficient_scope 403 so the studio can prompt to reconnect
with broader scope or paste a PAT.All under /api/teams/{id}/forge/ (admin/owner), except the OAuth callback
which is a public IdP redirect target authenticated by signed state + an
agent-binding cookie:
| Method | Path | Purpose |
|---|---|---|
| GET | /connections |
list connections |
| POST | /connections |
connect (mode: pat | oauth) → {connection} or {authorize_url} |
| DELETE | /connections/{conn_id} |
disconnect (deprovisions every repo first) |
| GET | /connections/{conn_id}/repos?search=&page= |
repo picker (admin-capable only) |
| GET | /api/forge/oauth/callback |
public OAuth redirect target |
| GET | /repo-bots |
list active integrations |
| GET | /repo-bots/preview?connection_id=&repo=&bots= |
events + scopes + conflicts, no writes |
| POST | /repo-bots |
enable {connection_id, repo, bot_ids} → ProvisionResult |
| DELETE | /repo-bots/{integration_id} |
disable |
| Provider | Mode(s) | Status |
|---|---|---|
| GitLab | OAuth · PAT | live (pkg/forge/gitlab) |
| GitHub | OAuth App · PAT · GitHub App | live (pkg/forge/github) |
| Forgejo / Gitea | OAuth · PAT¹ | live (pkg/forge/forgejo) |
¹ Forgejo OAuth-token API auth uses the Gitea token scheme like the PAT
path; validate against a live instance before relying on the OAuth (vs PAT)
mode there.
The GitHub App mode (a third connect option for GitHub) authenticates
with a per-installation token minted on demand from the App private key
(RS256 JWT → POST /app/installations/{id}/access_tokens, cached, re-minted
≈60 s before its ~1 h expiry by the refresh worker). The operator installs
the App via github.com/apps/<slug>/installations/new?state=…; GitHub’s
“Setup URL” must point at <PublicURL>/api/forge/github/app/callback. The
App posts as itself (<slug>[bot]), needs no user seat, and uses
fine-grained per-repo permissions. Config:
ITERION_FORGE_GITHUB_APP_ID
ITERION_FORGE_GITHUB_APP_PRIVATE_KEY_FILE (PEM; or _PRIVATE_KEY inline)
ITERION_FORGE_GITHUB_APP_SLUG
Adding a provider = implement the forge.Admin interface (+ an
OAuthExchanger/TokenRefresher for OAuth) and register it in the server’s
provider dispatch; the orchestrator + studio are provider-agnostic.