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.
- 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 },
},
});- 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
- Visit your site with `?aff=TESTCODE` — an `affitor_click_id` cookie is set
- `signup()` fires `POST /api/v1/track/lead` with a 2xx response after Supabase SIGNED_IN
- The click, lead, and sale appear under your program's tracking events
- Send `additional_data: { test_mode: true }` to create test events without commissions
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).