| title | Nango OAuth with Trigger.dev |
|---|---|
| sidebarTitle | Nango OAuth guide |
| description | Use Nango to authenticate API calls inside a Trigger.dev task, no token management required. |
| icon | key |
Nango handles OAuth for 250+ APIs, storing and automatically refreshing access tokens on your behalf. This makes it a natural fit for Trigger.dev tasks that need to call third-party APIs on behalf of your users.
In this guide you'll build a task that:
- Receives a Nango
connectionIdfrom your frontend - Fetches a fresh GitHub access token from Nango inside the task
- Calls the GitHub API to retrieve the user's open pull requests
- Uses Claude to summarize what's being worked on
This pattern works for any API Nango supports. Swap GitHub for HubSpot, Slack, Notion, or any other provider.
- A Next.js project with Trigger.dev installed
- A Nango account
- An Anthropic API key
sequenceDiagram
participant User
participant Frontend
participant API as Next.js API
participant TD as Trigger.dev task
participant Nango
participant GH as GitHub API
participant Claude
User->>Frontend: Clicks "Analyze my PRs"
Frontend->>API: POST /api/nango-session
API->>Nango: POST /connect/sessions (secret key)
Nango-->>API: session token (30 min TTL)
API-->>Frontend: session token
Frontend->>Nango: OAuth connect (frontend SDK + session token)
Nango-->>Frontend: connectionId
Frontend->>API: POST /api/analyze-prs { connectionId, repo }
API->>TD: tasks.trigger(...)
TD->>Nango: getConnection(connectionId)
Nango-->>TD: access_token
TD->>GH: GET /repos/:repo/pulls
GH-->>TD: open pull requests
TD->>Claude: Summarize PRs
Claude-->>TD: Summary
```bash
npm install @nangohq/frontend
```
The frontend SDK requires a short-lived **connect session token** issued by your backend — this replaces the old public key approach. Add an API route that creates the session:
```ts app/api/nango-session/route.ts
import { NextResponse } from "next/server";
export async function POST(req: Request) {
const { userId } = await req.json();
const response = await fetch("https://api.nango.dev/connect/sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NANGO_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
end_user: { id: userId },
}),
});
const { data } = await response.json();
return NextResponse.json({ token: data.token });
}
```
Then add a connect button to your UI that fetches the token and opens the Nango OAuth flow:
```tsx app/page.tsx
"use client";
import Nango from "@nangohq/frontend";
export default function Page() {
async function connectGitHub() {
// Get a short-lived session token from your backend
const sessionRes = await fetch("/api/nango-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId: "user_123" }), // replace with your actual user ID
});
const { token } = await sessionRes.json();
const nango = new Nango({ connectSessionToken: token });
const result = await nango.auth("github");
// result.connectionId is what you pass to your task
await fetch("/api/analyze-prs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
connectionId: result.connectionId,
repo: "triggerdotdev/trigger.dev",
}),
});
}
return <button onClick={connectGitHub}>Analyze my PRs</button>;
}
```
<Note>
The session token expires after 30 minutes and is used only to open the OAuth popup — it is not stored. The `connectionId` returned by `nango.auth()` is the stable identifier you pass to future task runs to retrieve the user's access token.
</Note>
Install the required packages:
npm install @nangohq/node @anthropic-ai/sdkCreate the task:
import { task } from "@trigger.dev/sdk";
import { Nango } from "@nangohq/node";
import Anthropic from "@anthropic-ai/sdk";
const nango = new Nango({ secretKey: process.env.NANGO_SECRET_KEY! });
const anthropic = new Anthropic();
export const analyzePRs = task({
id: "analyze-prs",
run: async (payload: { connectionId: string; repo: string }) => {
const { connectionId, repo } = payload;
// Fetch a fresh access token from Nango. It handles refresh automatically.
// Use the exact integration slug from your Nango dashboard, e.g. "github-getting-started"
const connection = await nango.getConnection("<your-integration-slug>", connectionId);
if (connection.credentials.type !== "OAUTH2") {
throw new Error(`Unexpected credentials type: ${connection.credentials.type}`);
}
const accessToken = connection.credentials.access_token;
// Call the GitHub API on behalf of the user
const response = await fetch(
`https://api.github.com/repos/${repo}/pulls?state=open&per_page=20`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/vnd.github.v3+json",
},
}
);
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
}
const prs = await response.json();
if (prs.length === 0) {
return { summary: "No open pull requests found.", prCount: 0 };
}
// Use Claude to summarize what's being worked on
const prList = prs
.map(
(pr: { number: number; title: string; user: { login: string }; body: string | null }) =>
`#${pr.number} by @${pr.user.login}: ${pr.title}\n${pr.body?.slice(0, 200) ?? ""}`
)
.join("\n\n");
const message = await anthropic.messages.create({
model: "claude-opus-4-6",
max_tokens: 1024,
messages: [
{
role: "user",
content: `Here are the open pull requests for ${repo}. Give a concise summary of what's being worked on, grouped by theme where possible.\n\n${prList}`,
},
],
});
const summary = message.content[0].type === "text" ? message.content[0].text : "";
return { summary, prCount: prs.length };
},
});Add a route handler that receives the connectionId from your frontend and triggers the task:
import { tasks } from "@trigger.dev/sdk";
import { analyzePRs } from "@/trigger/analyze-prs";
import { NextResponse } from "next/server";
export async function POST(req: Request) {
const { connectionId, repo } = await req.json();
if (!connectionId || !repo) {
return NextResponse.json({ error: "Missing connectionId or repo" }, { status: 400 });
}
const handle = await tasks.trigger<typeof analyzePRs>("analyze-prs", {
connectionId,
repo,
});
return NextResponse.json(handle);
}Add the following to your .env file. Trigger.dev dev mode reads from .env, not .env.local, so keep task-related secrets there:
NANGO_SECRET_KEY= # From Nango dashboard → Environment → Secret key
TRIGGER_SECRET_KEY= # From Trigger.dev dashboard → API keys
ANTHROPIC_API_KEY= # From Anthropic consoleAdd NANGO_SECRET_KEY and ANTHROPIC_API_KEY as environment variables in your Trigger.dev project too. These are used inside the task at runtime.
```bash
npm run dev
npx trigger.dev@latest dev
```
- Reuse the
connectionId: Once a user has connected, store theirconnectionIdand pass it in future task payloads. No need to re-authenticate. - Add retries: If the GitHub API returns a transient error, Trigger.dev retries will handle it automatically.
- Switch providers: The same pattern works for any Nango-supported API. Change
"github"to"hubspot","slack","notion", or any other provider. - Stream the analysis: Use Trigger.dev Realtime to stream Claude's response back to your frontend as it's generated.