$npx -y skills add AgentWorkforce/relay --skill github-oauth-nango-integrationUse when implementing GitHub OAuth + GitHub App authentication with Nango - provides two-connection pattern for user login and repo access with webhook handling
| 1 | # GitHub OAuth + Nango Integration |
| 2 | |
| 3 | ## Overview |
| 4 | |
| 5 | Implements dual-connection OAuth pattern: one for user identity (`github` integration), another for repository access (`github-app-oauth` integration). This separation enables secure login while maintaining granular repo permissions through GitHub App installations. |
| 6 | |
| 7 | ## When to Use |
| 8 | |
| 9 | - Setting up GitHub OAuth login via Nango |
| 10 | - Implementing GitHub App installation webhooks |
| 11 | - Reconciling OAuth users with GitHub App installations |
| 12 | - Building apps that need both user auth and repo access |
| 13 | - Handling Nango sync webhooks for GitHub data |
| 14 | |
| 15 | ## Why Two Connections? |
| 16 | |
| 17 | GitHub has **two different authentication mechanisms** that serve different purposes: |
| 18 | |
| 19 | ### GitHub OAuth App (`github` integration) |
| 20 | |
| 21 | - **What it is**: Traditional OAuth for user identity |
| 22 | - **What it gives you**: User profile (name, email, avatar, GitHub ID) |
| 23 | - **What it DOESN'T give you**: Access to repositories |
| 24 | - **Use for**: Login, "Sign in with GitHub" |
| 25 | |
| 26 | ### GitHub App (`github-app-oauth` integration) |
| 27 | |
| 28 | - **What it is**: Installable app with granular repo permissions |
| 29 | - **What it gives you**: Access to specific repos the user installed it on |
| 30 | - **What it DOESN'T give you**: User identity (it knows the installation, not who's using it) |
| 31 | - **Use for**: Reading PRs, commits, files; posting comments; webhooks |
| 32 | |
| 33 | ### The Reconciliation Problem |
| 34 | |
| 35 | ``` |
| 36 | OAuth App alone: "User john@example.com logged in" → but which repos can they access? |
| 37 | GitHub App alone: "Installation #12345 has access to repo X" → but who is the user? |
| 38 | ``` |
| 39 | |
| 40 | **Solution**: Two separate OAuth flows linked by user ID: |
| 41 | |
| 42 | 1. **Login flow** → User authenticates → Store user identity + `nangoConnectionId` |
| 43 | 2. **Repo flow** → Same user authorizes app → Store repos + link via `ownerId` |
| 44 | |
| 45 | This lets you answer: "User john@example.com can access repos X, Y, Z" |
| 46 | |
| 47 | ## Quick Reference |
| 48 | |
| 49 | | Connection Type | Nango Integration | Purpose | Stored In | |
| 50 | | --------------- | ------------------ | -------------------------- | ------------------------- | |
| 51 | | User Login | `github` | Authentication, identity | `users.nangoConnectionId` | |
| 52 | | Repo Access | `github-app-oauth` | PR operations, file access | `repos.nangoConnectionId` | |
| 53 | |
| 54 | | Flow | Endpoint | Webhook Type | |
| 55 | | ------------ | ------------------------------ | --------------------------- | |
| 56 | | Login | `GET /auth/nango-session` | `auth` + `github` | |
| 57 | | Repo Connect | `GET /auth/github-app-session` | `auth` + `github-app-oauth` | |
| 58 | | Data Sync | N/A (scheduled) | `sync` | |
| 59 | |
| 60 | ## Implementation |
| 61 | |
| 62 | ### 1. Database Schema |
| 63 | |
| 64 | ```typescript |
| 65 | // users table - stores login connection |
| 66 | export const users = pgTable('users', { |
| 67 | id: uuid('id').primaryKey().defaultRandom(), |
| 68 | githubId: text('github_id').unique().notNull(), |
| 69 | githubUsername: text('github_username').notNull(), |
| 70 | email: text('email'), |
| 71 | avatarUrl: text('avatar_url'), |
| 72 | nangoConnectionId: text('nango_connection_id'), // Permanent login connection |
| 73 | incomingConnectionId: text('incoming_connection_id'), // Temp polling connection |
| 74 | pendingInstallationRequest: timestamp('pending_installation_request'), // Org approval wait |
| 75 | }); |
| 76 | |
| 77 | // repos table - stores per-repo app connection |
| 78 | export const repos = pgTable('repos', { |
| 79 | id: uuid('id').primaryKey().defaultRandom(), |
| 80 | githubRepoId: text('github_repo_id').unique().notNull(), |
| 81 | fullName: text('full_name').notNull(), |
| 82 | installationId: uuid('installation_id').references(() => githubInstallations.id), |
| 83 | ownerId: uuid('owner_id').references(() => users.id), |
| 84 | nangoConnectionId: text('nango_connection_id'), // App connection for this repo |
| 85 | }); |
| 86 | |
| 87 | // github_installations - tracks app installations |
| 88 | export const githubInstallations = pgTable('github_installations', { |
| 89 | id: uuid('id').primaryKey().defaultRandom(), |
| 90 | installationId: text('installation_id').unique().notNull(), |
| 91 | accountType: text('account_type'), // 'user' | 'organization' |
| 92 | accountLogin: text('account_login'), |
| 93 | installedById: uuid('installed_by_id').references(() => users.id), |
| 94 | }); |
| 95 | ``` |
| 96 | |
| 97 | ### 2. Constants |
| 98 | |
| 99 | ```typescript |
| 100 | // constants.ts |
| 101 | export const NANGO_INTEGRATION = { |
| 102 | GITHUB_USER: 'github', // Login only |
| 103 | GITHUB_APP_OAUTH: 'github-app-oauth', // Repo access |
| 104 | } as const; |
| 105 | ``` |
| 106 | |
| 107 | ### 3. Login Flow Routes |
| 108 | |
| 109 | ```typescript |
| 110 | // GET /auth/nango-session - Create login OAuth session |
| 111 | app.get('/auth/nango-session', async (c) => { |
| 112 | const tempUserId = randomUUID(); |
| 113 | |
| 114 | const { sessionToken } = await nangoClient.createConnectSession({ |
| 115 | end_user: { id: tempUserId }, |
| 116 | allowed_integrations: [NANGO_INTEGRATION.GITHUB_USER], |
| 117 | }); |
| 118 | |
| 119 | return c.json({ sessionToken, tempUserId }); |
| 120 | }); |
| 121 | |
| 122 | // GET /auth/nango/status/:connectionId - Poll login completion |
| 123 | app. |