-
-
-
handleCardClick('local')}
- tabIndex={0}
- role="button"
- aria-label="Import local project"
- >
-
-
-
-
-
- Import a Local Project
-
- Select a directory from your computer to start working with your project in Onlook.
-
-
-
-
- {/* Temporary disabled */}
-
false && handleCardClick('github')}
- tabIndex={0}
- role="button"
- aria-label="Connect to GitHub"
- >
-
-
-
-
-
- Import from GitHub
-
- Connect your GitHub account to access and work with your repositories
-
-
-
-
-
+
+
handleCardClick('local')}
+ tabIndex={0}
+ role="button"
+ aria-label="Import local project"
+ >
+
+
+
+
+
+ Import a Local Project
+
+ Select a directory from your computer to start working with your project
+ in Onlook.
+
+
+
+
+
handleCardClick('github')}
+ tabIndex={0}
+ role="button"
+ aria-label="Connect to GitHub"
+ >
+
+
+
+
+
+ Import from GitHub
+
+ Connect your GitHub account to access and work with your repositories
+
+
+
+
);
};
diff --git a/apps/web/client/src/server/api/routers/github.ts b/apps/web/client/src/server/api/routers/github.ts
index 907b5f2491..465a5d4fe2 100644
--- a/apps/web/client/src/server/api/routers/github.ts
+++ b/apps/web/client/src/server/api/routers/github.ts
@@ -1,17 +1,19 @@
-import { users, type DrizzleDb } from '@onlook/db';
-import {
- createInstallationOctokit,
- generateInstallationUrl
-} from '@onlook/github';
+import { Octokit } from '@octokit/rest';
import { TRPCError } from '@trpc/server';
import { eq } from 'drizzle-orm';
import { z } from 'zod';
+
+import type { DrizzleDb } from '@onlook/db';
+import { users } from '@onlook/db';
+import { createInstallationOctokit, generateInstallationUrl } from '@onlook/github';
+
+import { createClient } from '@/utils/supabase/server';
import { createTRPCRouter, protectedProcedure } from '../trpc';
const getUserGitHubInstallation = async (db: DrizzleDb, userId: string) => {
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
- columns: { githubInstallationId: true }
+ columns: { githubInstallationId: true },
});
if (!user?.githubInstallationId) {
@@ -22,7 +24,28 @@ const getUserGitHubInstallation = async (db: DrizzleDb, userId: string) => {
}
return {
octokit: createInstallationOctokit(user.githubInstallationId),
- installationId: user.githubInstallationId
+ installationId: user.githubInstallationId,
+ };
+};
+
+const getUserGitHubOAuth = async () => {
+ const supabase = await createClient();
+ const {
+ data: { session },
+ } = await supabase.auth.getSession();
+
+ const providerToken = session?.provider_token;
+
+ if (!providerToken) {
+ throw new TRPCError({
+ code: 'PRECONDITION_FAILED',
+ message: 'GitHub OAuth access required',
+ });
+ }
+
+ return {
+ octokit: new Octokit({ auth: providerToken }),
+ token: providerToken,
};
};
@@ -31,7 +54,7 @@ export const githubRouter = createTRPCRouter({
.input(
z.object({
owner: z.string(),
- repo: z.string()
+ repo: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
@@ -39,63 +62,101 @@ export const githubRouter = createTRPCRouter({
const { data } = await octokit.rest.repos.get({ owner: input.owner, repo: input.repo });
return {
branch: data.default_branch,
- isPrivateRepo: data.private
+ isPrivateRepo: data.private,
};
}),
+ validateWithOAuth: protectedProcedure
+ .input(
+ z.object({
+ owner: z.string(),
+ repo: z.string(),
+ }),
+ )
+ .mutation(async ({ input }) => {
+ try {
+ const { octokit } = await getUserGitHubOAuth();
+ const { data } = await octokit.rest.repos.get({
+ owner: input.owner,
+ repo: input.repo,
+ });
+ return {
+ branch: data.default_branch,
+ isPrivateRepo: data.private,
+ hasAccess: true,
+ };
+ } catch (error) {
+ throw new TRPCError({
+ code: 'FORBIDDEN',
+ message: 'Unable to access this repository. Please check your permissions.',
+ cause: error,
+ });
+ }
+ }),
getRepo: protectedProcedure
.input(
z.object({
owner: z.string(),
- repo: z.string()
+ repo: z.string(),
}),
)
.query(async ({ input, ctx }) => {
const { octokit } = await getUserGitHubInstallation(ctx.db, ctx.user.id);
const { data } = await octokit.rest.repos.get({
owner: input.owner,
- repo: input.repo
+ repo: input.repo,
});
return data;
}),
- getOrganizations: protectedProcedure
- .query(async ({ ctx }) => {
- try {
- const { octokit, installationId } = await getUserGitHubInstallation(ctx.db, ctx.user.id);
+ getOrganizations: protectedProcedure.query(async ({ ctx }) => {
+ try {
+ const { octokit, installationId } = await getUserGitHubInstallation(
+ ctx.db,
+ ctx.user.id,
+ );
- // Get installation details to determine account type
- const installation = await octokit.rest.apps.getInstallation({
- installation_id: parseInt(installationId, 10),
- });
+ // Get installation details to determine account type
+ const installation = await octokit.rest.apps.getInstallation({
+ installation_id: parseInt(installationId, 10),
+ });
- // If installed on an organization, return that organization
- if (installation.data.account && 'type' in installation.data.account && installation.data.account.type === 'Organization') {
- return [{
+ // If installed on an organization, return that organization
+ if (
+ installation.data.account &&
+ 'type' in installation.data.account &&
+ installation.data.account.type === 'Organization'
+ ) {
+ return [
+ {
id: installation.data.account.id,
- login: 'login' in installation.data.account ? installation.data.account.login : (installation.data.account as any).name || '',
+ login:
+ 'login' in installation.data.account
+ ? installation.data.account.login
+ : (installation.data.account as any).name || '',
avatar_url: installation.data.account.avatar_url,
description: undefined, // Organizations don't have descriptions in this context
- }];
- }
-
- // If installed on a user account, return empty (no organizations)
- return [];
- } catch (error) {
- throw new TRPCError({
- code: 'FORBIDDEN',
- message: 'GitHub App installation is invalid or has been revoked',
- cause: error
- });
+ },
+ ];
}
- }),
+
+ // If installed on a user account, return empty (no organizations)
+ return [];
+ } catch (error) {
+ throw new TRPCError({
+ code: 'FORBIDDEN',
+ message: 'GitHub App installation is invalid or has been revoked',
+ cause: error,
+ });
+ }
+ }),
getRepoFiles: protectedProcedure
.input(
z.object({
owner: z.string(),
repo: z.string(),
path: z.string().default(''),
- ref: z.string().optional() // branch, tag, or commit SHA
- })
+ ref: z.string().optional(), // branch, tag, or commit SHA
+ }),
)
.query(async ({ input, ctx }) => {
const { octokit } = await getUserGitHubInstallation(ctx.db, ctx.user.id);
@@ -103,15 +164,17 @@ export const githubRouter = createTRPCRouter({
owner: input.owner,
repo: input.repo,
path: input.path,
- ...(input.ref && { ref: input.ref })
+ ...(input.ref && { ref: input.ref }),
});
return data;
}),
generateInstallationUrl: protectedProcedure
.input(
- z.object({
- redirectUrl: z.string().optional(),
- }).optional()
+ z
+ .object({
+ redirectUrl: z.string().optional(),
+ })
+ .optional(),
)
.mutation(async ({ input, ctx }) => {
const { url, state } = generateInstallationUrl({
@@ -122,34 +185,47 @@ export const githubRouter = createTRPCRouter({
return { url, state };
}),
- checkGitHubAppInstallation: protectedProcedure
- .query(async ({ ctx }): Promise
=> {
+ checkGitHubAppInstallation: protectedProcedure.query(
+ async ({ ctx }): Promise => {
try {
- const { octokit, installationId } = await getUserGitHubInstallation(ctx.db, ctx.user.id);
+ const { octokit, installationId } = await getUserGitHubInstallation(
+ ctx.db,
+ ctx.user.id,
+ );
await octokit.rest.apps.getInstallation({
installation_id: parseInt(installationId, 10),
});
return installationId;
} catch (error) {
- console.error('Error checking GitHub App installation:', error);
- throw new TRPCError({
- code: 'FORBIDDEN',
- message: error instanceof Error ? error.message : 'GitHub App installation is invalid or has been revoked',
- cause: error
- });
+ // If user doesn't have an installation, return null (not an error)
+ if (error instanceof TRPCError && error.code === 'PRECONDITION_FAILED') {
+ return null;
+ }
+ // For other errors (invalid installation, revoked, etc.), return null as well
+ // This is a "check" endpoint, so it should gracefully return null instead of throwing
+ console.warn(
+ 'GitHub App installation check failed:',
+ error instanceof Error ? error.message : error,
+ );
+ return null;
}
- }),
+ },
+ ),
- // Repository fetching using GitHub App installation (required)
getRepositoriesWithApp: protectedProcedure
.input(
- z.object({
- username: z.string().optional(),
- }).optional()
+ z
+ .object({
+ username: z.string().optional(),
+ })
+ .optional(),
)
.query(async ({ ctx }) => {
try {
- const { octokit, installationId } = await getUserGitHubInstallation(ctx.db, ctx.user.id);
+ const { octokit, installationId } = await getUserGitHubInstallation(
+ ctx.db,
+ ctx.user.id,
+ );
const { data } = await octokit.rest.apps.listReposAccessibleToInstallation({
installation_id: parseInt(installationId, 10),
@@ -158,7 +234,7 @@ export const githubRouter = createTRPCRouter({
});
// Transform to match reference implementation pattern
- return data.repositories.map(repo => ({
+ return data.repositories.map((repo) => ({
id: repo.id,
name: repo.name,
full_name: repo.full_name,
@@ -176,8 +252,9 @@ export const githubRouter = createTRPCRouter({
} catch (error) {
throw new TRPCError({
code: 'FORBIDDEN',
- message: 'GitHub App installation is invalid or has been revoked. Please reinstall the GitHub App.',
- cause: error
+ message:
+ 'GitHub App installation is invalid or has been revoked. Please reinstall the GitHub App.',
+ cause: error,
});
}
}),
@@ -187,7 +264,7 @@ export const githubRouter = createTRPCRouter({
installationId: z.string(),
setupAction: z.string(),
state: z.string(),
- })
+ }),
)
.mutation(async ({ input, ctx }) => {
// Validate state parameter matches current user ID for CSRF protection
@@ -201,7 +278,8 @@ export const githubRouter = createTRPCRouter({
// Update user's GitHub installation ID
try {
- await ctx.db.update(users)
+ await ctx.db
+ .update(users)
.set({ githubInstallationId: input.installationId })
.where(eq(users.id, ctx.user.id));
@@ -212,7 +290,6 @@ export const githubRouter = createTRPCRouter({
message: 'GitHub App installation completed successfully',
installationId: input.installationId,
};
-
} catch (error) {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
@@ -222,4 +299,58 @@ export const githubRouter = createTRPCRouter({
}
}),
-});
\ No newline at end of file
+ getRepositoriesWithOAuth: protectedProcedure
+ .input(
+ z
+ .object({
+ page: z.number().default(1),
+ perPage: z.number().default(100),
+ })
+ .optional(),
+ )
+ .query(async ({ input }) => {
+ try {
+ const { octokit } = await getUserGitHubOAuth();
+
+ const { data: user } = await octokit.rest.users.getAuthenticated();
+
+ const { data: allRepos } = await octokit.rest.repos.listForAuthenticatedUser({
+ per_page: input?.perPage ?? 100,
+ page: input?.page ?? 1,
+ sort: 'updated',
+ affiliation: 'owner,collaborator,organization_member',
+ });
+
+ // Filter to only include repos owned by the user or organizations
+ // Exclude repos owned by other individual users
+ const repos = allRepos.filter((repo) => {
+ const isOwnedByAuthUser = repo.owner.login === user.login;
+ const isOrganization = repo.owner.type === 'Organization';
+ return isOwnedByAuthUser || isOrganization;
+ });
+
+ // Extract unique organizations from repository owners
+ const ownerMap = new Map();
+ repos.forEach((repo) => {
+ if (!ownerMap.has(repo.owner.login)) {
+ ownerMap.set(repo.owner.login, {
+ id: repo.owner.id,
+ login: repo.owner.login,
+ avatar_url: repo.owner.avatar_url,
+ });
+ }
+ });
+
+ const organizations = Array.from(ownerMap.values());
+
+ return { repos, organizations };
+ } catch (error) {
+ throw new TRPCError({
+ code: 'FORBIDDEN',
+ message:
+ 'GitHub OAuth access is invalid or has been revoked. Please reconnect your GitHub account.',
+ cause: error,
+ });
+ }
+ }),
+});
diff --git a/docs/github-app-setup-testing.md b/docs/github-app-setup-testing.md
new file mode 100644
index 0000000000..742573b21a
--- /dev/null
+++ b/docs/github-app-setup-testing.md
@@ -0,0 +1,171 @@
+# GitHub App Setup for Testing
+
+This guide will help you create a test GitHub App to develop and test the GitHub import functionality.
+
+## Prerequisites
+
+- A GitHub account
+- Access to create GitHub Apps (personal account or organization)
+- Local development environment running
+
+## Step 1: Create a New GitHub App
+
+1. Go to GitHub Settings:
+ - **Personal account**: https://github.com/settings/apps
+ - **Organization**: https://github.com/organizations/YOUR_ORG/settings/apps
+
+2. Click **"New GitHub App"**
+
+3. Fill in the basic information:
+ - **GitHub App name**: `Onlook Test App` (or similar)
+ - **Homepage URL**: `http://localhost:3000` (your local dev URL)
+ - **Callback URL**: `http://localhost:3000/callback/github/install`
+ - **Setup URL**: Leave blank
+ - **Webhook URL**: Leave blank for now (or use ngrok for local testing)
+ - **Webhook secret**: Leave blank for testing
+
+## Step 2: Configure Permissions
+
+The app needs the following permissions:
+
+### Repository Permissions
+- **Contents**: Read-only (to read repository files and clone)
+- **Metadata**: Read-only (automatic, for basic repo info)
+
+### Account Permissions
+- **Email addresses**: Read-only (to get user email)
+
+### Where can this GitHub App be installed?
+- Select **"Any account"** for testing
+
+## Step 3: Generate Private Key
+
+1. After creating the app, scroll down to **"Private keys"**
+2. Click **"Generate a private key"**
+3. A `.pem` file will be downloaded - **save this securely**
+
+## Step 4: Note Your App Credentials
+
+You'll need these values:
+- **App ID**: Found at the top of your app's settings page
+- **Client ID**: Found in the "About" section
+- **App Slug**: The URL-friendly name (e.g., `onlook-test-app`)
+- **Private Key**: The `.pem` file you downloaded
+
+## Step 5: Convert Private Key (if needed)
+
+The private key needs to be in PKCS#8 format. Check the key format:
+
+```bash
+# If the key starts with "-----BEGIN RSA PRIVATE KEY-----", convert it:
+cd packages/github
+bun run convert-key path/to/your-downloaded-key.pem -out path/to/converted-key.pem
+```
+
+## Step 6: Configure Environment Variables
+
+1. Copy the example env file (if you haven't already):
+```bash
+cp apps/web/client/.env.example apps/web/client/.env.local
+```
+
+2. Add your GitHub App credentials to `.env.local`:
+
+```bash
+# GitHub App Configuration
+GITHUB_APP_ID="123456"
+GITHUB_APP_SLUG="onlook-test-app"
+GITHUB_APP_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC...
+...your full private key here (with newlines)...
+-----END PRIVATE KEY-----"
+```
+
+**Important**: The private key must include the full multi-line string with BEGIN/END markers.
+
+## Step 7: Install the App
+
+1. Start your local development server:
+```bash
+bun run dev
+```
+
+2. Navigate to the GitHub import flow: `http://localhost:3000/projects/import/github`
+
+3. Click the "Connect GitHub" or "Install GitHub App" button
+
+4. You'll be redirected to GitHub to authorize the app
+
+5. Select which repositories the app can access:
+ - **All repositories** (for testing)
+ - **Only select repositories** (choose test repos)
+
+6. Click **"Install"**
+
+7. You'll be redirected back to your local app
+
+## Step 8: Verify Installation
+
+Check that the installation worked:
+
+1. The callback page should show success
+2. Check your database - the `users` table should have a `githubInstallationId` for your user
+3. Try fetching repositories in the import UI
+
+## Troubleshooting
+
+### "Invalid credentials" or "401 Unauthorized"
+- Verify your App ID is correct
+- Check that the private key is in PKCS#8 format
+- Ensure the private key includes BEGIN/END markers
+
+### "Missing state parameter"
+- Clear your browser cookies/cache
+- Restart your dev server
+- Try the installation flow again
+
+### "GitHub App installation required"
+- The installation may not have completed
+- Check GitHub Settings > Applications > Installed GitHub Apps
+- Uninstall and try again
+
+### Private key format issues
+```bash
+# Check key format:
+head -1 your-key.pem
+
+# Should see: -----BEGIN PRIVATE KEY-----
+# If you see: -----BEGIN RSA PRIVATE KEY-----
+# Then convert it using the convert-key script
+```
+
+## Testing Checklist
+
+Once installed, test these scenarios:
+
+- [ ] Connect GitHub account
+- [ ] View list of repositories
+- [ ] Filter by organization
+- [ ] Search repositories
+- [ ] Import a small public repository
+- [ ] Import a small private repository
+- [ ] Handle installation errors
+- [ ] Uninstall and reinstall the app
+
+## Production Considerations
+
+When moving to production:
+
+1. Create a separate production GitHub App
+2. Update callback URL to production domain
+3. Store private key securely (use secrets manager)
+4. Enable webhook for future features
+5. Review and minimize permissions
+6. Set up proper error monitoring
+7. Add rate limit handling
+
+## Useful Links
+
+- GitHub Apps Documentation: https://docs.github.com/en/apps
+- Testing GitHub Apps: https://docs.github.com/en/apps/creating-github-apps/setting-up-a-github-app/creating-a-github-app
+- Octokit SDK (what we use): https://github.com/octokit/octokit.js
diff --git a/docs/github-import-production-plan.md b/docs/github-import-production-plan.md
new file mode 100644
index 0000000000..c3e6076457
--- /dev/null
+++ b/docs/github-import-production-plan.md
@@ -0,0 +1,315 @@
+# GitHub Import Functionality - Production Readiness Plan
+
+## Recent Updates
+
+### ✅ All-Scopes-Upfront OAuth (Completed)
+**Implementation Date**: 2025-01-23
+
+Implemented Vercel-style OAuth pattern - request all needed scopes during login:
+
+**Changes Made**:
+1. **Updated Login OAuth** (`apps/web/client/src/app/login/actions.tsx:30-31`):
+ - GitHub login now requests: `repo read:user user:email` scopes upfront
+ - Simpler flow - only one OAuth approval needed
+ - User sees all permissions at signup/login
+
+2. **Simplified Connect UI** (`apps/web/client/src/app/projects/import/github/_components/connect.tsx`):
+ - Only shows GitHub App installation step (OAuth already done at login)
+ - Clean, straightforward flow: Install App → Continue
+
+**Benefits**:
+- ✅ Simpler user flow - one OAuth approval
+- ✅ All permissions visible upfront at login
+- ✅ Matches Vercel/industry pattern
+- ✅ No separate OAuth step needed during import
+
+**Next Steps**:
+- Consider implementing OAuth-based repository listing (see `docs/github-oauth-setup.md` for guide)
+- This would allow users to see all repos they have access to (not just installed ones)
+
+---
+
+## Phase 1: Critical Fixes & Stability (High Priority)
+
+### 1.1 Pagination & Scalability
+**Issue**: Only fetches first 100 repos (hardcoded limit)
+- Implement pagination for `getRepositoriesWithApp` endpoint
+- Add infinite scroll or "Load More" in UI (`setup.tsx:244`)
+- Handle users with 100+ repositories
+- **Files**: `github.ts:144-183`, `setup.tsx:59-66`
+
+### 1.2 Environment Configuration Validation
+**Issue**: GitHub env vars are optional, can cause runtime failures
+- Make GitHub App env vars required when feature is enabled
+- Add startup validation in `config.ts:22-34`
+- Provide clear error messages when misconfigured
+- **Files**: `env.ts:60-62`, `config.ts`
+
+### 1.3 Timeout & Large Repository Handling
+**Issue**: 30s timeout may be insufficient for large repos
+- Increase timeout or make configurable
+- Add progress feedback during import
+- Implement streaming status updates
+- **Files**: `codesandbox/index.ts:176-201`, `sandbox.ts:182-226`
+
+### 1.4 Error Handling & Logging
+**Issue**: Console.error usage, poor error context
+- Replace console.error with proper logging/telemetry
+- Add structured error tracking (PostHog/Sentry integration)
+- Improve error messages for user troubleshooting
+- **Files**: All `_hooks/*.ts` files, `github.ts`
+
+## Phase 2: Feature Enhancements (Medium Priority)
+
+### 2.1 Branch Selection
+**Issue**: Always imports default branch
+- Add branch selector in setup UI
+- Fetch available branches via GitHub API
+- Store selected branch in context
+- **Files**: `setup.tsx`, `github.ts` (add `getBranches` endpoint)
+
+### 2.2 Repository Validation & Preview
+**Issue**: No pre-import validation
+- Validate repo size before import
+- Check for required files (package.json, etc.)
+- Show repository structure preview
+- Warn about large repositories
+- **Files**: New `use-repo-preview.ts`, `github.ts` (add validation endpoint)
+
+### 2.3 Import State Tracking
+**Issue**: No history of imports
+- Track imported repositories in database
+- Show import history in UI
+- Enable re-sync/update functionality
+- Detect duplicate imports
+- **Files**: New DB schema, new tRPC router
+
+### 2.4 Search Improvements
+**Issue**: Client-side only, fetches all repos first
+- Add server-side search via GitHub API
+- Debounce search queries
+- Cache repository list with TTL
+- **Files**: `github.ts` (modify getRepositoriesWithApp), `use-data.ts`
+
+## Phase 3: Polish & Optimization (Lower Priority)
+
+### 3.1 Installation State Management
+**Issue**: Refetches on every window focus
+- Implement smart caching with TTL
+- Reduce unnecessary API calls
+- Add manual refresh button
+- **Files**: `use-installation.ts:18-20`
+
+### 3.2 UX Improvements
+- Extend callback page auto-close from 3s to 5s
+- Add "Close manually" button
+- Improve loading states with skeleton screens
+- Add progress indicators during import
+- **Files**: `install/page.tsx:60-63`, `finalizing.tsx`
+
+### 3.3 Advanced Features
+- Monorepo support (select specific packages)
+- Commit/tag selection (not just branches)
+- Bulk import multiple repos
+- Private repo access verification
+- Organization-wide settings
+
+## Phase 4: Testing & Monitoring
+
+### 4.1 Automated Testing
+- Unit tests for GitHub API integration
+- Integration tests for import flow
+- E2E tests for complete user journey
+- Edge case testing (large repos, rate limits, network failures)
+
+### 4.2 Monitoring & Observability
+- Track import success/failure rates
+- Monitor API latency and timeouts
+- Alert on elevated error rates
+- Dashboard for GitHub App health
+
+### 4.3 Documentation
+- User-facing: How to set up GitHub App
+- Developer docs: Architecture overview
+- Troubleshooting guide
+- Security & permissions documentation
+
+## Implementation Order (Suggested)
+
+1. **Week 1-2**: Phase 1 (Critical Fixes)
+2. **Week 3-4**: Phase 2.1-2.2 (Branch selection, validation)
+3. **Week 5**: Phase 2.3-2.4 (Tracking, search)
+4. **Week 6**: Phase 3 (Polish)
+5. **Week 7-8**: Phase 4 (Testing, monitoring)
+
+## Key Files to Modify
+
+- `apps/web/client/src/server/api/routers/github.ts` - API logic
+- `apps/web/client/src/server/api/routers/project/sandbox.ts` - Import logic
+- `apps/web/client/src/app/projects/import/github/_components/setup.tsx` - UI
+- `apps/web/client/src/app/projects/import/github/_hooks/*` - State management
+- `packages/github/src/*` - GitHub integration
+- `packages/code-provider/src/providers/codesandbox/index.ts` - CodeSandbox integration
+
+---
+
+## Code Quality Issues (To Refactor)
+
+### Critical Priority 🔴
+
+#### 1. Hardcoded Pagination Limit (`github.ts:158-159`)
+```typescript
+per_page: 100,
+page: 1,
+```
+**Problem**: Users with 100+ repositories will never see them all.
+**Impact**: Feature completely broken for power users/large orgs.
+**Fix**: Implement pagination with cursor-based pagination or fetch all pages.
+
+#### 2. CSRF Validation Bug (`github.ts:196`)
+```typescript
+if (input.state && input.state !== ctx.user.id) {
+```
+**Problem**: If `input.state` is empty string/falsy, validation passes!
+**Impact**: Security vulnerability - CSRF protection can be bypassed.
+**Fix**: Change to `if (!input.state || input.state !== ctx.user.id)`
+
+#### 3. Excessive API Refetching (`use-installation.ts:19`)
+```typescript
+refetchOnWindowFocus: true,
+```
+**Problem**: Refetches every time user switches tabs/windows.
+**Impact**: Hammers API unnecessarily, poor performance.
+**Fix**: Remove or add `staleTime: 5 * 60 * 1000` (5 minutes).
+
+### High Priority 🟡
+
+#### 4. getUserGitHubInstallation Design Flaw (`github.ts:11-27`)
+```typescript
+if (!user?.githubInstallationId) {
+ throw new TRPCError({
+ code: 'PRECONDITION_FAILED',
+ message: 'GitHub App installation required',
+ });
+}
+```
+**Problem**: Forces every caller to handle exceptions for normal "not installed" case.
+**Impact**: Awkward error handling, checkGitHubAppInstallation hack needed.
+**Fix**: Return `null` or use Result type: `{ ok: true, data } | { ok: false, error }`
+
+#### 5. Unnecessary GitHub API Call (`github.ts:128-131`)
+```typescript
+const { octokit, installationId } = await getUserGitHubInstallation(ctx.db, ctx.user.id);
+await octokit.rest.apps.getInstallation({
+ installation_id: parseInt(installationId, 10),
+});
+```
+**Problem**: Wastes GitHub API rate limit just to validate installation exists.
+**Impact**: Rate limit exhaustion, slower response times.
+**Fix**: Just return installationId from database without validation call.
+
+#### 6. No Rate Limit Handling
+**Problem**: No handling for GitHub API rate limits (5000/hour).
+**Impact**: Will fail catastrophically when rate limited.
+**Fix**:
+- Catch 429 responses
+- Implement exponential backoff
+- Show user-friendly error messages
+- Cache responses
+
+#### 7. No Caching Strategy
+**Problem**: Every query hits DB + GitHub API, even for stable data.
+**Impact**: Poor performance, rate limit waste.
+**Fix**: Cache organizations/repos list for 5-10 minutes.
+
+### Medium Priority 🟠
+
+#### 8. Redundant Error State (`use-installation.ts:21-26`)
+```typescript
+const [error, setError] = useState(null);
+useEffect(() => {
+ setError(checkInstallationError?.message || null);
+}, [checkInstallationError]);
+```
+**Problem**: Duplicates React Query's built-in error state.
+**Impact**: Unnecessary complexity, potential state sync issues.
+**Fix**: Use `checkInstallationError?.message` directly in return statement.
+
+#### 9. Silent Error Swallowing (`use-installation.ts:42-44`)
+```typescript
+catch (error) {
+ console.error('Error generating GitHub App installation URL:', error);
+}
+```
+**Problem**: Errors logged but never shown to user.
+**Impact**: User has no feedback when installation flow fails.
+**Fix**: Set error state or show toast notification.
+
+#### 10. Unused Parameter (`github.ts:149`)
+```typescript
+username: z.string().optional(),
+```
+**Problem**: Accepted in schema but never used in function.
+**Impact**: Misleading API, confusing for developers.
+**Fix**: Remove parameter or implement filtering by username.
+
+#### 11. Type Casting with `as any` (`github.ts:75`)
+```typescript
+login: 'login' in installation.data.account ? installation.data.account.login : (installation.data.account as any).name || '',
+```
+**Problem**: Type safety bypass indicates upstream type issues.
+**Impact**: Will break if GitHub API changes, runtime errors.
+**Fix**: Properly type GitHub API responses or use type guards.
+
+#### 12. console.log/error/warn Throughout
+**Locations**: `github.ts:84, 140, 179, 197, 210` and all `_hooks/*.ts`
+**Problem**: Unstructured logging, no correlation IDs, hard to debug production.
+**Impact**: Poor observability, can't track issues in production.
+**Fix**: Use structured logging (PostHog events, Sentry, or logging library).
+
+### Low Priority 🟢
+
+#### 13. Dead Code (`use-installation.ts:34`)
+```typescript
+const finalRedirectUrl = redirectUrl;
+```
+**Problem**: Useless variable assignment.
+**Impact**: Code clutter.
+**Fix**: Remove variable, use `redirectUrl` directly.
+
+#### 14. Redundant Null Coalescing (`use-installation.ts:49`)
+```typescript
+installationId: installationId || null,
+```
+**Problem**: `installationId` is already `string | null` type.
+**Impact**: Unnecessary operation.
+**Fix**: Just return `installationId`.
+
+#### 15. Repeated Try-Catch Patterns
+**Problem**: Same error handling boilerplate in multiple endpoints.
+**Impact**: Code duplication, maintenance burden.
+**Fix**: Extract to middleware or helper function.
+
+---
+
+## Refactoring Priority Order
+
+1. **🔴 Critical** (Must fix before production):
+ - Fix pagination (Issue #1)
+ - Fix CSRF bug (Issue #2)
+ - Remove excessive refetching (Issue #3)
+
+2. **🟡 High** (Should fix soon):
+ - Refactor getUserGitHubInstallation (Issue #4)
+ - Remove unnecessary API call (Issue #5)
+ - Add rate limit handling (Issue #6)
+ - Add caching layer (Issue #7)
+
+3. **🟠 Medium** (Technical debt):
+ - Clean up hook error handling (Issues #8, #9)
+ - Fix unused parameters/types (Issues #10, #11)
+ - Replace console.* with structured logging (Issue #12)
+
+4. **🟢 Low** (Polish):
+ - Remove dead code (Issues #13, #14)
+ - Extract common patterns (Issue #15)
diff --git a/docs/github-oauth-setup.md b/docs/github-oauth-setup.md
new file mode 100644
index 0000000000..fafee2907b
--- /dev/null
+++ b/docs/github-oauth-setup.md
@@ -0,0 +1,482 @@
+# GitHub OAuth + App Hybrid Setup
+
+This guide explains how to add OAuth for repository discovery while keeping GitHub App for actual access (like Vercel does).
+
+## Architecture Overview
+
+### Two-Token System:
+
+1. **OAuth Token** (Discovery)
+ - Used to: List all repos user can see (owned, collaborator, org member)
+ - Permissions: Read-only access to user's repos
+ - Scope: `read:user`, `repo` (read)
+ - Stored: Per-user in database
+
+2. **GitHub App Installation Token** (Access)
+ - Used to: Actually import/access repos
+ - Permissions: Contents read-only (from app configuration)
+ - Scope: Only repos where app is installed
+ - Stored: Per-user installation ID in database
+
+### User Flow:
+
+```
+1. User connects GitHub (OAuth) → Gets list of ALL repos they can see
+2. User selects repo to import
+ ├─ Has app installed? → Import directly
+ └─ No app installed? → Prompt "Install app on this repo"
+3. User installs app → Now can import
+```
+
+---
+
+## Step 1: Enable OAuth in Your GitHub App
+
+### 1.1 Configure OAuth Settings
+
+1. Go to your GitHub App settings: https://github.com/settings/apps/onlook-test-app
+2. Scroll to **"Identifying and authorizing users"** section
+3. Fill in:
+ - **Callback URL**: `http://localhost:3000/api/auth/callback/github` (or your production URL)
+ - **Request user authorization (OAuth) during installation**: ☐ Leave **unchecked** (we want separate flows)
+
+### 1.2 Note Your OAuth Credentials
+
+From the app settings page:
+- **Client ID**: Found in "About" section
+- **Client Secret**: Click "Generate a new client secret"
+ - Copy and save it immediately (shown only once)
+
+---
+
+## Step 2: Add Environment Variables
+
+Add to your `.env` file:
+
+```bash
+# Existing GitHub App variables
+GITHUB_APP_ID=2167008
+GITHUB_APP_SLUG=onlook-test-app
+GITHUB_APP_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
+...
+-----END PRIVATE KEY-----"
+
+# New OAuth variables
+GITHUB_CLIENT_ID=Iv1.abc123def456
+GITHUB_CLIENT_SECRET=abc123def456ghi789jkl012mno345pqr678stu
+```
+
+---
+
+## Step 3: Update Environment Schema
+
+Edit `apps/web/client/src/env.ts`:
+
+```typescript
+server: {
+ // ... existing vars ...
+
+ // GitHub App
+ GITHUB_APP_ID: z.string().optional(),
+ GITHUB_APP_PRIVATE_KEY: z.string().optional(),
+ GITHUB_APP_SLUG: z.string().optional(),
+
+ // GitHub OAuth (new)
+ GITHUB_CLIENT_ID: z.string().optional(),
+ GITHUB_CLIENT_SECRET: z.string().optional(),
+},
+
+runtimeEnv: {
+ // ... existing vars ...
+
+ // GitHub
+ GITHUB_APP_ID: process.env.GITHUB_APP_ID,
+ GITHUB_APP_PRIVATE_KEY: process.env.GITHUB_APP_PRIVATE_KEY,
+ GITHUB_APP_SLUG: process.env.GITHUB_APP_SLUG,
+
+ // GitHub OAuth (new)
+ GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
+ GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
+}
+```
+
+---
+
+## Step 4: Update Database Schema
+
+Add OAuth token storage to your users table.
+
+In `packages/db/src/schema/user/user.ts`:
+
+```typescript
+export const users = pgTable('users', {
+ // ... existing fields ...
+
+ githubInstallationId: text('github_installation_id'), // existing
+ githubAccessToken: text('github_access_token'), // new - OAuth token
+ githubTokenExpiry: timestamp('github_token_expiry'), // new - when token expires
+});
+```
+
+Run migration:
+```bash
+bun run db:push
+```
+
+---
+
+## Step 5: Create OAuth Router
+
+Create `apps/web/client/src/server/api/routers/github-oauth.ts`:
+
+```typescript
+import { users } from '@onlook/db';
+import { TRPCError } from '@trpc/server';
+import { eq } from 'drizzle-orm';
+import { Octokit } from '@octokit/rest';
+import { z } from 'zod';
+import { createTRPCRouter, protectedProcedure } from '../trpc';
+import { env } from '@/env';
+
+export const githubOAuthRouter = createTRPCRouter({
+ // Generate OAuth authorization URL
+ getAuthUrl: protectedProcedure
+ .input(z.object({ redirectUrl: z.string().optional() }).optional())
+ .mutation(async ({ ctx, input }) => {
+ const params = new URLSearchParams({
+ client_id: env.GITHUB_CLIENT_ID!,
+ redirect_uri: `${env.NEXT_PUBLIC_SITE_URL}/api/auth/callback/github`,
+ scope: 'read:user,repo', // repo scope for read-only access
+ state: ctx.user.id, // CSRF protection
+ ...(input?.redirectUrl && { redirect_uri: input.redirectUrl }),
+ });
+
+ const url = `https://github.com/login/oauth/authorize?${params.toString()}`;
+ return { url };
+ }),
+
+ // Exchange code for access token
+ handleCallback: protectedProcedure
+ .input(z.object({
+ code: z.string(),
+ state: z.string(),
+ }))
+ .mutation(async ({ ctx, input }) => {
+ // Verify state matches user ID (CSRF protection)
+ if (input.state !== ctx.user.id) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'Invalid state parameter',
+ });
+ }
+
+ // Exchange code for access token
+ const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ },
+ body: JSON.stringify({
+ client_id: env.GITHUB_CLIENT_ID,
+ client_secret: env.GITHUB_CLIENT_SECRET,
+ code: input.code,
+ redirect_uri: `${env.NEXT_PUBLIC_SITE_URL}/api/auth/callback/github`,
+ }),
+ });
+
+ const tokenData = await tokenResponse.json();
+
+ if (tokenData.error) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: tokenData.error_description || 'Failed to get access token',
+ });
+ }
+
+ // Store token in database
+ await ctx.db.update(users)
+ .set({
+ githubAccessToken: tokenData.access_token,
+ // OAuth tokens don't expire by default, but you can add expiry if using refresh tokens
+ })
+ .where(eq(users.id, ctx.user.id));
+
+ return { success: true };
+ }),
+
+ // Get all repos user has access to (using OAuth token)
+ getAllRepositories: protectedProcedure
+ .input(z.object({
+ type: z.enum(['all', 'owner', 'member', 'collaborator']).default('all'),
+ sort: z.enum(['created', 'updated', 'pushed', 'full_name']).default('updated'),
+ per_page: z.number().min(1).max(100).default(30),
+ page: z.number().min(1).default(1),
+ }).optional())
+ .query(async ({ ctx, input }) => {
+ // Get user's OAuth token
+ const user = await ctx.db.query.users.findFirst({
+ where: eq(users.id, ctx.user.id),
+ columns: {
+ githubAccessToken: true,
+ githubInstallationId: true,
+ },
+ });
+
+ if (!user?.githubAccessToken) {
+ throw new TRPCError({
+ code: 'PRECONDITION_FAILED',
+ message: 'GitHub OAuth not connected. Please connect your GitHub account.',
+ });
+ }
+
+ // Create Octokit with OAuth token (NOT app token)
+ const octokit = new Octokit({
+ auth: user.githubAccessToken,
+ });
+
+ // Get all repos user has access to
+ const { data: repos } = await octokit.rest.repos.listForAuthenticatedUser({
+ type: input?.type || 'all',
+ sort: input?.sort || 'updated',
+ per_page: input?.per_page || 30,
+ page: input?.page || 1,
+ });
+
+ // Check which repos have app installed (by comparing with installation repos)
+ let installationRepos: string[] = [];
+ if (user.githubInstallationId) {
+ try {
+ // This would require app token - keeping it simple for now
+ // In production, you'd check installation repo access
+ installationRepos = []; // TODO: implement checking
+ } catch (error) {
+ console.warn('Could not check installation repos:', error);
+ }
+ }
+
+ // Transform and mark which repos have app installed
+ return repos.map(repo => ({
+ id: repo.id,
+ name: repo.name,
+ full_name: repo.full_name,
+ description: repo.description,
+ private: repo.private,
+ default_branch: repo.default_branch,
+ clone_url: repo.clone_url,
+ html_url: repo.html_url,
+ updated_at: repo.updated_at,
+ owner: {
+ login: repo.owner.login,
+ avatar_url: repo.owner.avatar_url,
+ },
+ permissions: repo.permissions,
+ // Mark if app is installed on this repo
+ hasAppInstalled: user.githubInstallationId ? installationRepos.includes(repo.full_name) : false,
+ }));
+ }),
+
+ // Check OAuth connection status
+ checkOAuthConnection: protectedProcedure
+ .query(async ({ ctx }) => {
+ const user = await ctx.db.query.users.findFirst({
+ where: eq(users.id, ctx.user.id),
+ columns: { githubAccessToken: true },
+ });
+
+ return {
+ isConnected: !!user?.githubAccessToken,
+ };
+ }),
+
+ // Revoke OAuth token
+ revokeOAuth: protectedProcedure
+ .mutation(async ({ ctx }) => {
+ await ctx.db.update(users)
+ .set({ githubAccessToken: null })
+ .where(eq(users.id, ctx.user.id));
+
+ return { success: true };
+ }),
+});
+```
+
+---
+
+## Step 6: Add OAuth Router to Root
+
+Edit `apps/web/client/src/server/api/root.ts`:
+
+```typescript
+import { githubRouter } from './routers/github';
+import { githubOAuthRouter } from './routers/github-oauth'; // new
+
+export const appRouter = createTRPCRouter({
+ // ... existing routers ...
+ github: githubRouter,
+ githubOAuth: githubOAuthRouter, // new
+});
+```
+
+---
+
+## Step 7: Create OAuth Callback Route
+
+Create `apps/web/client/src/app/api/auth/callback/github/route.ts`:
+
+```typescript
+import { NextRequest, NextResponse } from 'next/server';
+
+export async function GET(request: NextRequest) {
+ const searchParams = request.nextUrl.searchParams;
+ const code = searchParams.get('code');
+ const state = searchParams.get('state');
+ const error = searchParams.get('error');
+
+ if (error) {
+ return NextResponse.redirect(
+ `${process.env.NEXT_PUBLIC_SITE_URL}/projects/import/github?error=${error}`
+ );
+ }
+
+ if (!code || !state) {
+ return NextResponse.redirect(
+ `${process.env.NEXT_PUBLIC_SITE_URL}/projects/import/github?error=missing_params`
+ );
+ }
+
+ // Redirect to a page that will handle the token exchange via tRPC
+ return NextResponse.redirect(
+ `${process.env.NEXT_PUBLIC_SITE_URL}/projects/import/github/oauth-callback?code=${code}&state=${state}`
+ );
+}
+```
+
+Create `apps/web/client/src/app/projects/import/github/oauth-callback/page.tsx`:
+
+```typescript
+'use client';
+
+import { api } from '@/trpc/react';
+import { useRouter, useSearchParams } from 'next/navigation';
+import { useEffect } from 'react';
+
+export default function OAuthCallbackPage() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const exchangeToken = api.githubOAuth.handleCallback.useMutation();
+
+ useEffect(() => {
+ const code = searchParams.get('code');
+ const state = searchParams.get('state');
+
+ if (code && state) {
+ exchangeToken.mutate(
+ { code, state },
+ {
+ onSuccess: () => {
+ router.push('/projects/import/github');
+ },
+ onError: (error) => {
+ console.error('OAuth failed:', error);
+ router.push('/projects/import/github?error=oauth_failed');
+ },
+ }
+ );
+ }
+ }, []);
+
+ return (
+
+
+
Connecting to GitHub...
+
Please wait
+
+
+ );
+}
+```
+
+---
+
+## Step 8: Update UI to Use OAuth
+
+Now in your GitHub import UI, you'll have TWO connection states:
+
+1. **OAuth Connected** - Can browse all repos
+2. **App Installed** - Can actually import repos
+
+Example updated context:
+
+```typescript
+// In _context/index.tsx
+const { data: oauthStatus } = api.githubOAuth.checkOAuthConnection.useQuery();
+const { data: appInstallation } = api.github.checkGitHubAppInstallation.useQuery();
+
+// Show different UI based on connection state:
+// - No OAuth: Show "Connect GitHub" button
+// - OAuth but no App: Show repos with "Install App" button on each
+// - OAuth + App: Show repos, can import any with app installed
+```
+
+---
+
+## Usage Flow
+
+### For Users:
+
+1. **Connect GitHub (OAuth)**
+ ```typescript
+ const connectGitHub = async () => {
+ const { url } = await api.githubOAuth.getAuthUrl.mutate();
+ window.location.href = url;
+ };
+ ```
+
+2. **Browse All Repos**
+ ```typescript
+ const { data: allRepos } = api.githubOAuth.getAllRepositories.useQuery();
+ // Shows ALL repos user has access to
+ ```
+
+3. **Import Repo**
+ ```typescript
+ if (repo.hasAppInstalled) {
+ // Import directly
+ await importRepo(repo);
+ } else {
+ // Prompt to install app
+ const { url } = await api.github.generateInstallationUrl.mutate();
+ window.open(url);
+ }
+ ```
+
+---
+
+## Benefits of Hybrid Approach
+
+✅ **Better Discovery**: Users see all repos they have access to
+✅ **Clearer UX**: "You need to install the app on this repo"
+✅ **Flexible**: Works with repos user doesn't own (as collaborator)
+✅ **Secure**: Still uses App tokens for actual access
+✅ **Like Vercel**: Industry-standard pattern
+
+---
+
+## Security Considerations
+
+- 🔒 OAuth tokens stored encrypted in database
+- 🔒 State parameter for CSRF protection
+- 🔒 Separate scopes for discovery vs access
+- 🔒 Users can revoke OAuth independently of App
+- 🔒 OAuth tokens used ONLY for listing, not for importing
+
+---
+
+## Next Steps
+
+1. ✅ Complete all setup steps above
+2. ✅ Test OAuth flow end-to-end
+3. ✅ Update UI to show all repos with install status
+4. ✅ Add "Install App" buttons for repos without installation
+5. ✅ Test importing with OAuth + App combination
diff --git a/packages/github/README.md b/packages/github/README.md
index f9bc363333..eca26c51a5 100644
--- a/packages/github/README.md
+++ b/packages/github/README.md
@@ -1,23 +1,148 @@
# @onlook/github
-GitHub integration package for Onlook.
+GitHub integration package for Onlook that enables importing repositories from GitHub.
-## Setup
+## Creating a GitHub App
-### GitHub App Configuration
+### Step 1: Create New GitHub App
-You need to set these environment variables:
+1. Go to GitHub Settings:
+ - **Personal account**: https://github.com/settings/apps
+ - **Organization**: https://github.com/organizations/YOUR_ORG/settings/apps
-- `GITHUB_APP_ID` - Your GitHub App's ID
-- `GITHUB_APP_PRIVATE_KEY` - Your GitHub App's private key (PKCS#8 format)
-- `GITHUB_APP_SLUG` - Your GitHub App's slug name
+2. Click **"New GitHub App"**
-### Private Key Format
+3. Fill in basic information:
+ - **GitHub App name**: `Onlook` (or `Onlook Dev` for testing)
+ - **Homepage URL**: Your production URL or `http://localhost:3000` for local dev
+ - **Callback URL**: `https://yourdomain.com/callback/github/install` (or `http://localhost:3000/callback/github/install` for local)
+ - **Setup URL**: `https://yourdomain.com/callback/github/install` (or `http://localhost:3000/callback/github/install` for local)
+ - ✅ **Check** "Redirect on update"
+ - **Webhook**: Leave unchecked/inactive for now
+ - **Webhook URL**: Leave blank
+ - **Webhook secret**: Leave blank
-The GitHub App private key must be in PKCS#8 format. If you have a PKCS#1 key (starts with `-----BEGIN RSA PRIVATE KEY-----`), convert it using:
+### Step 2: Configure Permissions
+
+Set these permissions (all others should be "No access"):
+
+#### Repository Permissions
+- ✅ **Contents**: **Read-only**
+- ✅ **Metadata**: **Read-only** (automatic)
+
+#### Organization Permissions
+- ❌ All: **No access**
+
+#### Account Permissions
+- ❌ All: **No access**
+
+### Step 3: Configure Installation
+
+- **Where can this GitHub App be installed?**
+ - Select **"Any account"** (recommended)
+ - Or **"Only on this account"** for testing
+
+### Step 4: Post-Installation Settings
+
+- ☑️ **Expire user authorization tokens**: Checked (recommended)
+- ☐ **Request user authorization (OAuth) during installation**: Unchecked
+- ☐ **Enable Device Flow**: Unchecked
+
+### Step 5: Webhooks & Events
+
+- ☐ **Active**: Unchecked (no webhooks needed for basic import)
+- **Subscribe to events**: Leave all unchecked
+
+### Step 6: Generate Private Key
+
+1. After creating the app, scroll to **"Private keys"** section
+2. Click **"Generate a private key"**
+3. Save the downloaded `.pem` file securely
+
+### Step 7: Note Your Credentials
+
+From the GitHub App settings page, copy:
+- **App ID** (at the top of the page)
+- **App Slug** (in the URL: `github.com/apps/YOUR-SLUG-HERE`)
+- **Private Key** (the `.pem` file you downloaded)
+
+---
+
+## Environment Configuration
+
+### Step 1: Convert Private Key
+
+The private key must be in PKCS#8 format. Check the format:
```bash
-bun run convert-key path/to/your-key.pem -out path/to/converted-key.pem
+head -1 /path/to/your-key.pem
```
-Then use the contents of the converted key for the `GITHUB_APP_PRIVATE_KEY` environment variable.
\ No newline at end of file
+If it shows `-----BEGIN RSA PRIVATE KEY-----`, convert it:
+
+```bash
+cd packages/github
+openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt \
+ -in /path/to/your-key.pem \
+ -out /path/to/converted-key.pem
+```
+
+Verify the converted key shows `-----BEGIN PRIVATE KEY-----`:
+
+```bash
+head -1 /path/to/converted-key.pem
+```
+
+### Step 2: Add to Environment Variables
+
+Add these to your `.env` or `.env.local` file:
+
+```bash
+# GitHub App Configuration
+GITHUB_APP_ID="123456"
+GITHUB_APP_SLUG="your-app-slug"
+GITHUB_APP_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC...
+...your full private key here (with newlines)...
+-----END PRIVATE KEY-----"
+```
+
+**Important**:
+- The private key must include the full multi-line string with BEGIN/END markers
+- Keep the quotes around the key value
+- Delete the `.pem` files after adding to environment variables
+
+## Architecture
+
+This package uses GitHub App authentication (installation tokens), not OAuth user tokens:
+
+- **Installation Authentication**: App authenticates as itself to access repositories
+- **Installation ID**: Stored per-user in the database
+- **Octokit**: GitHub REST API client with App authentication
+- **Permissions**: Scoped to only what the app needs (read-only repository contents)
+
+Key files:
+- `src/auth.ts` - Creates authenticated Octokit instances
+- `src/config.ts` - Validates GitHub App configuration
+- `src/installation.ts` - Handles installation URL generation and callbacks
+- `src/types.ts` - TypeScript types for GitHub resources
+
+---
+
+## Security Notes
+
+- 🔒 Private keys should never be committed to source control
+- 🔒 Use environment variables or secrets managers for credentials
+- 🔒 The app uses installation authentication, not user OAuth tokens
+- 🔒 State parameter is used for CSRF protection in callbacks
+- 🔒 Minimum permissions principle (only read-only repository contents)
+- 🔒 Installation can be revoked at any time by the user on GitHub
+
+---
+
+## Useful Links
+
+- [GitHub Apps Documentation](https://docs.github.com/en/apps)
+- [Creating a GitHub App](https://docs.github.com/en/apps/creating-github-apps)
+- [Octokit SDK](https://github.com/octokit/octokit.js)
+- [GitHub App Authentication](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app)
\ No newline at end of file
diff --git a/packages/github/src/index.ts b/packages/github/src/index.ts
index 902681f82b..9407bd45f4 100644
--- a/packages/github/src/index.ts
+++ b/packages/github/src/index.ts
@@ -1,4 +1,4 @@
export * from './auth';
export * from './config';
export * from './installation';
-export * from './types';
+// Types removed - use tRPC inferred types instead
diff --git a/packages/github/src/types.ts b/packages/github/src/types.ts
deleted file mode 100644
index 5316561027..0000000000
--- a/packages/github/src/types.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-export interface GitHubOrganization {
- id: number;
- login: string;
- avatar_url: string;
- description?: string;
-}
-
-export interface GitHubRepository {
- id: number;
- name: string;
- full_name: string;
- description?: string;
- private: boolean;
- default_branch: string;
- clone_url: string;
- html_url: string;
- updated_at: string;
- owner: {
- login: string;
- avatar_url: string;
- };
-}
\ No newline at end of file