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
| Field | Value |
|---|---|
| App name | hydra-review (or your preferred name) |
| Homepage URL | Your Hyrax instance URL |
| Webhook URL | https://{your-hydra-domain}/api/webhooks/github |
| Webhook secret | Generate one: openssl rand -hex 32 |
| Setup URL | https://{your-hydra-domain}/auth/github/setup |
| Redirect on update | ✅ ON — 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
| Permission | Access | Used for |
|---|---|---|
| Contents | Read & Write | Clone repos, push fix branches |
| Pull requests | Read & Write | Read diffs, post review comments, create PRs |
| Checks | Write | Post check run annotations |
| Members (org-level) | Read | List 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
| Event | Purpose |
|---|---|
| Installation | Register/remove app installations |
| Installation repositories | Track per-install repo add/remove |
| Pull request | Trigger automatic PR reviews |
| Push | Stamp repos.last_push_at for the docs-freshness planner |
| Organization | Propagate organization.renamed → tenants.github_org_login + repos.github_org |
| Repository | Propagate repository.renamed → repos.github_repo + repos.name (without this, the SPA URL /repos/<org>/<repo>/<branch> 404s after a GitHub-side repo rename) |
| Team | Sync tenant_role_team_mappings to GitHub-side team lifecycle |
| Membership | Revoke sessions on team-membership drop |
| Check run | Surface CI status on Hyrax-authored PRs |
| Github app authorization | Revoke 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:
- Org admin installs the Hyrax GitHub App on their GitHub org.
- GitHub fires
installation.createdto Hyrax's webhook endpoint. The payload'sinstallation.senderblock identifies the user who clicked Install. - Hyrax auto-creates the tenant row (
plan_code='free';slugset from the org login at the time and frozen;github_org_loginset 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. - The installation is auto-linked to the new tenant.
- The installer is promoted to bootstrap owner — the GitHub user who clicked Install gets a
usersrow withis_account_owner = trueinside 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). Nodefault_roleflip required; the owner survives any future team-mapping changes via the resolver's owner-bypass precedence rule. - Org members log in via
/auth/github/start→ GitHub →/auth/github/callback; the callback cross-references the user's GitHub orgs againstpublic.github_installations.github_org_loginand resolves the tenant. 0 tenants → heal-on-miss againstGET /user/installationsthen no-access page if no install found; 1 → bound session; N → tenant picker. Session is a server-side row inpublic.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:
| Path | Trigger | Latency | Handler |
|---|---|---|---|
| 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 tenants | inline | _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 min | bounded | hyrax.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 loginas a fallback (no PAT needed).
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| "No GitHub token available" | App not installed or not linked to tenant | Check Settings > Integrations |
| PR review runs but no comment posted | pull_requests: write not granted | Update App permissions on GitHub |
| Fix job push fails with 403 | contents: write not granted | Update App permissions on GitHub |
| Webhook events not arriving | Wrong webhook URL or secret | Check GITHUB_WEBHOOK_SECRET matches |
| Installation not auto-linked | Multi-tenant deployment | Link manually in Settings > Integrations |