Overview
The installation flow links a GitHub App installation to a user session. It is separate from the OAuth flow: a user can be authenticated without having any installations, and installations can be added at any time post-login.
Install start
GET /api/install/start — requires active session; redirects to /api/auth/start with 302 if no session found.
- Create install state JWT:
{ type: 'install', csrf, returnTo, sessionId } - Set
gh_auth_install_csrfcookie (httpOnly, sameSite=none, secure, 10 min) - Redirect to the GitHub App install page
The sessionId is embedded in the JWT so the callback can recover the session even if the gh_session cookie is unavailable (e.g. cross-device or cross-origin redirect).
Install callback
GET /api/install/callback
Failure responses:
| Condition | Status |
|---|---|
| State JWT invalid or expired | 400 |
| CSRF mismatch | 403 |
installation_id not a finite integer | 400 |
| GitHub validation fails | 400/502 |
Install complete
POST /api/install/complete — programmatic alternative to the callback redirect, useful for headless clients.
Request body:
{ "installationId": 12345678 }Steps:
- Validate
installationIdis a finite number → 400 if not - Validate the installation exists via GitHub API → error if not found
- Insert
session_installations(session_id, installation_id) - Return
{ ok: true, installationId }
Install status
GET /api/install/status — returns the current installation state for all installations linked to the session.
For each installation ID in the session: InstallationService.getInstallationRepositories(id).
Response shape:
{
installed: boolean
installationIds: number[]
accounts: Array<{
installationId: number
accountLogin: string
repositoryCount: number
repositories: Repository[]
updatedAt: string
}>
summary: {
totalInstallations: number
// ... aggregate fields
}
}Installation permission check
Every organization workspace runs a permission gate before rendering. GET /api/[org]/installation/access resolves the org’s installation and compares its granted permissions (read authoritatively from GitHub via apps.getInstallation) against the set the app requires:
| Category | Permission | Key | Level |
|---|---|---|---|
| Repository | Metadata | metadata | Read |
| Repository | Contents | contents | Read |
| Repository | Pull requests | pull_requests | Read |
| Repository | Issues | issues | Read |
| Organization | Members | members | Read |
The required set lives in lib/github/required-permissions.ts (REQUIRED_INSTALLATION_PERMISSIONS); findMissingPermissions() returns the entries that are absent or below the required level. Response shape:
{
installed: boolean
installationId: number | null
organizationId: number | null
suspended: boolean
canManage: boolean // viewer can administer the org
missingPermissions: MissingPermission[]
manageUrl: string | null // GitHub installation settings URL
}InstallationAccessBanner (mounted in the org layout) renders a non-blocking notification when the app is not installed, is suspended, or is missing permissions. Org admins get a link to the GitHub installation settings to review and approve; non-admins are told to ask an admin.
Webhook events
Installation lifecycle events from GitHub flow through POST /api/install/webhook. See webhooks for the full event table and HMAC verification details.