Use when you need Codex to coordinate multiple agents through Agent Relay for peer-to-peer messaging, lead/worker handoffs, or shared status tracking across sub-agents and terminals.
日本語の概要は準備中です。原文の説明を表示しています。
Use when implementing GitHub OAuth + GitHub App authentication with Nango - provides two-connection pattern for user login and repo access with webhook handling
インストールする前に、エージェントに与えられる指示の中身を確認できます。
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.
GitHub has two different authentication mechanisms that serve different purposes:
github integration)github-app-oauth integration)OAuth App alone: "User john@example.com logged in" → but which repos can they access?
GitHub App alone: "Installation #12345 has access to repo X" → but who is the user?
Solution: Two separate OAuth flows linked by user ID:
nangoConnectionIdownerIdThis lets you answer: "User john@example.com can access repos X, Y, Z"
| Connection Type | Nango Integration | Purpose | Stored In |
|---|---|---|---|
| User Login | github | Authentication, identity | users.nangoConnectionId |
| Repo Access | github-app-oauth | PR operations, file access | repos.nangoConnectionId |
| Flow | Endpoint | Webhook Type |
|---|---|---|
| Login | GET /auth/nango-session | auth + github |
| Repo Connect | GET /auth/github-app-session | auth + github-app-oauth |
| Data Sync | N/A (scheduled) | sync |
// users table - stores login connection
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
githubId: text('github_id').unique().notNull(),
githubUsername: text('github_username').notNull(),
email: text('email'),
avatarUrl: text('avatar_url'),
nangoConnectionId: text('nango_connection_id'), // Permanent login connection
incomingConnectionId: text('incoming_connection_id'), // Temp polling connection
pendingInstallationRequest: timestamp('pending_installation_request'), // Org approval wait
});
// repos table - stores per-repo app connection
export const repos = pgTable('repos', {
id: uuid('id').primaryKey().defaultRandom(),
githubRepoId: text('github_repo_id').unique().notNull(),
fullName: text('full_name').notNull(),
installationId: uuid('installation_id').references(() => githubInstallations.id),
ownerId: uuid('owner_id').references(() => users.id),
nangoConnectionId: text('nango_connection_id'), // App connection for this repo
});
// github_installations - tracks app installations
export const githubInstallations = pgTable('github_installations', {
id: uuid('id').primaryKey().defaultRandom(),
installationId: text('installation_id').unique().notNull(),
accountType: text('account_type'), // 'user' | 'organization'
accountLogin: text('account_login'),
installedById: uuid('installed_by_id').references(() => users.id),
});
// constants.ts
export const NANGO_INTEGRATION = {
GITHUB_USER: 'github', // Login only
GITHUB_APP_OAUTH: 'github-app-oauth', // Repo access
} as const;
// GET /auth/nango-session - Create login OAuth session
app.get('/auth/nango-session', async (c) => {
const tempUserId = randomUUID();
const { sessionToken } = await nangoClient.createConnectSession({
end_user: { id: tempUserId },
allowed_integrations: [NANGO_INTEGRATION.GITHUB_USER],
});
return c.json({ sessionToken, tempUserId });
});
// GET /auth/nango/status/:connectionId - Poll login completion
app.get('/auth/nango/status/:connectionId', async (c) => {
const { connectionId } = c.req.param();
// Check if user exists with this incoming connection
const user = await userRepo.findByIncomingConnectionId(connectionId);
if (!user) {
return c.json({ ready: false });
}
// Issue JWT and return
const token = authService.issueToken(user);
await userRepo.clearIncomingConnectionId(user.id);
return c.json({ ready: true, token, user });
});
// GET /auth/github-app-session - Create app OAuth session (authenticated)
app.get('/auth/github-app-session', authMiddleware, async (c) => {
const user = c.get('user');
const { sessionToken } = await nangoClient.createConnectSession({
end_user: { id: user.id, email: user.email },
allowed_integrations: [NANGO_INTEGRATION.GITHUB_APP_OAUTH],
});
return c.json({ sessionToken });
});
// GET /auth/github-app/status/:connectionId - Poll repo sync
app.get('/auth/github-app/status/:connectionId', authMiddleware, async (c) => {
const user = c.get('user');
// Check for pending org approval
if (user.pendingInstallationRequest) {
return c.json({ ready: false, pendingApproval: true });
}
// Check if repos synced
const repos = await repoRepo.findByOwnerId(user.id);
return c.json({ ready: repos.length > 0, repos });
});
// auth-webhook-service.ts
export async function handleAuthWebhook(payload: NangoAuthWebhook): Promise<boolean> {
const { connectionId, providerConfigKey, endUser } = payload;
if (providerConfigKey === NANGO_INTEGRATION.GITHUB_USER) {
return handleLoginWebhook(connectionId, endUser);
}
if (providerConfigKey === NANGO_INTEGRATION.GITHUB_APP_OAUTH) {
return handleAppOAuthWebhook(connectionId, endUser);
}
return false;
}
async function handleLoginWebhook(connectionId: string, endUser?: EndUser) {
// Fetch GitHub user info via Nango
const githubUser = await nangoService.getGitHubUser(connectionId);
// Check if user exists
const existingUser = await userRepo.findByGitHubId(String(githubUser.id));
if (existingUser) {
// Returning user - store temp connection for polling
await userRepo.update(existingUser.id, {
incomingConnectionId: connectionId,
});
// Delete duplicate connection later
await nangoService.deleteConnection(connectionId);
} else {
// New user - create record
const user = await userRepo.create({
githubId: String(githubUser.id),
githubUsername: githubUser.login,
email: githubUser.email,
avatarUrl: githubUser.avatar_url,
nangoConnectionId: connectionId,
incomingConnectionId: connectionId,
});
// Update connection with real user ID
await nangoService.patchConnection(connectionId, {
end_user: { id: user.id, email: user.email },
});
}
return true;
}
async function handleAppOAuthWebhook(connectionId: string, endUser?: EndUser) {
const userId = endUser?.id;
if (!userId) throw new Error('No user ID in app OAuth webhook');
const user = await userRepo.findById(userId);
if (!user) throw new Error('User not found');
try {
// Fetch repos user has access to
const repos = await githubService.getInstallationReposRaw(connectionId);
// Sync repos to database
for (const repo of repos) {
await repoRepo.upsert({
githubRepoId: String(repo.id),
fullName: repo.full_name,
ownerId: user.id,
nangoConnectionId: connectionId,
});
}
// Trigger Nango syncs
await nangoService.triggerSync(connectionId, ['pull-requests', 'commits']);
} catch (error) {
if (error.status === 403) {
// Org approval pending
await userRepo.update(user.id, {
pendingInstallationRequest: new Date(),
});
return true; // Graceful degradation
}
throw error;
}
return true;
}
// webhooks.ts
app.post('/api/webhooks/nango', async (c) => {
const signature = c.req.header('X-Nango-Signature');
const body = await c.req.text();
// Verify signature
const expectedSignature = createHmac('sha256', NANGO_SECRET_KEY).update(body).digest('hex');
if (signature !== expectedSignature) {
return c.json({ error: 'Invalid signature' }, 401);
}
const payload = JSON.parse(body);
if (payload.type === 'auth') {
const success = await handleAuthWebhook(payload);
return c.json({ success });
}
if (payload.type === 'sync') {
await processSyncWebhook(payload);
return c.json({ success: true });
}
return c.json({ success: false });
});
// Login flow
async function handleLogin() {
const res = await fetch('/api/auth/nango-session');
const { sessionToken } = await res.json();
const nango = new Nango({ connectSessionToken: sessionToken });
nango.openConnectUI({
onEvent: async (event) => {
if (event.type === 'connect') {
// Poll for completion
const result = await pollForAuth(event.payload.connectionId);
if (result.ready) {
localStorage.setItem('token', result.token);
navigate('/dashboard');
}
}
},
});
}
// Repo connection flow (after login)
async function handleConnectRepos() {
const res = await fetch('/api/auth/github-app-session', {
headers: { Authorization: `Bearer ${token}` },
});
const { sessionToken } = await res.json();
const nango = new Nango({ connectSessionToken: sessionToken });
nango.openConnectUI({
onEvent: async (event) => {
if (event.type === 'connect') {
const result = await pollForRepos(event.payload.connectionId);
if (result.pendingApproval) {
showMessage('Waiting for org admin approval...');
} else if (result.ready) {
setRepos(result.repos);
}
}
},
});
}
USER LOGIN:
Frontend → GET /auth/nango-session
→ Nango.openConnectUI(sessionToken)
→ User authorizes GitHub
→ Nango webhook (type: auth, providerConfigKey: github)
→ Backend creates/updates user
→ Frontend polls /auth/nango/status/:connectionId
→ Returns JWT token
REPO CONNECTION (authenticated):
Frontend → GET /auth/github-app-session (with JWT)
→ Nango.openConnectUI(sessionToken)
→ User authorizes GitHub App
→ Nango webhook (type: auth, providerConfigKey: github-app-oauth)
→ Backend fetches repos, syncs to DB
→ Frontend polls /auth/github-app/status/:connectionId
→ Returns repos list
DATA SYNCS (background):
Nango → Scheduled sync every 4 hours
→ Webhook (type: sync, model: GithubPullRequest)
→ Backend processes incremental updates
| Mistake | Fix |
|---|---|
| Using same connection for login and repo access | Use two integrations: github for login, github-app-oauth for repos |
| Not handling org approval pending | Check for 403 error, set pendingInstallationRequest flag |
Missing endUser.id in connection | Always set in createConnectSession, update after user creation |
| Polling wrong connection ID | Store incomingConnectionId separately for returning users |
| Not verifying webhook signature | Always verify X-Nango-Signature with HMAC-SHA256 |
| Keeping duplicate connections | Delete temp connection after returning user authenticates |
# Required
NANGO_SECRET_KEY=your-nango-secret-key
JWT_SECRET=your-jwt-secret-min-32-chars
DATABASE_URL=postgres://...
# Configure in Nango Dashboard
# - github integration: OAuth App credentials
# - github-app-oauth integration: GitHub App credentials
Create github integration (for login):
read:user, user:emailCreate github-app-oauth integration (for repos):
repo, pull_request, etc.Configure webhook URL: https://your-domain/api/webhooks/nango
Enable syncs: pull-requests, commits, issues, etc.
まだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
Use when you need Codex to coordinate multiple agents through Agent Relay for peer-to-peer messaging, lead/worker handoffs, or shared status tracking across sub-agents and terminals.
日本語の概要は準備中です。原文の説明を表示しています。
Use when testing web applications with visual verification - automates Chrome browser interactions, element selection, and screenshot capture for confirming UI functionality
日本語の概要は準備中です。原文の説明を表示しています。
Use when coordinating multiple AI agents with Agent Relay's workflow engine and need to pick the right orchestration pattern - covers the 10 core patterns (fan-out, pipeline, hub-spoke, consensus, mesh, handoff, cascade, dag, debate, hierarchical) plus 14 specialized ones, with decision framework and accurate SDK/YAML examples.
日本語の概要は準備中です。原文の説明を表示しています。
Use when creating Agent Skills packages (SKILL.md format) for Codex CLI, GitHub Copilot, or Amp - provides the agentskills.io specification with frontmatter constraints, directory structure, and validation rules
日本語の概要は準備中です。原文の説明を表示しています。
Use when creating or improving Claude Code agents. Expert guidance on agent file structure, frontmatter, persona definition, tool access, model selection, and validation against schema.
日本語の概要は準備中です。原文の説明を表示しています。
Use when creating or publishing Claude Code hooks - covers executable format, event types, JSON I/O, exit codes, security requirements, and PRPM package structure
日本語の概要は準備中です。原文の説明を表示しています。