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

  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. Register your application as a source with POST /v1/sources and keep its sourceNamespaceId.
  3. 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

milestoneThe signup counts
account-created (default)When the account is created
email-verifiedWhen the person who created the account verifies it
group-createdWhen the user creates an organization
  • account-created works for every sign-up method: email and password, social login, magic link, and email code.
  • email-verified counts 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-created uses 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, resolveSignup also gets organization with its id, name, and slug.
refrefAnalytics({
  // ...
  milestone: "group-created",
  resolveSignup: async ({ user, organization }) => ({
    participant: { kind: "group", externalId: organization.id },
    actor: { kind: "individual", externalId: user.id },
  }),
});

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-created mode, a later email verification also sends nothing. In email-verified mode, 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.

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

TestExpected result
A shares a link; B follows it and signs up with emailB's signup has a referral from A
B signs up with a social login insteadSame result
B asks for a magic link on a laptop and opens it on a phoneB's signup has a referral from A
B signs in againNo second signup
With email-verified, B never verifiesNo signup and no referral
An existing user follows a referral link and signs inNo referral

On this page