Skip to content
Self-hosting

GitHub App and Orb

A self-host needs webhook delivery and installation tokens. Direct GitHub App is the default, recommended model — Orb broker mode is private/managed-beta only.

Choose a connection mode

Direct GitHub App (recommended default)
Your self-host stores its own App id, slug, private key, and webhook secret, and mints installation tokens directly. No shared quota, no dependency on gittensory's own infrastructure to process a review.
Brokered Orb (private/managed-beta only)
Your self-host uses ORB_ENROLLMENT_SECRET to request short-lived installation tokens from the central Orb broker instead of holding its own App key. Not open for general public use — see the operational risks below before considering it.
Direct App mode is the public default: it costs gittensory nothing to support and can't overrun a shared rate-limit budget. Brokered mode routes every token mint through gittensory's own infrastructure and GitHub API quota — every external brokered install is gittensory's rate-limit and reliability problem, not just the operator's, so it stays private/managed-beta until the safeguards below are in place.

One-click App creation (recommended for a Direct App)

Before the App exists (no GITHUB_APP_ID set yet), the self-host serves a setup wizard at GET /setup. It renders a form that POSTs a GitHub App manifest — the exact permission and event set below, pre-filled — to GitHub's own App-creation flow. GitHub creates the App with the correct configuration in one step and redirects back to exchange credentials automatically; there is no manual permission checklist to get right or wrong. The route is disabled once an App is configured, so it can't rebind a live install.

.env
PUBLIC_API_ORIGIN=https://reviews.example.com  # exact public URL, embedded in the manifest
SELFHOST_SETUP_TOKEN=change-this-long-random-value  # unlocks /setup for a freshly-booted instance
open "https://reviews.example.com/setup"
bash

Enter SELFHOST_SETUP_TOKEN in the browser form. For scripted setup checks, send the token in an x-setup-token header or Authorization: Bearerheader instead; never place the setup token in the URL.

https://reviews.example.com above is a placeholder — it assumes you already have a real domain terminating TLS. GitHub delivers webhooks to whatever PUBLIC_API_ORIGIN you set here, so it must be an address GitHub's servers can actually reach: the caddy profile (see Security's TLS termination section) is the shipped way to get one, or bring your own public reverse proxy. The tailscale profile's private tailnet address does not work here — GitHub cannot deliver webhooks to it. A Tailscale-only instance should use brokered pull mode instead (it polls for work rather than receiving pushed webhooks) — see "Pull vs. push relay mode" below.
Manual App creation (below) is still fully supported — for an air-gapped instance, a stricter change-review process, or simply a preference for reviewing every permission by hand before it exists. Whichever path you take, the resulting App needs the SAME permissions: this doc's manual list is kept in sync with the wizard's manifest and checked in CI, so the two can't silently drift apart.

Direct App permissions

  • Pull requests: write.
  • Checks: write — the gate posts a check-run; checks: read alone 403s that write (silently fails the first review with no obvious cause).
  • Issues: write.
  • Contents: write — required for BOTH merging and the auto-maintain update_branch action. contents: read looks sufficient at creation time but silently breaks auto-merge later with no error surfaced in the UI; there is no lesser permission that keeps merge/update-branch working.
  • Commit statuses: read.
  • Metadata: read.
  • Actions: write — lets a repo opt into cancelling a closed PR's in-flight CI runs (the contributorCapCancelCi setting). Off by default and never required: a repo that doesn't enable it, or an installation that hasn't re-approved this permission on an existing App, sees no behavior change — the cancellation attempt is skipped and logged, never blocking the close itself.

Events: pull request, pull request review, push, issues, check suite, check run, and status.

Re-approving a permission bump on an existing App

A future release can widen this permission list (most recently, Actions: write for the opt-in CI-cancellation feature). GitHub does not silently grant a new permission to an App that's already installed — the operator who owns the App must explicitly re-approve it, the same one-time consent step as the original install.

Until you re-approve, the self-host keeps working exactly as before: any feature that needs the new permission degrades gracefully (skipped and logged, never a hard failure) rather than erroring. There's no forced upgrade window.

To re-approve:

  1. Open your App's settings page — https://github.com/settings/apps/<your-app-slug>/permissions (organization Apps: https://github.com/organizations/<org>/settings/apps/<your-app-slug>/permissions).
  2. GitHub shows a diff between the App's currently-granted permissions and what the App manifest now requests. Review it, then save — GitHub sends the installation owner a request to accept the new grant.
  3. Accept the request (as the installation owner, on each installed org/account). The new permission takes effect immediately; no App reinstall or webhook resubscription needed.

Direct App env

.env
GITHUB_APP_ID=123456
GITHUB_APP_SLUG=my-gittensory-app
GITHUB_APP_PRIVATE_KEY_FILE=/run/secrets/github-app-private-key.pem
GITHUB_WEBHOOK_SECRET=<same-secret-configured-on-the-app>

Telemetry is separate from token brokerage

These are two independent things people conflate because they're both "Orb": anonymized fleet-calibration telemetry export (enabled by default, works in either connection mode) and token brokerage (optional, private/managed-beta only, lets your self-host get installation tokens from gittensory instead of holding its own App key). Choosing Direct App mode does not opt you out of telemetry, and it's what makes the homepage counters and cross-fleet gate calibration reflect direct installs, not just brokered ones.

What's exported
Per resolved PR: the gate verdict, the realized outcome (merged/closed), a reversal flag, a bucketed reason category, and cycle time.
What's never exported
Repo/owner/PR names, commit SHAs, source code, diffs, comments, or logins. Repo/PR identifiers are HMAC-anonymized by default with a per-instance secret gittensory's own collector never holds.
Disabling it
Set ORB_AIR_GAP=true to compute everything locally and send nothing — the only supported opt-out. There is no partial opt-out short of air-gapping.
ORB_ANONYMIZE
Repo/PR identifiers are HMAC-anonymized by default (ORB_ANONYMIZE=true), not unconditionally — an operator can set ORB_ANONYMIZE=false to export raw repo/PR names instead. There's no scenario where gittensory's own hosted collector needs raw names; the toggle exists for an operator running their own collector (see ORB_COLLECTOR_URL below) who wants readable identifiers in their own infrastructure. Leave this at the default unless you control the collector end.

ORB_COLLECTOR_URL overrides the export endpoint — default gittensory's hosted collector, or point it at your own private collector if you're aggregating telemetry yourself instead of sending it to gittensory. ORB_COLLECTOR_TOKEN is the bearer credential for that private collector; leave it unset when using gittensory's own hosted collector, which accepts unauthenticated, rate-limited, aggregate-only exports.

Brokered Orb env

.env
ORB_ENROLLMENT_SECRET=<issued-once-by-orb>
ORB_BROKER_URL=https://gittensory-api.aethereal.dev
ORB_RELAY_MODE=pull  # or omit for push (the default) -- see "Choosing a relay mode" below

ORB_APP_ID overrides the seed used to derive this instance's stable, anonymous instance_id in telemetry exports — normally derived from GITHUB_APP_ID. A brokered instance holds no App ID of its own (it uses the broker's tokens instead), so its identity falls back to the export secret unless you set ORB_APP_ID explicitly. Most operators never need to set this; it exists so a brokered instance's telemetry identity can be pinned independent of any App ID.

Choosing a relay mode: pull vs. push

Brokered mode still needs a way for GitHub webhook events to reach your self-host through the broker. ORB_RELAY_MODE picks how:

pull (recommended for NAT/tailnet — no public ingress needed)
The container polls the broker outbound on a short interval and drains queued events -- no inbound endpoint is ever exposed, and PUBLIC_API_ORIGIN is not required. A failed registration attempt is non-fatal (logged as a warning, not an error): the drain loop keeps retrying on its own schedule and events still arrive once it succeeds.
push (the default — requires a stable public origin)
The broker calls your self-host directly at PUBLIC_API_ORIGIN, which must be a real, internet-reachable, TLS-terminated URL -- the broker validates it server-side at registration time and rejects a loopback or private address outright. A failed registration is fatal: the container looks healthy but never receives an event, since there's no fallback delivery path.
If you're not behind a stable public ingress — a home connection, a NAT without port forwarding, a tailnet-only deployment — set ORB_RELAY_MODE=pull. It needs no DNS record, TLS certificate, or firewall rule of its own, and tolerates a transient broker outage more gracefully (see the release checklist's known-warnings table below). Use push only once you already have a stable, publicly reachable HTTPS origin for this instance — the Direct App setup wizard, for instance, always requires one anyway, so an operator running Direct App today has it available for brokered push mode too. See Security's TLS termination section for how to stand one up: the caddy profile for a public domain, or note that tailscale's private tailnet address does not satisfy push mode's internet-reachable requirement — pull mode is the right fit for a Tailscale-only instance.
Brokered mode operational risks
Before enabling this for anyone outside a controlled managed-beta cohort, weigh: (1) rate-limit blast radius — every brokered install's GitHub API traffic draws from token pools gittensory manages, so one misbehaving or high-volume install can degrade every other brokered install; (2) quota management — there is no automatic per-install cap on how much of that shared budget one enrollment can consume; (3) support burden — a broken brokered install looks like a gittensory outage to its operator, not a self-host misconfiguration, and lands as a support request on gittensory directly; (4) abuse/misconfiguration risk — an enrollment secret that leaks or a misconfigured relay can mint tokens or receive webhook traffic for repos the intended operator doesn't control.

Minimum broker safeguards before a public rollout

A maintainer go/no-go checklist — do not open brokered enrollment beyond a small, known, controlled cohort until every item below is true:

  • Enrollment quota — a hard cap on how many brokered installs can be active at once, not just an informal agreement.
  • Per-install concurrency limit — one brokered install cannot occupy an unbounded share of the token-minting or webhook-relay pipeline.
  • Per-install rate budget — a ceiling on GitHub API calls attributable to a single enrollment, independent of the other installs sharing the broker.
  • Revocation path — an enrollment secret can be revoked immediately, without waiting for a deploy, when it's compromised or the install is abusive.
  • Metrics broken out by enrollment — token-mint volume, webhook-relay volume, and error rate are visible per-enrollment, not only aggregated across every brokered install, so one bad actor is identifiable instead of hiding in the average.

See Troubleshooting for what a degraded brokered relay looks like in logs today, and the release checklist's brokered-mode scenario for the smoke tests that exercise both relay modes.

Connectivity checks

Confirm you can reach the instance at all before checking GitHub's own webhook delivery:

curl https://reviews.example.com/health
curl https://reviews.example.com/ready
bash

reviews.example.com here stands in for whatever you're checking from — the caddy profile's domain, an existing reverse proxy, or (if you're on the same tailnet) a Tailscale instance's tailnet address on port 8787. This only confirms you can reach the instance, not that GitHub can — a Tailscale-only instance in push mode will pass this check and still never receive a real webhook, since GitHub itself cannot reach a private tailnet address (see the callout above on PUBLIC_API_ORIGIN).

After installing the App on a test repo, open a small PR and confirm the webhook delivery appears in GitHub and a job appears in self-host logs — this is the check that actually proves GitHub can reach you. Continue with Operations for log and metric checks.