From 631fd77c1ca568acd20d6d1841dae0d3de5a09b1 Mon Sep 17 00:00:00 2001 From: isshaddad Date: Tue, 24 Mar 2026 14:19:47 -0400 Subject: [PATCH 1/3] add Nango OAuth integration guide --- docs/docs.json | 1 + docs/guides/frameworks/nango.mdx | 284 +++++++++++++++++++++++++++++++ 2 files changed, 285 insertions(+) create mode 100644 docs/guides/frameworks/nango.mdx diff --git a/docs/docs.json b/docs/docs.json index 14d728e2db1..19df795e1d8 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -368,6 +368,7 @@ "guides/ai-agents/claude-code-trigger", "guides/frameworks/drizzle", "guides/frameworks/prisma", + "guides/frameworks/nango", "guides/frameworks/sequin", { "group": "Supabase", diff --git a/docs/guides/frameworks/nango.mdx b/docs/guides/frameworks/nango.mdx new file mode 100644 index 00000000000..306a5311ab4 --- /dev/null +++ b/docs/guides/frameworks/nango.mdx @@ -0,0 +1,284 @@ +--- +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](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. + +In this guide you'll build a task that: + +1. Receives a Nango `connectionId` from your frontend +2. Fetches a fresh GitHub access token from Nango inside the task +3. Calls the GitHub API to retrieve the user's open pull requests +4. 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. + +## Prerequisites + +- A Next.js project with [Trigger.dev installed](/guides/frameworks/nextjs) +- A [Nango](https://app.nango.dev/) account +- An [Anthropic](https://console.anthropic.com/) API key + +## How it works + +```mermaid +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 +``` + +## Step 1: Connect GitHub in Nango + + + + 1. In your [Nango dashboard](https://app.nango.dev/), go to **Integrations** and click **Set up new integration**. + 2. Search for **GitHub** and select GitHub (User OAuth). + 3. Create and add a test connection + + + Install the Nango frontend SDK in your Next.js project: + + ```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 ; + } + ``` + + + 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. + + + + + +## Step 2: Create the Trigger.dev task + +Install the required packages: + +```bash +npm install @nangohq/node @anthropic-ai/sdk +``` + +Create the task: + + + +```ts trigger/analyze-prs.ts +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("", 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 }; + }, +}); +``` + + + +## Step 3: Create the API route + +Add a route handler that receives the `connectionId` from your frontend and triggers the task: + +```ts app/api/analyze-prs/route.ts +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("analyze-prs", { + connectionId, + repo, + }); + + return NextResponse.json(handle); +} +``` + +## Step 4: Set environment variables + +Add the following to your `.env` file. Trigger.dev dev mode reads from `.env`, not `.env.local`, so keep task-related secrets there: + +```bash +NANGO_SECRET_KEY= # From Nango dashboard → Environment → Secret key +TRIGGER_SECRET_KEY= # From Trigger.dev dashboard → API keys +ANTHROPIC_API_KEY= # From Anthropic console +``` + +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. + +## Test it + + + + + ```bash + npm run dev + npx trigger.dev@latest dev + ``` + + + + Open your app, click **Analyze my PRs**, and complete the GitHub OAuth flow. The task will be triggered automatically. + + + 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. + + + + + Your task is now fetching a fresh GitHub token from Nango, calling the GitHub API on behalf of the + user, and using Claude to summarize their open PRs. No token storage or refresh logic required. + + +## Next steps + +- **Reuse the `connectionId`**: Once a user has connected, store their `connectionId` and pass it in future task payloads. No need to re-authenticate. +- **Add retries**: If the GitHub API returns a transient error, Trigger.dev [retries](/errors-retrying) 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](/realtime/overview) to stream Claude's response back to your frontend as it's generated. From 634a9a70aa976f71bf6c82f9f1b76c571feb8c7e Mon Sep 17 00:00:00 2001 From: isshaddad Date: Tue, 24 Mar 2026 19:55:51 -0400 Subject: [PATCH 2/3] final tweaks --- docs/guides/frameworks/nango.mdx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/guides/frameworks/nango.mdx b/docs/guides/frameworks/nango.mdx index 306a5311ab4..5e5980f2596 100644 --- a/docs/guides/frameworks/nango.mdx +++ b/docs/guides/frameworks/nango.mdx @@ -66,7 +66,7 @@ sequenceDiagram 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: + The frontend SDK requires a short-lived **connect session token** issued by your backend. Add an API route that creates the session: ```ts app/api/nango-session/route.ts import { NextResponse } from "next/server"; @@ -85,6 +85,12 @@ sequenceDiagram }), }); + if (!response.ok) { + const text = await response.text(); + console.error("Nango error:", response.status, text); + return NextResponse.json({ error: text }, { status: response.status }); + } + const { data } = await response.json(); return NextResponse.json({ token: data.token }); } @@ -125,10 +131,6 @@ sequenceDiagram } ``` - - 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. - - @@ -242,7 +244,7 @@ export async function POST(req: Request) { ## Step 4: Set environment variables -Add the following to your `.env` file. Trigger.dev dev mode reads from `.env`, not `.env.local`, so keep task-related secrets there: +Add the following to your `.env.local` file: ```bash NANGO_SECRET_KEY= # From Nango dashboard → Environment → Secret key From 7d03ee6d63acdce0457648e19bd1495728f592c1 Mon Sep 17 00:00:00 2001 From: isshaddad Date: Tue, 24 Mar 2026 20:07:09 -0400 Subject: [PATCH 3/3] coderabbit changes --- docs/guides/frameworks/nango.mdx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/guides/frameworks/nango.mdx b/docs/guides/frameworks/nango.mdx index 5e5980f2596..91c3ad8f738 100644 --- a/docs/guides/frameworks/nango.mdx +++ b/docs/guides/frameworks/nango.mdx @@ -74,6 +74,10 @@ sequenceDiagram export async function POST(req: Request) { const { userId } = await req.json(); + if (!userId || typeof userId !== "string") { + return NextResponse.json({ error: "Missing or invalid userId" }, { status: 400 }); + } + const response = await fetch("https://api.nango.dev/connect/sessions", { method: "POST", headers: { @@ -114,7 +118,8 @@ sequenceDiagram const { token } = await sessionRes.json(); const nango = new Nango({ connectSessionToken: token }); - const result = await nango.auth("github"); + // Use the exact integration slug from your Nango dashboard + const result = await nango.auth(""); // result.connectionId is what you pass to your task await fetch("/api/analyze-prs", { @@ -222,7 +227,6 @@ export const analyzePRs = task({ Add a route handler that receives the `connectionId` from your frontend and triggers the task: ```ts app/api/analyze-prs/route.ts -import { tasks } from "@trigger.dev/sdk"; import { analyzePRs } from "@/trigger/analyze-prs"; import { NextResponse } from "next/server"; @@ -233,10 +237,7 @@ export async function POST(req: Request) { return NextResponse.json({ error: "Missing connectionId or repo" }, { status: 400 }); } - const handle = await tasks.trigger("analyze-prs", { - connectionId, - repo, - }); + const handle = await analyzePRs.trigger({ connectionId, repo }); return NextResponse.json(handle); }