|
| 1 | +--- |
| 2 | +title: "Nango OAuth with Trigger.dev" |
| 3 | +sidebarTitle: "Nango OAuth guide" |
| 4 | +description: "Use Nango to authenticate API calls inside a Trigger.dev task, no token management required." |
| 5 | +icon: "key" |
| 6 | +--- |
| 7 | + |
| 8 | +[Nango](https://www.nango.dev/) 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. |
| 9 | + |
| 10 | +In this guide you'll build a task that: |
| 11 | + |
| 12 | +1. Receives a Nango `connectionId` from your frontend |
| 13 | +2. Fetches a fresh GitHub access token from Nango inside the task |
| 14 | +3. Calls the GitHub API to retrieve the user's open pull requests |
| 15 | +4. Uses Claude to summarize what's being worked on |
| 16 | + |
| 17 | +This pattern works for any API Nango supports. Swap GitHub for HubSpot, Slack, Notion, or any other provider. |
| 18 | + |
| 19 | +## Prerequisites |
| 20 | + |
| 21 | +- A Next.js project with [Trigger.dev installed](/guides/frameworks/nextjs) |
| 22 | +- A [Nango](https://app.nango.dev/) account |
| 23 | +- An [Anthropic](https://console.anthropic.com/) API key |
| 24 | + |
| 25 | +## How it works |
| 26 | + |
| 27 | +```mermaid |
| 28 | +sequenceDiagram |
| 29 | + participant User |
| 30 | + participant Frontend |
| 31 | + participant API as Next.js API |
| 32 | + participant TD as Trigger.dev task |
| 33 | + participant Nango |
| 34 | + participant GH as GitHub API |
| 35 | + participant Claude |
| 36 | +
|
| 37 | + User->>Frontend: Clicks "Analyze my PRs" |
| 38 | + Frontend->>API: POST /api/nango-session |
| 39 | + API->>Nango: POST /connect/sessions (secret key) |
| 40 | + Nango-->>API: session token (30 min TTL) |
| 41 | + API-->>Frontend: session token |
| 42 | + Frontend->>Nango: OAuth connect (frontend SDK + session token) |
| 43 | + Nango-->>Frontend: connectionId |
| 44 | + Frontend->>API: POST /api/analyze-prs { connectionId, repo } |
| 45 | + API->>TD: tasks.trigger(...) |
| 46 | + TD->>Nango: getConnection(connectionId) |
| 47 | + Nango-->>TD: access_token |
| 48 | + TD->>GH: GET /repos/:repo/pulls |
| 49 | + GH-->>TD: open pull requests |
| 50 | + TD->>Claude: Summarize PRs |
| 51 | + Claude-->>TD: Summary |
| 52 | +``` |
| 53 | + |
| 54 | +## Step 1: Connect GitHub in Nango |
| 55 | + |
| 56 | +<Steps titleSize="h3"> |
| 57 | + <Step title="Create a GitHub integration in Nango"> |
| 58 | + 1. In your [Nango dashboard](https://app.nango.dev/), go to **Integrations** and click **Set up new integration**. |
| 59 | + 2. Search for **GitHub** and select GitHub (User OAuth). |
| 60 | + 3. Create and add a test connection |
| 61 | + </Step> |
| 62 | + <Step title="Add the Nango frontend SDK"> |
| 63 | + Install the Nango frontend SDK in your Next.js project: |
| 64 | + |
| 65 | + ```bash |
| 66 | + npm install @nangohq/frontend |
| 67 | + ``` |
| 68 | + |
| 69 | + 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: |
| 70 | + |
| 71 | + ```ts app/api/nango-session/route.ts |
| 72 | + import { NextResponse } from "next/server"; |
| 73 | + |
| 74 | + export async function POST(req: Request) { |
| 75 | + const { userId } = await req.json(); |
| 76 | + |
| 77 | + const response = await fetch("https://api.nango.dev/connect/sessions", { |
| 78 | + method: "POST", |
| 79 | + headers: { |
| 80 | + Authorization: `Bearer ${process.env.NANGO_SECRET_KEY}`, |
| 81 | + "Content-Type": "application/json", |
| 82 | + }, |
| 83 | + body: JSON.stringify({ |
| 84 | + end_user: { id: userId }, |
| 85 | + }), |
| 86 | + }); |
| 87 | + |
| 88 | + const { data } = await response.json(); |
| 89 | + return NextResponse.json({ token: data.token }); |
| 90 | + } |
| 91 | + ``` |
| 92 | + |
| 93 | + Then add a connect button to your UI that fetches the token and opens the Nango OAuth flow: |
| 94 | + |
| 95 | + ```tsx app/page.tsx |
| 96 | + "use client"; |
| 97 | + |
| 98 | + import Nango from "@nangohq/frontend"; |
| 99 | + |
| 100 | + export default function Page() { |
| 101 | + async function connectGitHub() { |
| 102 | + // Get a short-lived session token from your backend |
| 103 | + const sessionRes = await fetch("/api/nango-session", { |
| 104 | + method: "POST", |
| 105 | + headers: { "Content-Type": "application/json" }, |
| 106 | + body: JSON.stringify({ userId: "user_123" }), // replace with your actual user ID |
| 107 | + }); |
| 108 | + const { token } = await sessionRes.json(); |
| 109 | + |
| 110 | + const nango = new Nango({ connectSessionToken: token }); |
| 111 | + const result = await nango.auth("github"); |
| 112 | + |
| 113 | + // result.connectionId is what you pass to your task |
| 114 | + await fetch("/api/analyze-prs", { |
| 115 | + method: "POST", |
| 116 | + headers: { "Content-Type": "application/json" }, |
| 117 | + body: JSON.stringify({ |
| 118 | + connectionId: result.connectionId, |
| 119 | + repo: "triggerdotdev/trigger.dev", |
| 120 | + }), |
| 121 | + }); |
| 122 | + } |
| 123 | + |
| 124 | + return <button onClick={connectGitHub}>Analyze my PRs</button>; |
| 125 | + } |
| 126 | + ``` |
| 127 | + |
| 128 | + <Note> |
| 129 | + 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. |
| 130 | + </Note> |
| 131 | + |
| 132 | + </Step> |
| 133 | +</Steps> |
| 134 | + |
| 135 | +## Step 2: Create the Trigger.dev task |
| 136 | + |
| 137 | +Install the required packages: |
| 138 | + |
| 139 | +```bash |
| 140 | +npm install @nangohq/node @anthropic-ai/sdk |
| 141 | +``` |
| 142 | + |
| 143 | +Create the task: |
| 144 | + |
| 145 | +<CodeGroup> |
| 146 | + |
| 147 | +```ts trigger/analyze-prs.ts |
| 148 | +import { task } from "@trigger.dev/sdk"; |
| 149 | +import { Nango } from "@nangohq/node"; |
| 150 | +import Anthropic from "@anthropic-ai/sdk"; |
| 151 | + |
| 152 | +const nango = new Nango({ secretKey: process.env.NANGO_SECRET_KEY! }); |
| 153 | +const anthropic = new Anthropic(); |
| 154 | + |
| 155 | +export const analyzePRs = task({ |
| 156 | + id: "analyze-prs", |
| 157 | + run: async (payload: { connectionId: string; repo: string }) => { |
| 158 | + const { connectionId, repo } = payload; |
| 159 | + |
| 160 | + // Fetch a fresh access token from Nango. It handles refresh automatically. |
| 161 | + // Use the exact integration slug from your Nango dashboard, e.g. "github-getting-started" |
| 162 | + const connection = await nango.getConnection("<your-integration-slug>", connectionId); |
| 163 | + |
| 164 | + if (connection.credentials.type !== "OAUTH2") { |
| 165 | + throw new Error(`Unexpected credentials type: ${connection.credentials.type}`); |
| 166 | + } |
| 167 | + |
| 168 | + const accessToken = connection.credentials.access_token; |
| 169 | + // Call the GitHub API on behalf of the user |
| 170 | + const response = await fetch( |
| 171 | + `https://api.github.com/repos/${repo}/pulls?state=open&per_page=20`, |
| 172 | + { |
| 173 | + headers: { |
| 174 | + Authorization: `Bearer ${accessToken}`, |
| 175 | + Accept: "application/vnd.github.v3+json", |
| 176 | + }, |
| 177 | + } |
| 178 | + ); |
| 179 | + |
| 180 | + if (!response.ok) { |
| 181 | + throw new Error(`GitHub API error: ${response.status} ${response.statusText}`); |
| 182 | + } |
| 183 | + |
| 184 | + const prs = await response.json(); |
| 185 | + |
| 186 | + if (prs.length === 0) { |
| 187 | + return { summary: "No open pull requests found.", prCount: 0 }; |
| 188 | + } |
| 189 | + |
| 190 | + // Use Claude to summarize what's being worked on |
| 191 | + const prList = prs |
| 192 | + .map( |
| 193 | + (pr: { number: number; title: string; user: { login: string }; body: string | null }) => |
| 194 | + `#${pr.number} by @${pr.user.login}: ${pr.title}\n${pr.body?.slice(0, 200) ?? ""}` |
| 195 | + ) |
| 196 | + .join("\n\n"); |
| 197 | + |
| 198 | + const message = await anthropic.messages.create({ |
| 199 | + model: "claude-opus-4-6", |
| 200 | + max_tokens: 1024, |
| 201 | + messages: [ |
| 202 | + { |
| 203 | + role: "user", |
| 204 | + 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}`, |
| 205 | + }, |
| 206 | + ], |
| 207 | + }); |
| 208 | + |
| 209 | + const summary = message.content[0].type === "text" ? message.content[0].text : ""; |
| 210 | + |
| 211 | + return { summary, prCount: prs.length }; |
| 212 | + }, |
| 213 | +}); |
| 214 | +``` |
| 215 | + |
| 216 | +</CodeGroup> |
| 217 | + |
| 218 | +## Step 3: Create the API route |
| 219 | + |
| 220 | +Add a route handler that receives the `connectionId` from your frontend and triggers the task: |
| 221 | + |
| 222 | +```ts app/api/analyze-prs/route.ts |
| 223 | +import { tasks } from "@trigger.dev/sdk"; |
| 224 | +import { analyzePRs } from "@/trigger/analyze-prs"; |
| 225 | +import { NextResponse } from "next/server"; |
| 226 | + |
| 227 | +export async function POST(req: Request) { |
| 228 | + const { connectionId, repo } = await req.json(); |
| 229 | + |
| 230 | + if (!connectionId || !repo) { |
| 231 | + return NextResponse.json({ error: "Missing connectionId or repo" }, { status: 400 }); |
| 232 | + } |
| 233 | + |
| 234 | + const handle = await tasks.trigger<typeof analyzePRs>("analyze-prs", { |
| 235 | + connectionId, |
| 236 | + repo, |
| 237 | + }); |
| 238 | + |
| 239 | + return NextResponse.json(handle); |
| 240 | +} |
| 241 | +``` |
| 242 | + |
| 243 | +## Step 4: Set environment variables |
| 244 | + |
| 245 | +Add the following to your `.env` file. Trigger.dev dev mode reads from `.env`, not `.env.local`, so keep task-related secrets there: |
| 246 | + |
| 247 | +```bash |
| 248 | +NANGO_SECRET_KEY= # From Nango dashboard → Environment → Secret key |
| 249 | +TRIGGER_SECRET_KEY= # From Trigger.dev dashboard → API keys |
| 250 | +ANTHROPIC_API_KEY= # From Anthropic console |
| 251 | +``` |
| 252 | + |
| 253 | +Add `NANGO_SECRET_KEY` and `ANTHROPIC_API_KEY` as [environment variables](/deploy-environment-variables) in your Trigger.dev project too. These are used inside the task at runtime. |
| 254 | + |
| 255 | +## Test it |
| 256 | + |
| 257 | +<Steps titleSize="h3"> |
| 258 | + <Step title="Start your dev servers"> |
| 259 | + |
| 260 | + ```bash |
| 261 | + npm run dev |
| 262 | + npx trigger.dev@latest dev |
| 263 | + ``` |
| 264 | + |
| 265 | + </Step> |
| 266 | + <Step title="Connect GitHub and trigger the task"> |
| 267 | + Open your app, click **Analyze my PRs**, and complete the GitHub OAuth flow. The task will be triggered automatically. |
| 268 | + </Step> |
| 269 | + <Step title="Watch the run in Trigger.dev"> |
| 270 | + Open your [Trigger.dev dashboard](https://cloud.trigger.dev/) and navigate to **Runs** to see the task execute. You'll see the PR count and Claude's summary in the output. |
| 271 | + </Step> |
| 272 | +</Steps> |
| 273 | + |
| 274 | +<Check> |
| 275 | + Your task is now fetching a fresh GitHub token from Nango, calling the GitHub API on behalf of the |
| 276 | + user, and using Claude to summarize their open PRs. No token storage or refresh logic required. |
| 277 | +</Check> |
| 278 | + |
| 279 | +## Next steps |
| 280 | + |
| 281 | +- **Reuse the `connectionId`**: Once a user has connected, store their `connectionId` and pass it in future task payloads. No need to re-authenticate. |
| 282 | +- **Add retries**: If the GitHub API returns a transient error, Trigger.dev [retries](/errors-retrying) will handle it automatically. |
| 283 | +- **Switch providers**: The same pattern works for any Nango-supported API. Change `"github"` to `"hubspot"`, `"slack"`, `"notion"`, or any other provider. |
| 284 | +- **Stream the analysis**: Use [Trigger.dev Realtime](/realtime/overview) to stream Claude's response back to your frontend as it's generated. |
0 commit comments