Installation & Webhooks
A user authenticating with OAuth is step one. Step two is installing the GitHub App on an organization — without this, there are no leaderboards, no invites, no data.
The install flow
Start
GET /api/install/start creates a state JWT embedding the current sessionId, sets a CSRF cookie, and redirects to https://github.com/apps/{appName}/installations/new. If no session exists, it redirects to /api/auth/start first — signing in and installing are chained seamlessly.
Callback
GET /api/install/callback verifies:
- The state JWT is valid and unexpired
- The CSRF value in the cookie matches the one in the JWT
installation_idis a valid integer
It then calls InstallationService.validateInstallation(installationId) — a live GitHub API call that confirms the installation exists and is accessible. On success, the installation ID is written to session_installations, linking it to the user’s session.
Programmatic completion
POST /api/install/complete is a headless alternative to the callback redirect. Useful for client apps that handle the install flow outside the browser:
POST /api/install/complete
{ "installationId": 56789 }Returns { ok: true, installationId }. Requires an existing session (requireApiSession).
Permission checking
Every org workspace runs a permission gate through GET /api/[org]/installation/access. This endpoint:
- Resolves the org’s installation via
resolveInstallationForOrganization - Fetches granted permissions from GitHub’s
GET /app/installations/{id}(live API call for authoritative data) - Compares against
REQUIRED_INSTALLATION_PERMISSIONSusingfindMissingPermissions() - Falls back to the Supabase cache if the GitHub API call fails
The response:
{
"installed": true,
"installationId": 56789,
"organizationId": 12345,
"suspended": false,
"canManage": true,
"missingPermissions": [
{
"key": "issues",
"label": "Issues",
"category": "repository",
"level": "read",
"reason": "Required to count issue activity in leaderboard scoring"
}
],
"manageUrl": "https://github.com/organizations/my-org/settings/installations/56789"
}InstallationAccessBanner in the frontend renders a non-blocking alert when the app is not installed, suspended, or missing permissions. Org admins get a link to the GitHub installation settings page to review and approve; non-admins are told to ask an admin.
Staleness caveat
viewerCanAdminister is read from GitHub at sign-in time and stored in organization_memberships. If someone is promoted to admin on GitHub after signing in, they must sign out and back in for the new permission to take effect. There is no background refresh.
Webhook events
GitHub sends webhook events to POST /api/install/webhook whenever the app’s installation state changes.
Signature verification
Every payload is signed with X-Hub-Signature-256:
HMAC-SHA256(GITHUB_WEBHOOK_SECRET, rawRequestBody)The handler compares using crypto.timingSafeEqual to prevent timing attacks. If GITHUB_WEBHOOK_SECRET is not set, the handler logs a warning and accepts all requests — never deploy without this in production.
Handled events
| Event | Action | DB mutation |
|---|---|---|
installation created | Fetch full details from GitHub | INSERT or UPDATE github_installations |
installation deleted | Remove the app | DELETE FROM github_installations |
installation suspend | Mark as suspended | UPDATE github_installations SET suspended_at, suspended_by |
installation unsuspend | Restore | UPDATE github_installations (clear suspended fields) |
installation_repositories added | Repo count changed | Refresh installation details + repo list |
installation_repositories removed | Repo count changed | Refresh installation details + repo list |
organization any | Organization metadata changed | Log only, no DB write |
membership any | Membership changed | Log only, no DB write |
member any | Member changed | Log only, no DB write |
Events not listed above are silently acknowledged with 200.
Idempotency
All handlers are idempotent — processing the same event twice produces the same database state. Upserts are used for inserts and updates; deletes are safe to repeat. GitHub’s webhook delivery retries are safe.
Response codes
| Status | Condition |
|---|---|
200 { ok: true } | Event processed |
| 400 | Invalid JSON body |
| 403 | HMAC signature mismatch |
| 500 | Error during processing |
Manual install re-sync
If the install callback didn’t fire for some reason (network blip, browser closed mid-flow), you can re-link an installation programmatically:
curl -X POST http://localhost:3000/api/install/complete \
-H "Authorization: Bearer <sessionId>" \
-d '{"installationId": 56789}'This calls InstallationService.validateInstallation and inserts the session_installations row without needing the browser redirect flow.