Skip to main content

GitHub App setup

Hyrax uses a GitHub App for all GitHub operations: cloning repos, reading PR diffs, posting review comments, creating PRs, and posting check runs. The App replaces personal access tokens (PATs) with short-lived, scoped installation tokens.

For runtime mechanics (token caching, webhook routing, signature verification) see Integrations.

1. Create the App

Go to GitHub Settings > Developer settings > GitHub Apps > New GitHub App (or https://github.com/organizations/{org}/settings/apps/new for org-owned apps).

Basic info

FieldValue
App namehydra-review (or your preferred name)
Homepage URLYour Hyrax instance URL
Webhook URLhttps://{your-hydra-domain}/api/webhooks/github
Webhook secretGenerate one: openssl rand -hex 32
Setup URLhttps://{your-hydra-domain}/auth/github/setup
Redirect on updateON — fires Setup URL on every Configure-Save, not just initial install

The Setup URL is a deliberately-distinct endpoint from the OAuth callback (/auth/github/callback). The callback handles login-with-OAuth-code; the Setup URL handles post-install / post-Configure round-trips with just an installation_id. Conflating them used to cause the dead-end loop where users with stale install state got bounced to installations/new, GitHub auto-redirected to select_target for the pre-existing install, and they were stranded on github.com. Turn "Redirect on update" ON so a user editing the repo allowlist via Configure-Save round-trips back into Hyrax rather than seeing the bare GitHub settings page.

Permissions

PermissionAccessUsed for
ContentsRead & WriteClone repos, push fix branches
Pull requestsRead & WriteRead diffs, post review comments, create PRs
ChecksWritePost check run annotations
Members (org-level)ReadList org members + teams for role assignment in the onboarding wizard + Users page; resolves private team memberships that the user's OAuth token can't see

No other permissions are needed. Avoid granting admin permissions or write on Members.

Re-consent on Members:Read upgrade. GitHub surfaces an org-permission addition as a yellow "Permissions Updated" banner inside the org admin's GitHub Apps settings. The App keeps working with the prior permission set; the new permission only activates after an org admin clicks Accept. Existing tenants whose admin hasn't accepted see the onboarding wizard's "By person" / "By team" tabs render a "Re-accept permissions on GitHub" CTA, not an error — the wizard degrades to repo-scope-only until consent lands.

Subscribe to events

EventPurpose
InstallationRegister/remove app installations
Installation repositoriesTrack per-install repo add/remove
Pull requestTrigger automatic PR reviews
PushStamp repos.last_push_at for the docs-freshness planner
OrganizationPropagate organization.renamedtenants.github_org_login + repos.github_org
RepositoryPropagate repository.renamedrepos.github_repo + repos.name (without this, the SPA URL /repos/<org>/<repo>/<branch> 404s after a GitHub-side repo rename)
TeamSync tenant_role_team_mappings to GitHub-side team lifecycle
MembershipRevoke sessions on team-membership drop
Check runSurface CI status on Hyrax-authored PRs
Github app authorizationRevoke sessions on user-side App auth revoke

If the live App is missing the Repository subscription (every App created before this guide landed), an org admin needs to toggle it on in the App settings page — webhooks for events the App isn't subscribed to never fire, and the in-repo handler is a no-op without delivery.

Where can this GitHub App be installed?

Choose Only on this account for internal use, or Any account if deploying as a SaaS.

2. Generate a private key

After creating the App, click Generate a private key. Save the .pem file securely.

3. Configure Hyrax

Set these environment variables on the API, webhook, and worker deployments:

GITHUB_APP_ID=123456 # From the App's settings page
GITHUB_APP_SLUG=hydra-ai-dev # The App's public slug (the `<slug>` in github.com/apps/<slug>)
GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
GITHUB_WEBHOOK_SECRET=your-webhook-secret

For Docker, escape newlines in the PEM as literal \n characters. Hyrax handles the conversion.

4. Install on your org

Visit https://github.com/apps/{slug}/installations/new and install on the org(s) that contain your target repos.

Customer onboarding (auto-provisioning)

For customer-plan tenants (free / pro / team), installing the App is the entire onboarding step:

  1. Org admin installs the Hyrax GitHub App on their GitHub org.
  2. GitHub fires installation.created to Hyrax's webhook endpoint. The payload's installation.sender block identifies the user who clicked Install.
  3. Hyrax auto-creates the tenant row (plan_code='free'; slug set from the org login at the time and frozen; github_org_login set to the same value but mutable; github_org_id = <org id>, immutable), provisions the per-tenant schema (tenant_<slug>), runs alembic, and provisions the per-tenant Postgres role.
  4. The installation is auto-linked to the new tenant.
  5. The installer is promoted to bootstrap owner — the GitHub user who clicked Install gets a users row with is_account_owner = true inside the new tenant schema, atomically seeded by the tenant_provisioner tick (industry-standard installer-as-owner model, by inheriting GitHub's own org-admin authorization decision). No default_role flip required; the owner survives any future team-mapping changes via the resolver's owner-bypass precedence rule.
  6. Org members log in via /auth/github/start → GitHub → /auth/github/callback; the callback cross-references the user's GitHub orgs against public.github_installations.github_org_login and resolves the tenant. 0 tenants → heal-on-miss against GET /user/installations then no-access page if no install found; 1 → bound session; N → tenant picker. Session is a server-side row in public.sessions; the cookie holds only the opaque row id (12h TTL). The GitHub user-to-server token is not stored.

Three-leg state convergence

Install state on the Hyrax side (public.github_installations + public.tenants) is a cache of GitHub-side state. GitHub is the source of truth. Three convergence paths keep the cache fresh:

PathTriggerLatencyHandler
Webhook (push)installation.created fires~seconds_handle_installation_created in apps/api/app/routers/webhooks.py
OAuth heal-on-miss (pull-on-demand)User logs in and DB says 0 tenantsinline_heal_tenant_options_from_github calls GET /user/installations and provisions for any install GitHub reports but DB doesn't
Reconciler tick (pull-periodic)Worker tick every 10 minboundedhyrax.worker.installation_reconciler walks GET /app/installations (App JWT) and reconciles forward + disables orphans

All three call the same idempotent chokepoint, hyrax.db.install_provisioning.provision_or_heal_from_install. Webhook reliability becomes a latency concern (push is fastest), not a correctness concern for the tenant + install rows — the reconciler catches anything push missed within 10 minutes, and OAuth heal-on-miss catches anything the moment a user tries to log in. Concurrent-delivery races between the four paths are handled inside the chokepoint by re-fetching on github_org_id and returning success when a peer call won.

The installer-as-owner seed is webhook-only. Only the installation.created webhook carries installation.sender (the GitHub user who clicked Install); the OAuth-heal, Setup URL, and reconciler paths all run with installer_*=None and DO NOT auto-promote anyone to owner. A tenant whose webhook delivery was dropped lands a fully-functional row via the reconciler but without an owner — operators promote one manually via POST /api/admin/tenants/{slug}/users/promote-owner. The reconciler emits install_reconciler.provisioned_without_owner_seed per row so this state is visible. The _heal_tenant_options_from_github path additionally requires GITHUB_APP_SLUG to be set (fail-closed) so a misconfigured deploy can't leak sibling-App installs into the heal.

Reconciler orphan-disable safeguards. disable_tenant + destroy_sessions_for_tenant would be catastrophic on a bad GitHub response, so the orphan branch refuses to fire when GET /app/installations returns empty against a non-empty DB, when the orphan ratio exceeds HYRAX_INSTALL_RECONCILER_ORPHAN_RATIO_CAP (default 0.25), or until an install has been missing for HYRAX_INSTALL_RECONCILER_ORPHAN_CONFIRM_TICKS consecutive ticks (default 2). Each safeguard tripping emits a distinct WARN log.

When a customer renames their GitHub org, the organization.renamed webhook updates github_org_login only — slug stays put so existing URLs (/repos/<slug>/...) keep working and bookmarks don't break.

The org admin sees the result in their GitHub > Settings > Applications > Installed GitHub Apps > Hyrax > Recent Deliveries log. A 200 means the tenant is provisioned; a 422 means the slug collided with a reserved Hyrax slug (see below).

Reserved slugs

These slugs cannot be auto-claimed (they collide with Hyrax's own tenants or operational subdomains):

hydra · hydra-admin · api · app · admin · internal · static · assets

If your org's GitHub login matches one, the webhook returns 422 and no tenant is created. Rename the GitHub org or contact Hyrax support — Hyrax deliberately doesn't auto-suffix because that would silently hand back a slug you didn't ask for.

Re-installing after uninstall

installation.deleted flips disabled_at on the customer tenant; the row, schema, and audit history all survive. Re-installing nulls out disabled_at (matched by github_org_id) and the tenant picks up where it left off. Internal-plan tenants are unaffected by uninstall — those are operator-managed.

Manual operator path (internal-plan seeding)

Operator control-plane identities live in public.operators, not in any tenant. Internal-plan tenants are minted via the standard GitHub App install path on a Hyrax-staff org (e.g. kandji-inc for dogfood) and then promoted to plan_code='internal' via the operator-shell tenant detail page — the webhook never accepts a plan override from the GitHub payload.

5. Verify

After installation, the Hyrax web UI (Settings > Integrations) should show the linked installation. You can also verify by triggering a PR review from a repo's detail page.

Token lifecycle

  • Hyrax generates short-lived installation tokens (valid 1 hour, cached 50 minutes).
  • Tokens are scoped to the specific org and permissions granted.
  • No PATs needed for normal operation.
  • Local dev uses gh auth login as a fallback (no PAT needed).

Troubleshooting

ProblemCauseFix
"No GitHub token available"App not installed or not linked to tenantCheck Settings > Integrations
PR review runs but no comment postedpull_requests: write not grantedUpdate App permissions on GitHub
Fix job push fails with 403contents: write not grantedUpdate App permissions on GitHub
Webhook events not arrivingWrong webhook URL or secretCheck GITHUB_WEBHOOK_SECRET matches
Installation not auto-linkedMulti-tenant deploymentLink manually in Settings > Integrations