Supabase Auth
Connect Supabase Auth signups to referral clicks with one copied file, with no storage in your database.
The RefRef recipe for Supabase Auth connects each new account to the browser's referral click. It is one file that you copy into your application. It adds nothing to your database.
Set up
- Add the landing script to the pages where a referral link can land. It keeps the browser's referral handle in the
refref_handlecookie. - Copy the recipe file into your application as
refref.ts. SetREFREF_API_URL,REFREF_API_KEY, andREFREF_PROJECT_IDon your server. - On the server, add the RefRef data to each sign-up. The first call also registers your Supabase project as a source, before its first account; a RefRef failure never fails the sign-up.
import { cookies } from "next/headers";
import { refrefSignupData } from "./refref";
const data = await refrefSignupData(
(await cookies()).get("refref_handle")?.value,
);
await supabase.auth.signUp({
email,
password,
options: { data, emailRedirectTo },
});
await supabase.auth.signInWithOtp({
email,
options: { data, emailRedirectTo },
});- Where the user is confirmed, send the signup:
import { trackSignup } from "./refref";
const { data } = await supabase.auth.verifyOtp({ token_hash, type });
if (data.user) await trackSignup(data.user);- To make links work in any browser, change the Confirm signup and Magic link email templates to link to your confirm route with the token hash:
<a href="{{ .RedirectTo }}?token_hash={{ .TokenHash }}&type=email">Confirm</a>Build emailRedirectTo from your configured site URL, never from the request's Host header. Keep the API key on your server.
What it does
- A new account: Supabase writes the RefRef data into the user's metadata only when it creates the user. When the email is confirmed, the recipe sends the signup with the browser's handle. The signup counts at the confirmation.
- A link opened on another device: the handle travels in the user's metadata, so the click counts in any browser.
- Nothing else: a sign-in or a sign-up for an existing email does not change the metadata. A user that existed before you added RefRef has no RefRef data and sends nothing. If such a user adds RefRef data to its own metadata, RefRef refuses it: the account is older than the source, so its signup does not count and identifies no browser.
- OAuth: Supabase OAuth sign-in has no sign-up data, so this recipe does not cover OAuth sign-ups yet.
Failures
A RefRef failure never fails a sign-in. The recipe logs it. Send the same call again later: a repeated signup returns the first result.
The recipe file
/**
* The RefRef recipe for Supabase Auth (decision 0065). Copy this file into a
* Supabase application. It needs no table and stores no referral state:
*
* - `await refrefSignupData(handle)` goes into `options.data` of `signUp`
* and `signInWithOtp`. Supabase writes it into the user's metadata only
* when it creates the user, so it marks a user that a sign-up of this app
* created, with the handle of the browser that started it. A sign-in or a
* sign-up for an existing email never changes it. It also registers the
* app as a source before the first account exists.
* - `trackSignup(user)` runs where the user is confirmed: `/auth/confirm` and
* the email code check. It sends the signup with that handle.
*/
import type { User } from "@supabase/supabase-js";
/** The `options.data` of a sign-up that this app starts. A RefRef failure
* never fails the sign-up. */
export async function refrefSignupData(handle: string | undefined) {
await sourceNamespaceId().catch((error: unknown) => console.error(error));
return { refref_signup: true, ...(handle && { refref_handle: handle }) };
}
let source: Promise<string> | undefined;
/** Registers this Supabase project as a source of the RefRef Project, once
* per process, before the first account exists. RefRef never attributes a
* signup older than its source namespace, and never identifies its handle,
* so users that existed before cannot be attributed. */
function sourceNamespaceId() {
source ??= call("/v1/sources", {
provider: "supabase-auth",
accountKey: process.env.NEXT_PUBLIC_SUPABASE_URL ?? "",
}).then(
async (response) =>
((await response.json()) as { sourceNamespaceId: string })
.sourceNamespaceId,
);
source.catch(() => (source = undefined));
return source;
}
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 a sign-up of this app created. A user with
* no `refref_signup` mark (created before the integration, by an admin, or by
* another app) sends nothing. A later sign-in sends the same signup again,
* which RefRef takes once. The time is the account creation: a user created
* before the source namespace never qualifies.
*/
export async function trackSignup(user: User) {
const metadata = user.user_metadata as Record<string, unknown>;
if (metadata.refref_signup !== true) return;
const handle =
typeof metadata.refref_handle === "string"
? metadata.refref_handle
: undefined;
try {
await call("/v1/track/events", {
sourceNamespaceId: await sourceNamespaceId(),
type: "signup",
participant: { kind: "individual", externalId: user.id },
occurredAt: new Date(user.created_at).toISOString(),
...(handle && { identify: { handle } }),
});
} catch (error) {
// Sign-in never fails for RefRef. Log, and send the same call again
// later: a repeated signup returns the first result.
console.error(error);
}
}Check your integration
| Test | Expected result |
|---|---|
| A shares a link; B follows it, signs up, and confirms | B's signup has a referral from A |
| B signs up but never confirms | No signup and no referral |
| B asks for a magic link on a laptop and opens it on a phone | B's signup has a referral from A |
| An existing user follows another referral link and signs in | The first referral stays |