How It Connects to GitHub
This app talks to GitHub through a GitHub App, not a personal OAuth token. That distinction matters for everything downstream: who can see what, rate limits, and security.
Why a GitHub App
A GitHub App has its own identity. It’s installed on an organization by an admin, who grants it specific repository permissions. When the app makes API calls, it does so as itself — not as whichever user happens to be signed in.
Concrete benefits:
- Rate limits are per-installation: 15,000 requests/hour vs 5,000 for user tokens. Leaderboard ingest can pull data across dozens of repos without hitting the ceiling.
- Access doesn’t depend on who’s logged in: if the org admin who installed the app leaves the org, the app keeps working. A personal token would break.
- Permissions are explicit and auditable: the org admin sees exactly what the app can do in their GitHub settings.
The tradeoff: you need org admin buy-in to install it. But once installed, it stays installed.
Two tokens, two jobs
The app uses two different GitHub credentials depending on what it’s doing:
| Token | Source | Used for | Scope |
|---|---|---|---|
| User OAuth token | Obtained when someone signs in through the browser | GraphQL proxy, listing orgs, viewing repo lists | What the signed-in user can see |
| Installation token | Generated per-installation via getInstallationOctokit() | Leaderboard ingest, mass-invite, bulk-role, team operations | All repos the app is installed on |
Why two? Ingest needs to see all repos the app is installed on — not just the repos the signed-in user can see. If the app ingested with a user token, a lead dev who leaves the org takes the leaderboard with them. The installation token is tied to the app, not the person.
Required permissions
The app requests these permissions during installation. GET /api/[org]/installation/access compares what was granted against this list and surfaces any gaps as missingPermissions in the UI.
| Permission | Level | Why |
|---|---|---|
| Metadata | Read | Identify repositories and their properties |
| Contents | Read | Read commit history for scoring |
| Pull requests | Read | Read PR open/merge/close events |
| Issues | Read | Read issue open/close events |
| Members | Read | Resolve team membership for team leaderboards |
These are defined in lib/github/required-permissions.ts. If an org admin hasn’t approved a permission that was added in a newer version of the app, the InstallationAccessBanner shows which ones are missing and links to the GitHub settings page to approve them.
Authorization model
Every API route that touches org data goes through a guard chain:
requireApiSession → requireOrganizationAccess → requireOrganizationAdmin → resolveInstallationForOrganizationEach guard short-circuits: no session → 401, not a member of the org → 404 (404, not 403, to avoid leaking org existence), not an admin → 403, no installation → 403.
requireApiSessionchecks thegh_sessioncookie (httpOnly, Secure) orAuthorization: Bearer <sessionId>headerrequireOrganizationAccesschecks that the user’s session includes a membership for the requested orgrequireOrganizationAdminchecksviewerCanAdminister(snapshotted at sign-in from GitHub)resolveInstallationForOrganizationverifies a GitHub App installation exists for the org
Webhook-driven sync
GitHub sends webhook events to POST /api/install/webhook when the app is installed, uninstalled, suspended, or when repositories are added/removed. The handler verifies the X-Hub-Signature-256 HMAC signature and updates the github_installations table.
Events that mutate the database: installation (created, deleted, suspended, unsuspended), installation_repositories (added, removed). Events that are logged but not acted on: organization, membership, member.
Warning: If
GITHUB_WEBHOOK_SECRETis not set in production, the webhook handler accepts all requests without signature verification. Always set it.