Next.js + NextAuth

Add Affitor lead tracking to a Next.js app using NextAuth (Auth.js) — handle the click-id cookie gap correctly.

This guide covers the signup step when your Next.js app uses NextAuth (Auth.js) for authentication. Steps 1 (capture click) and 3 (track sale) are identical to the base Next.js guide — this page focuses on what is different: wiring the lead call to NextAuth's auth lifecycle without losing the click attribution.

Info

The @affitor/sdk package is Beta. The documented happy-path works; report issues on GitHub.

Prerequisites

  • Your program ID (dashboard → program settings)
  • A program API key for server-side calls
  • A Next.js App Router app with NextAuth (Auth.js) configured

1. Capture the click

Follow Step 1 of the Next.js guide. Install @affitor/sdk, create the client component that wraps your app in <AffitorProvider>, and render it in your root layout. No changes needed for NextAuth.

2. Track the signup

NextAuth's events.createUser callback fires on the server, once per new user — it looks like the ideal place to call trackLead. It is — but with one important caveat:

The affitor_click_id is a first-party browser cookie. A server-side callback has no access to it. If you call @affitor/sdk/server trackLead from events.createUser without a clickId, Affitor cannot find the click: the call returns 400 and no lead is recorded.

Send the lead from your own route handler, with your program API key. A request from the browser to your route handler carries the first-party affitor_click_id cookie, so the server can read it directly.

Stock Auth.js sessions do not include user.id. Expose it once in your auth config, and add the type so session.user.id compiles:

// auth.ts
export const { auth, handlers, signIn, signOut } = NextAuth({
  // ...your providers and adapter
  callbacks: {
    session({ session, user, token }) {
      session.user.id = user?.id ?? token.sub ?? '';
      return session;
    },
  },
});

// types/next-auth.d.ts
import type { DefaultSession } from 'next-auth';
declare module 'next-auth' {
  interface Session {
    user: { id: string } & DefaultSession['user'];
  }
}

Then the route:

// app/api/affitor/lead/route.ts
import { cookies } from 'next/headers';
import Affitor from '@affitor/sdk/server';
import { auth } from '@/auth'; // your NextAuth (Auth.js) instance

const affitor = new Affitor({ apiKey: process.env.AFFITOR_API_KEY! });

// Needs the session callback above; without it session.user.id is empty and no lead is sent.
export async function POST() {
  const session = await auth();
  const clickId = (await cookies()).get('affitor_click_id')?.value;
  if (!session?.user?.id || !clickId) return new Response(null, { status: 204 });

  // user.id is the NextAuth user ID — use it as customerExternalId everywhere:
  // signup, sale, Stripe metadata.
  await affitor.trackLead({
    customerExternalId: session.user.id,
    clickId,
    email: session.user.email ?? undefined,
  });
  return new Response(null, { status: 204 });
}

Call it once from the browser after NextAuth confirms a new user session:

// app/auth-tracker.tsx
'use client';

import { useEffect, useRef } from 'react';
import { useSession } from 'next-auth/react';

export function AffitorAuthTracker() {
  const { status } = useSession();
  const tracked = useRef(false);

  useEffect(() => {
    if (status !== 'authenticated' || tracked.current) return;
    tracked.current = true;
    fetch('/api/affitor/lead', { method: 'POST' });
  }, [status]);

  return null;
}

Render it inside your root layout, inside SessionProvider:

// app/layout.tsx
import { Providers } from './affitor-provider';
import { AffitorAuthTracker } from './auth-tracker';
import { SessionProvider } from 'next-auth/react';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <SessionProvider>
          <Providers>
            <AffitorAuthTracker />
            {children}
          </Providers>
        </SessionProvider>
      </body>
    </html>
  );
}
Info

A lead is recorded once per customer. A repeat call for the same customer (for example on a later sign-in) returns 400 from trackLead, which resolves with ok: false instead of throwing, so it is safe to ignore.

Browser signup() is provisional on new programs

The tracker's browser helper, affitor.signup(id, email) from useAffitor(), sends the lead without your API key. New programs require server-verified leads, so a browser-only lead is stored as provisional and does not settle, save the user ID, or count until your server sends the lead or a Stripe Checkout payment for that customer arrives through your connected Stripe account. A sale sent to /api/v1/track/sale does not settle it. See Lead auth policy.

Alternative: server-side via events.createUser with click-id forwarding

If you need the lead recorded the moment the user row is created (before the client redirects), you can use events.createUser — but you must forward the click ID from the browser yourself.

Step A — read the click ID on the client before the user submits the sign-up form:

'use client';
import { useAffitor } from '@affitor/sdk/react';

// In your sign-up form component, read the click ID and include
// it in the request or store it on the user's session/metadata.
const affitor = useAffitor(); // null until the tracker has loaded
const clickId = affitor?.getClickId() ?? null; // the affitor_click_id cookie value

Step B — pass it through to your NextAuth config and call trackLead in the event:

// auth.ts (NextAuth config)
import Affitor from '@affitor/sdk/server';
import type { NextAuthConfig } from 'next-auth';

const affitor = new Affitor({ apiKey: process.env.AFFITOR_API_KEY! });

export const authConfig: NextAuthConfig = {
  // ...providers, callbacks, etc.
  events: {
    async createUser({ user }) {
      // user.id is the NextAuth-generated user ID — use it as
      // customerExternalId everywhere: signup, sale, Stripe metadata.
      await affitor.trackLead({
        customerExternalId: user.id,
        clickId: user.affitorClickId ?? undefined, // forwarded from the browser
        email: user.email ?? undefined,
      });
    },
  },
};
Info

user.affitorClickId is illustrative — the forwarding mechanism depends on your auth flow. Common approaches: store the click ID in the sign-up form's hidden field and write it to a custom user attribute before createUser fires, or pass it through a custom session object. Without it, clickId is undefined and the lead call returns 400 with nothing recorded.

If you cannot reliably forward the click ID into events.createUser, use the server route above.

3. Track the sale

Follow Step 3 of the Next.js guide. Use session.user.id as affitor_customer_key (Stripe path) or customerExternalId (server SDK path) — it must match the value you sent at signup.

Verify

Verify success
Browser
  • Visit your site with ?aff=TESTCODE — an affitor_click_id cookie is set
Network
  • After sign-in, your /api/affitor/lead route's trackLead() call returns 2xx
Dashboard
  • The lead appears under your program's tracking events, attributed to the correct partner
If it doesn't work
  • If no lead appears, the click ID is not reaching your server — confirm the affitor_click_id cookie is set before the user signs in. Create a test lead from your server with affitor.trackLead({ customerExternalId: 'test_customer', additionalData: { test_mode: true, program_id: YOUR_PROGRAM_ID } }).

Common mistakes

Common mistakes
  • Calling trackLead from events.createUser without a clickId — the call returns 400 and no lead is recorded. Use the server route above or forward the click ID explicitly.
  • Different customer IDs at signup vs sale — session.user.id must be used consistently as customerExternalId / affitor_customer_key everywhere.
  • Wrapping AffitorAuthTracker outside SessionProvider — useSession() will throw. SessionProvider must be an ancestor.
  • Trying to track a sale from the browser — sales are server-side only (Stripe metadata or the server SDK).
Next recommended step
Ready to track sales?

Connect Stripe so renewals and one-time payments attribute automatically.

Continue →
© 2026 Affitor