Auth0

Connect Auth0 signups to referral clicks with a small Post Login Action and one copied file, with no storage in your database.

The RefRef recipe for Auth0 connects each new account to the browser's referral click. A small Post Login Action marks a new user; your application then sends the signup from the browser that completed it. Nothing is added to your database, and no RefRef key goes to Auth0.

Set up

  1. Add the landing script to the pages where a referral link can land. It keeps the browser's referral handle in the refref_handle cookie.
  2. In Auth0, create a Post Login Action from refref-new-user.js, deploy it, and add it to the Login flow.
  3. Copy the recipe file into your application as refref.ts. Set REFREF_API_URL, REFREF_API_KEY, and REFREF_PROJECT_ID on your server.
  4. Keep the RefRef claim in the session:
lib/auth0.ts
import {
  Auth0Client,
  filterDefaultIdTokenClaims,
} from "@auth0/nextjs-auth0/server";
import { refrefClaims } from "./refref";

export const auth0 = new Auth0Client({
  beforeSessionSaved: async (session) => ({
    ...session,
    user: {
      ...filterDefaultIdTokenClaims(session.user),
      ...refrefClaims(session.user),
    },
  }),
});
  1. Send people to sign-up with a returnTo on your own route, and send the signup there:
<a href="/auth/login?screen_hint=signup&returnTo=/refref/after-login"
  >Sign up</a
>
app/refref/after-login/route.ts
import { cookies } from "next/headers";
import { NextResponse, type NextRequest } from "next/server";
import { auth0 } from "@/lib/auth0";
import { trackAuth0Signup } from "@/lib/refref";

/** The `returnTo` of the login: the browser that completed the sign-up, with
 * its `refref_handle` cookie and the session's claims. */
export async function GET(request: NextRequest) {
  const session = await auth0.getSession();
  if (session)
    await trackAuth0Signup(
      session.user,
      (await cookies()).get("refref_handle")?.value,
    );
  return NextResponse.redirect(new URL("/dashboard", request.url));
}

Keep the API key on your server.

What it does

  • A new account: on the user's first login, the Action adds https://refref.ai/new_user to the ID token, which Auth0 signs. Your route reads it from the session and sends the signup with the browser's handle.
  • Nothing else: a later login is not a first login, so it sends nothing. A user who existed before you added RefRef has logged in before and is never marked.

A user that you import, create with the Management API, or migrate from a legacy database at login has no login yet, so the Action marks their first login. Turn the Action off while you import or migrate users.

Failures

A RefRef failure never fails a login: the route logs it and goes on to the dashboard. The claim stays in the session until the first token refresh, so another server route can call trackAuth0Signup again in that time. A repeated signup returns the first result.

The Action

refref-new-user.js
/**
 * The RefRef Post Login Action for Auth0 (decision 0065). Add it to the
 * Post Login trigger. On a user's first login, which is the login of the
 * sign-up, it adds a claim to the ID token; the application then sends the
 * signup. It holds no RefRef secret and makes no network call.
 *
 * @param {Event} event
 * @param {PostLoginAPI} api
 */
exports.onExecutePostLogin = async (event, api) => {
  const firstLogin =
    event.stats.logins_count === 1 &&
    event.transaction?.protocol !== "oauth2-refresh-token";
  if (!firstLogin) return;
  api.idToken.setCustomClaim("https://refref.ai/new_user", true);
};

The recipe file

lib/refref.ts
/**
 * The RefRef recipe for Auth0 (decision 0065), application part. Copy this
 * file and add the `refref-new-user` Post Login Action to the tenant. It
 * needs no table and stores no referral state:
 *
 * - The Action marks the ID token of a user's first login with
 *   `https://refref.ai/new_user`. Auth0 signs it, and a user who existed
 *   before has logged in already, so it is never marked. An imported or
 *   migrated user has not: turn the Action off while you import users.
 * - `refrefClaims(user)` keeps the claim in the session
 *   (`beforeSessionSaved`).
 * - After the login callback, `trackAuth0Signup(user, handle)` runs on the
 *   app's own route, with the browser's `refref_handle` cookie. It sends the
 *   signup of a marked user. An existing user is never marked, so a sign-in
 *   sends nothing.
 */

const NEW_USER = "https://refref.ai/new_user";

/** The RefRef claim of an ID token, to keep in the session. */
export function refrefClaims(user: Record<string, unknown>) {
  return user[NEW_USER] === true ? { [NEW_USER]: true } : {};
}

let source: Promise<string> | undefined;
/** Registers this Auth0 tenant as a source of the RefRef Project, once per
 * process. RefRef never attributes a signup older than its source namespace,
 * and never identifies its handle. */
function sourceNamespaceId() {
  source ??= call("/v1/sources", {
    provider: "auth0",
    accountKey: process.env.AUTH0_DOMAIN ?? "",
  }).then(
    async (response) =>
      ((await response.json()) as { sourceNamespaceId: string })
        .sourceNamespaceId,
  );
  source.catch(() => (source = undefined));
  return source;
}

/** Registers the source; a RefRef failure never fails the caller. */
export async function prepareRefRefSource() {
  await sourceNamespaceId().catch((error: unknown) => console.error(error));
}

async function call(path: string, body: object) {
  const response = await fetch(
    `${(process.env.REFREF_API_URL ?? "").replace(/\/$/, "")}${path}`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Api-Key": process.env.REFREF_API_KEY ?? "",
      },
      body: JSON.stringify({
        projectId: process.env.REFREF_PROJECT_ID,
        ...body,
      }),
    },
  );
  if (!response.ok)
    throw new Error(`RefRef ${path} failed with status ${response.status}`);
  return response;
}

/**
 * Sends the signup of a user that the Action marked as new, with the handle
 * of the browser that completed the sign-up. Repeating it sends the same
 * signup, which RefRef takes once. A RefRef failure never fails the login.
 */
export async function trackAuth0Signup(
  user: Record<string, unknown>,
  handle: string | undefined,
) {
  if (user[NEW_USER] !== true || typeof user.sub !== "string") return;
  try {
    await call("/v1/track/events", {
      sourceNamespaceId: await sourceNamespaceId(),
      type: "signup",
      // No time: the signup takes its receipt, which never predates the
      // source, wherever the sign-up started.
      participant: { kind: "individual", externalId: user.sub },
      ...(handle && { identify: { handle } }),
    });
  } catch (error) {
    console.error(error);
  }
}

Check your integration

TestExpected result
A shares a link; B follows it and signs up on Auth0B's signup has a referral from A
B logs in againNo second signup
An existing user follows another referral link and logs inThe first referral stays

On this page