Next.js + Supabase Auth

Add Affitor tracking to a Next.js app that uses Supabase Auth — clicks, signups, and sales.

This guide covers the Supabase-specific signup step. Steps 1 (capture the click) and 3 (track the sale) are identical to the base Next.js guide — refer there for full detail.

:::note 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 (sales)
  • A Next.js app with Supabase Auth configured

1. Capture the click

Follow Step 1 of the Next.js guide. Install @affitor/sdk, create an <AffitorInit /> client component that calls init({ programId }) on mount, and render it once in your root layout.

2. Track the signup

Supabase Auth exposes onAuthStateChange — a client-side listener that fires whenever the session changes. This is the recommended place to call signup() because the browser SDK reads the affitor_click_id cookie automatically. No click-id plumbing required.

'use client';

import { useEffect } from 'react';
import { createClient } from '@/utils/supabase/client';
import { signup } from '@affitor/sdk';

export function AffitorAuthSync() {
  useEffect(() => {
    const supabase = createClient();

    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (event, session) => {
        if (event === 'SIGNED_IN' && session?.user) {
          // Only fire on a genuinely new sign-up, not every page load.
          // Use a flag stored in sessionStorage to call signup() once.
          const key = `affitor_lead_sent_${session.user.id}`;
          if (!sessionStorage.getItem(key)) {
            signup(session.user.id, session.user.email ?? undefined);
            sessionStorage.setItem(key, '1');
          }
        }
      }
    );

    return () => subscription.unsubscribe();
  }, []);

  return null;
}

Render this component alongside <AffitorInit /> in your root layout:

// app/layout.tsx
import { AffitorInit } from './affitor-init';
import { AffitorAuthSync } from './affitor-auth-sync';

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

signup(userId, email?) fires POST /api/v1/track/lead. The userId here is session.user.id — the stable Supabase UUID. Use this exact value as customerExternalId everywhere: at sale time and in any Stripe metadata.

:::note Why client-side is the reliable default. The affitor_click_id is a first-party browser cookie. A Supabase DB trigger or Edge Function runs with no access to that cookie, so it cannot supply the click ID on its own. The onAuthStateChange listener runs in the browser where the cookie exists, so attribution is automatic. :::

Server-side option (DB trigger / Edge Function)

If you need to track leads from a Supabase Edge Function or DB trigger, you must forward the click ID yourself.

  1. On the client, read the click ID before or during sign-up:
import { getClickId } from '@affitor/sdk';

const clickId = getClickId(); // returns the affitor_click_id cookie value
// Store clickId on the user's metadata or pass it through your sign-up flow.
await supabase.auth.signUp({
  email,
  password,
  options: {
    data: { affitor_click_id: clickId },
  },
});
  1. In your Edge Function, read the forwarded click ID and call @affitor/sdk/server:
// supabase/functions/on-signup/index.ts
import Affitor from '@affitor/sdk/server';

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

const clickId = user.user_metadata?.affitor_click_id;

await affitor.trackLead({
  customerExternalId: user.id,  // Supabase user.id
  clickId,                      // undefined if user did not arrive via a partner link
  email: user.email,
});

Without the forwarded clickId, the lead has no partner to attribute to — the event is recorded but conversion credit cannot be assigned.

3. Track the sale

Follow Step 3 of the Next.js guide. Use session.user.id (your customerExternalId) as affitor_customer_key in Stripe metadata, or as customerExternalId in affitor.trackSale(). Sales must always be tracked server-side.

Verify

Verify success
Browser
  • Visit your site with `?aff=TESTCODE` — an `affitor_click_id` cookie is set
Network
  • `signup()` fires `POST /api/v1/track/lead` with a 2xx response after Supabase SIGNED_IN
Dashboard
  • The click, lead, and sale appear under your program's tracking events
If it doesn't work
  • Send `additional_data: { test_mode: true }` to create test events without commissions

Common mistakes

Common mistakes
  • Firing signup() on every SIGNED_IN event — SIGNED_IN fires on every page load when a session exists, not only on first sign-up. Guard with a sessionStorage flag keyed to the user ID.
  • Using different IDs at signup vs sale — `session.user.id` must match the `customerExternalId` / `affitor_customer_key` sent at sale time.
  • Tracking leads from a server webhook without forwarding the click ID — the browser cookie is not available server-side. Either use the client-side path or store and forward the click ID explicitly.
  • Subscriptions missing `subscription_data.metadata` — renewals won't attribute to the originating partner click.
  • 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 Affitor attributes subscription renewals automatically.

Continue
Edit on GitHub
© 2026 Affitor