Docs /features/github-integration

GitHub Integration

OAuth authentication and repository access

📋 Overview

GitHub Integration provides OAuth-based authentication using Better Auth, enabling users to log in with their GitHub account and access their repositories for analysis and test generation.

🎯 Current Status

Status: ✅ Implemented

GitHub OAuth is fully functional for authentication. The GitHub App side (installation-scoped webhooks, Check Runs, sticky PR comments, and the PR-event delta orchestrator) is also live; see apps/api/src/routes/githubApp.ts and packages/trpc/src/services/prDeltaOrchestrator.ts.

🏗️ Authentication Architecture

Better Auth Configuration

Location: packages/auth/src/index.ts

import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: 'pg'
  }),
  
  socialProviders: {
    github: {
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
      redirectURI: process.env.GITHUB_REDIRECT_URI || 
        `${process.env.BETTER_AUTH_URL}/auth/callback/github`
    }
  },
  
  session: {
    expiresIn: 60 * 60 * 24 * 7, // 7 days
    updateAge: 60 * 60 * 24 // 1 day
  }
})

🔐 OAuth Flow

1. User Clicks "Sign in with GitHub"

// Frontend button
<Button asChild>
  <a href={`${API_URL}/api/auth/github`}>
    <GithubIcon /> Sign in with GitHub
  </a>
</Button>

2. Redirect to GitHub

GET https://github.com/login/oauth/authorize
  ?client_id=YOUR_CLIENT_ID
  &redirect_uri=https://api.yourdomain.com/auth/callback/github
  &scope=read:user,user:email,repo

3. GitHub Callback

// Better Auth handles this automatically
// Exchanges code for access token
// Creates/updates user in database
// Creates session
// Redirects to frontend

4. Frontend Receives Session

// Frontend callback handler
export async function GitHubCallback() {
  const searchParams = useSearchParams()
  const session = searchParams.get('session')
  
  if (session) {
    // Store session
    localStorage.setItem('session', session)
    // Redirect to dashboard
    navigate('/dashboard')
  }
}

🔌 API Integration

Database Schema

accounts table (Better Auth):

{
  id: string
  userId: string
  providerId: 'github'
  accountId: string         // GitHub user ID
  accessToken: string       // OAuth access token
  refreshToken: string?
  accessTokenExpiresAt: Date?
  scope: string
  createdAt: Date
  updatedAt: Date
}

Accessing GitHub API

import { Octokit } from '@octokit/rest'

async function getUserRepositories(userId: string) {
  // Get GitHub access token from accounts table
  const account = await db.query.accounts.findFirst({
    where: and(
      eq(accounts.userId, userId),
      eq(accounts.providerId, 'github')
    )
  })
  
  if (!account?.accessToken) {
    throw new Error('GitHub not connected')
  }
  
  // Use Octokit with user's token
  const octokit = new Octokit({
    auth: account.accessToken
  })
  
  const { data } = await octokit.repos.listForAuthenticatedUser({
    sort: 'updated',
    per_page: 100
  })
  
  return data
}

🎨 Frontend Implementation

GitHub Sign In Button

import { GithubIcon } from 'lucide-react'
import { Button } from '~/components/ui/button'

export function GitHubSignIn() {
  const apiUrl = import.meta.env.VITE_API_URL

  return (
    <Button asChild variant="outline" className="w-full">
      <a href={`${apiUrl}/api/auth/github`}>
        <GithubIcon className="mr-2 h-4 w-4" />
        Sign in with GitHub
      </a>
    </Button>
  )
}

Repository Selector

export function RepositorySelector({ onSelect }: Props) {
  const { data, isLoading } = trpc.github.listRepositories.useQuery()

  return (
    <Command>
      <CommandInput placeholder="Search repositories..." />
      <CommandList>
        {isLoading && <CommandLoading />}
        
        <CommandGroup heading="Your Repositories">
          {data?.repositories.map((repo) => (
            <CommandItem
              key={repo.id}
              onSelect={() => onSelect(repo)}
            >
              <GithubIcon className="mr-2" />
              <div>
                <div>{repo.name}</div>
                <div className="text-sm text-muted-foreground">
                  {repo.fullName}
                </div>
              </div>
            </CommandItem>
          ))}
        </CommandGroup>
      </CommandList>
    </Command>
  )
}

🪝 Webhooks (Planned)

GitHub Webhook Setup

Planned for Phase 4:

// Register webhook with GitHub
POST https://api.github.com/repos/owner/repo/hooks
{
  "config": {
    "url": "https://api.yourdomain.com/webhooks/github",
    "content_type": "json",
    "secret": "your_webhook_secret"
  },
  "events": ["push", "pull_request"],
  "active": true
}

Webhook Handler (Planned)

app.post('/webhooks/github', async (c) => {
  const signature = c.req.header('x-hub-signature-256')
  const payload = await c.req.json()
  
  // Verify signature
  if (!verifySignature(payload, signature)) {
    return c.json({ error: 'Invalid signature' }, 401)
  }
  
  // Handle event
  if (payload.action === 'push') {
    // Trigger analysis for new commit
    await analyzeRepository({
      repoUrl: payload.repository.clone_url,
      commitSha: payload.after
    })
  }
  
  return c.json({ received: true })
})

🔧 Configuration

Environment Variables

# GitHub OAuth App
GITHUB_CLIENT_ID=Iv1.1234567890abcdef
GITHUB_CLIENT_SECRET=1234567890abcdef1234567890abcdef12345678
GITHUB_REDIRECT_URI=https://api.yourdomain.com/auth/callback/github

# Better Auth
BETTER_AUTH_SECRET=your_32_char_secret_here
BETTER_AUTH_URL=https://api.yourdomain.com

GitHub OAuth App Setup

  1. Go to GitHub Settings → Developer Settings → OAuth Apps
  2. Click "New OAuth App"
  3. Fill in details:
    • Application name: Izri
    • Homepage URL: https://yourdomain.com
    • Authorization callback URL: https://api.yourdomain.com/auth/callback/github
  4. Copy Client ID and Client Secret
  5. Add to environment variables

Required Scopes

read:user      # Read user profile
user:email     # Read user email
repo           # Access repositories (for private repos)

🔐 Security

Token Storage

  • Access tokens stored encrypted in database
  • Never exposed to frontend
  • Used only on backend for API calls

Token Refresh

Better Auth handles token refresh automatically when:

  • Access token expires
  • Refresh token is available

Webhook Security

import crypto from 'node:crypto'

function verifySignature(payload: any, signature: string): boolean {
  const secret = process.env.GITHUB_WEBHOOK_SECRET
  const hmac = crypto.createHmac('sha256', secret)
  const digest = 'sha256=' + hmac.update(JSON.stringify(payload)).digest('hex')
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest))
}

✅ Testing

Mock GitHub OAuth

describe('GitHub OAuth', () => {
  it('creates user after GitHub callback', async () => {
    const mockGitHubUser = {
      id: 123456,
      login: 'testuser',
      email: 'test@example.com'
    }
    
    // Simulate OAuth callback
    const user = await handleGitHubCallback(mockGitHubUser)
    
    expect(user.email).toBe('test@example.com')
    expect(user.accounts[0].providerId).toBe('github')
  })
})

🔗 Related Documentation


Next: API Keys

Reading this with an agent? /docs/features/github-integration.md serves the raw markdown.

All docs