WorkOS AuthKit
Connect WorkOS AuthKit signups to referral clicks from your callback route, with one copied file and no storage in your database.
The RefRef recipe for WorkOS AuthKit connects each new account to the browser's referral click. AuthKit returns to your callback route in the browser that completed the sign-up; the recipe sends the signup from there. Nothing is added to your database, and no RefRef key goes to WorkOS.
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. - Send the signup from the
onSuccessof your AuthKit callback route:
import { handleAuth } from "@workos-inc/authkit-nextjs";
import { cookies } from "next/headers";
import { trackWorkOSSignup } from "@/lib/refref";
/** AuthKit's callback, in the browser that completed the sign-in or sign-up. */
export const GET = handleAuth({
returnPathname: "/dashboard",
onSuccess: async ({ user, impersonator }) => {
await trackWorkOSSignup(
user,
(await cookies()).get("refref_handle")?.value,
impersonator != null,
);
},
});Keep the API key on your server.
What it does
- A new account: AuthKit gives no "new user" flag, so the recipe counts a user whose account is at most 24 hours old as new, and sends the signup with the browser's handle.
- Nothing else: a sign-in by an older account sends nothing, so a user created more than 24 hours before you added RefRef is never sent. An impersonated session sends nothing.
- A repeated sign-in: a second sign-in within the 24 hours sends the same signup. RefRef keeps the first and identifies no other handle.
A user that you create or import and who signs in within 24 hours counts as new. Do not send a referral link to a user that you import.
Failures
A RefRef failure never fails a sign-in: AuthKit clears the session when onSuccess throws, so the recipe logs the error instead. A sign-in within the 24 hours sends the signup again, and a repeated signup returns the first result.
The recipe file
/**
* The RefRef recipe for WorkOS AuthKit (decision 0065). Copy this file into
* an AuthKit application. It needs no table and stores no referral state:
*
* - `trackWorkOSSignup(user, handle, impersonated)` runs in the `onSuccess`
* of `handleAuth`, the callback route of the application, with the
* browser's `refref_handle` cookie. AuthKit gives no "new user" flag, so a
* user whose account is at most 24 hours old counts as new: WorkOS creates
* the account before the person enters an emailed code, which can take a
* while. A user created more than 24 hours before the integration never
* counts.
* - The signup has no time: its receipt never predates the source namespace.
* A repeated callback within the 24 hours sends the same signup, which
* RefRef takes once.
*/
/** How long after the account creation a callback still counts as its sign-up. */
const NEW_USER_MS = 24 * 60 * 60 * 1000;
let source: Promise<string> | undefined;
/** Registers this WorkOS environment as a source of the RefRef Project, once
* per process. */
function sourceNamespaceId() {
source ??= call("/v1/sources", {
provider: "workos",
accountKey: process.env.WORKOS_CLIENT_ID ?? "",
}).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 new user with the handle of the browser that
* completed the sign-up. An impersonated session or an older account sends
* nothing. A RefRef failure never fails the login: AuthKit clears the session
* when `onSuccess` throws, so this function never throws.
*/
export async function trackWorkOSSignup(
user: { id: string; createdAt: string },
handle: string | undefined,
impersonated: boolean,
) {
// A clock behind WorkOS gives a new account a negative age; an invalid
// time gives NaN, which fails the test.
const age = Date.now() - new Date(user.createdAt).getTime();
if (impersonated || !(age <= NEW_USER_MS)) return;
try {
await call("/v1/track/events", {
sourceNamespaceId: await sourceNamespaceId(),
type: "signup",
participant: { kind: "individual", externalId: user.id },
...(handle && { identify: { handle } }),
});
} catch (error) {
console.error(error);
}
}