Organization listing
GET /api/organizations — requires session.
Returns the user’s organizations plus GitHub App installation status for each. Calls getOrgInstallationsByOrganizationIds from lib/supabase/installation-repository.ts.
Response:
{
"organizations": [
{
"id": 12345,
"login": "my-org",
"name": "My Org",
"avatarUrl": "https://avatars.githubusercontent.com/u/12345",
"viewerCanAdminister": true,
"hasAppInstalled": true,
"installationId": 56789,
"repositoryCount": 42,
"repositorySelection": "all",
"suspendedAt": null
}
]
}Organization detail
GET /api/organizations/[login] — requires session + org access.
Calls resolveInstallationForOrganization then InstallationService.getInstallationForOrg. Returns installation metadata for the specified org.
Mass-invite
POST /api/organizations/[login]/mass-invite — requires session + admin + active installation.
Validation pipeline
| Step | Check | Failure |
|---|---|---|
| 1 | User has valid session | 401 |
| 2 | User can access the org | 400 (orgRequired) |
| 3 | User is an org admin | 403 (notAdmin) |
| 4 | Body is valid JSON | 400 |
| 5 | userLogins is a non-empty array of strings | 400 |
| 6 | Up to 50 users (deduped) | 413 |
| 7 | GitHub App is installed on the org | 403 |
Invite execution
Uses installation token (getInstallationOctokit), not user OAuth token.
For each login, two REST calls run simultaneously via Promise.allSettled over all users — there is no batching or concurrency limit constant:
const results = await Promise.allSettled(
uniqueLogins.map(async login => {
const user = await octokit.rest.users.getByUsername({ login });
await octokit.rest.orgs.createInvitation({
org: login,
invitee_id: user.data.id,
role: 'direct_member',
});
return login;
})
);Role is always 'direct_member'. There is no option to invite as owner or billing manager through this endpoint.
Request
{
"userLogins": ["username1", "username2"]
}Response
{
"success": ["username1"],
"failed": [{ "login": "username2", "error": "Not Found" }]
}Note: the response key is success, not succeeded.
Common failure reasons
'already a member'— user is already in the org'Not Found'— GitHub username does not exist'insufficient permissions'— GitHub App lacks permissions or is not installed
The endpoint is fully synchronous — it blocks until all Promise.allSettled results are back, then returns. There is no background job or queue involved.
Bulk role change
POST /api/organizations/[login]/bulk-role — requires session + admin + installation.
Sets multiple users to member or admin in a single request.
Request
{
"userLogins": ["username1", "username2"],
"role": "admin"
}role must be 'member' or 'admin'.
Response
{
"success": ["username1"],
"invited": [],
"failed": [{ "login": "username2", "error": "user not found in org" }]
}Execution
Sequential loop (not Promise.allSettled). Max 50 users.
Auth note: Uses
getRequestSessiondirectly with an inline admin check instead of the standardrequireOrganizationAdminguard. The behavior is equivalent but the implementation differs from most other admin routes.
Bulk team membership add
POST /api/organizations/[login]/teams/bulk-add — requires session + admin + installation.
Adds multiple users to a specified team in a single request.
Request
{
"userLogins": ["username1", "username2"],
"teamSlug": "frontend"
}Response
{
"success": ["username1"],
"invited": [],
"failed": [{ "login": "username2", "error": "already a member" }]
}Execution
Calls octokit.rest.teams.addOrUpdateMembershipForUserInOrg sequentially for each user. Max 50 users.
Auth note: Same non-standard auth pattern as bulk-role.
Organization summary
GET /api/[org]/organization/summary — requires session + org access.
Queries repositories, teams, and organization_memberships tables via the Supabase admin client. Three parallel reads keyed by organization_id.
Response:
{
"repositories": 42,
"teams": 7,
"members": 130
}Teams members
GET /api/[org]/teams/members — requires session + org access.
Queries teams and team_members tables. Returns per-team member counts.
Response:
[
{ "teamSlug": "frontend", "memberCount": 8 },
{ "teamSlug": "backend", "memberCount": 12 }
]Note: Uses N+1 query pattern — issues one Supabase
countquery per team instead of a singleGROUP BY. Performance impact is negligible for orgs with fewer than 100 teams.
Installation access
GET /api/[org]/installation/access — requires session + org access.
Resolves the org’s installation and reads granted permissions authoritatively from GitHub via apps.getInstallation. Compares against REQUIRED_INSTALLATION_PERMISSIONS using findMissingPermissions() (lib/github/required-permissions.ts). Falls back to Supabase cache on GitHub API error.
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"
}Install an app on an org
If no installation exists: GET /api/install/start begins the flow. This redirects to GitHub’s app installation page. After approval, GitHub redirects to /api/install/callback?installation_id=<id>, which links the installation to the user’s session.
Related
- Installation — the full install lifecycle
- Organization Management Guide — UI walkthrough
- API Reference — full route map