Better Auth
Connect Better Auth signups to referral clicks with the RefRef plugin, with configuration only and no storage in your database.
The RefRef plugin for Better Auth sends the signup of each new account to RefRef and connects it to the browser's referral click. You write configuration only. The plugin needs no client secret and adds nothing to your database.
The plugin package is not published yet. Until it is, use the calls in signup tracking with any auth system from your Better Auth hooks.
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. - Register your application as a source with
POST /v1/sourcesand keep itssourceNamespaceId. - Add the plugin to your Better Auth configuration:
import { betterAuth } from "better-auth";
import { refrefAnalytics } from "@refref/better-auth";
export const auth = betterAuth({
// ...your configuration
plugins: [
refrefAnalytics({
apiUrl: "https://api.refref.ai",
apiKey: process.env.REFREF_API_KEY!,
projectId: process.env.REFREF_PROJECT_ID!,
sourceNamespaceId: process.env.REFREF_SOURCE_NAMESPACE_ID!,
resolveSignup: async ({ user }) => ({
participant: { kind: "individual", externalId: user.id },
}),
}),
],
});resolveSignup names the customer with your own ID. Return the same customer for every call about one user. Keep the API key on your server.
Choose when a signup counts
milestone | The signup counts |
|---|---|
account-created (default) | When the account is created |
email-verified | When the person who created the account verifies it |
group-created | When the user creates an organization |
account-createdworks for every sign-up method: email and password, social login, magic link, and email code.email-verifiedcounts only a verified email. An account that is verified when it is created, for example a social login whose provider gives a verified email, counts at once. Use this mode when your Program pays referrers, so an account created with someone else's email does not earn.group-createduses the Better Auth organization plugin. The organization is the referred customer, and the user who created it is the actor. Members who join later do not count. In this mode,resolveSignupalso getsorganizationwith itsid,name, andslug.
refrefAnalytics({
// ...
milestone: "group-created",
resolveSignup: async ({ user, organization }) => ({
participant: { kind: "group", externalId: organization.id },
actor: { kind: "individual", externalId: user.id },
}),
});Magic links and email codes on another device
A person can ask for a magic link on a laptop and open it on a phone. The plugin handles this for you: when it sends the link or code, it connects the laptop's click to that one challenge, and when the challenge creates the account, the click counts on any device. Nothing is stored in your database.
This needs Better Auth to keep its verification rows in the database. With secondaryStorage, set verification.storeInDatabase: true. With emailOTP({ resendStrategy: "reuse" }), email codes count only when they are completed in the browser that asked for them.
What the plugin ignores
- A sign-in or linking another login method sends nothing. In
account-createdmode, a later email verification also sends nothing. Inemail-verifiedmode, the first verification is the signup. - An account that a signed-in administrator creates, or that your backend creates without a browser, gets a signup but no referral click.
- An anonymous user sends nothing. The real account that the person creates later counts.
Keep the cookie longer in Safari
Safari keeps a cookie that a script writes for 7 days. The plugin has an endpoint that sets the same cookie from your server for 90 days. Call it after capture:
await window.RefRefAttribution.capture();
await fetch("/api/auth/refref/handle", { method: "POST" });If the landing script uses data-cookie-domain, give the plugin the same value in cookieDomain.
Failures
A RefRef failure never fails a sign-up. The plugin tries each call up to three times. When a call still fails, onError(error, delivery) gets it. By default the plugin logs it. To retry later, keep delivery and send it again in the order you received it.
refrefAnalytics({
// ...
onError: async (error, delivery) => {
if (delivery) await saveForRetry(delivery);
},
});Check your integration
| Test | Expected result |
|---|---|
| A shares a link; B follows it and signs up with email | B's signup has a referral from A |
| B signs up with a social login instead | Same result |
| B asks for a magic link on a laptop and opens it on a phone | B's signup has a referral from A |
| B signs in again | No second signup |
With email-verified, B never verifies | No signup and no referral |
| An existing user follows a referral link and signs in | No referral |