React and Next.js

Capture referrals, show who invited a visitor, attribute signups and receive webhooks with @referalli/react and @referalli/next.

Embed
Preview of the card
<a href="https://referalli.com/docs/react-and-nextjs"><img src="https://referalli.com/docs/_api/public/orgs/07f432f4-34b9-46a7-aaad-c0e395d2e4f9/spaces/2bf4f614-3630-4fbf-85f9-5ecafa6b7d97/pages/dda41763-5cb2-4e5f-96f0-a765e40b8585/social-card.svg?v=3e7ae33eec8b" alt="React and Next.js" width="600" height="315"></a>

Two kits cover a React or Next.js app:

  • @referalli/react runs in the browser. It captures the referral from the landing URL, records the visit, tells you who invited the visitor, and renders partner share links. It uses only your public tracking token.

  • @referalli/next runs on your server. It has edge middleware that saves the referral before any page renders, getReferral() for server components and actions, attributeSignup(), and a webhook route handler. It uses the secret rk_ key through the referalli client.

npm install @referalli/react @referalli/next referalli

Both kits read and write the same first-party cookie, referalli_ctx, which the tracking script uses too, so you can mix them. A complete app is nextjs-saas in referalli-examples.

Environment

# .env.local
REFERALLI_API_KEY=rk_test_...            # server only
REFERALLI_WEBHOOK_SECRET=whsec_...       # server only
NEXT_PUBLIC_REFERALLI_TRACKING_TOKEN=tk_...  # public: safe in the browser
REFERALLI_PROGRAM_ID=prg_...

The tracking token is in GET /v1/org (trackingToken) and under Settings → Tracking.

Capture the referral (middleware)

// middleware.ts
export { referalliMiddleware as middleware } from '@referalli/next/middleware';
export const config = { matcher: ['/((?!_next|api|.*\\..*).*)'] };

When a request URL carries ?via=, ?ref=, ?aff= or ref_click_id (a referral link redirects with these), the middleware saves the referral in the cookie. The cookie is not HttpOnly, so the browser kit shares it. To pass options, or to add it to a middleware you already have:

// middleware.ts
import { NextResponse, type NextFetchEvent, type NextRequest } from 'next/server';
import { captureReferral } from '@referalli/next/middleware';

export function middleware(req: NextRequest, event: NextFetchEvent) {
  const res = NextResponse.next();
  return captureReferral(
    req,
    res,
    { publishableKey: process.env.NEXT_PUBLIC_REFERALLI_TRACKING_TOKEN, cookieDomain: '.example.com' },
    event,
  );
}

Option

Default

publishableKey

none

Your tracking token. When set, the landing is also recorded from the server, so it counts on pages that never load the browser kit.

cookieDays

60

Cookie lifetime, 1 to 400 days.

firstTouch

false

Keep the first referral until it expires instead of the latest (for first_click programs).

cookieDomain

the host

Share the cookie with subdomains, such as .example.com.

params

via, ref, aff

Query parameters that carry a code, in priority order.

The middleware imports only edge-safe code.

Read the referral on the server

// app/page.tsx
import { getReferral } from '@referalli/next';

export default async function Home() {
  const referral = await getReferral(); // { anonId, code, clickId, ts } | null
  return referral?.code ? <p>You were invited by a friend: welcome!</p> : null;
}

In Pages Router API routes, use getReferralFromRequest(req).

Attribute the signup

Call attributeSignup from the server action or route handler that creates the user. It reads the cookie, hashes the email, and calls associate with the idempotency key signup:<customerRef>. It never throws, so attribution can't break your signup.

// app/signup/actions.ts
'use server';
import { Referalli } from 'referalli';
import { attributeSignup } from '@referalli/next';

const referalli = new Referalli(); // reads REFERALLI_API_KEY

export async function signUp(form: FormData) {
  const user = await db.users.create({ email: String(form.get('email')) });
  await attributeSignup({
    client: referalli,
    programId: process.env.REFERALLI_PROGRAM_ID!,
    customerRef: user.id, // the id your billing provider sees for this customer
    email: user.email,
  });
}

customerRef must match what your billing provider reports for the customer (with Stripe, set it to the cus_… id, or put your user id in the customer's metadata). See Associating customers.

The browser kit

Wrap your app in ReferalliProvider. In the App Router, the kit's components are client components, so you can render the provider from app/layout.tsx:

// app/layout.tsx
import { ReferalliProvider } from '@referalli/react';
import { getReferral } from '@referalli/next';

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const referral = await getReferral();
  return (
    <html lang="en">
      <body>
        <ReferalliProvider
          publishableKey={process.env.NEXT_PUBLIC_REFERALLI_TRACKING_TOKEN!}
          program="acme"
          initialReferral={referral}
        >
          {children}
        </ReferalliProvider>
      </body>
    </html>
  );
}

initialReferral lets the page render the referral before hydration. Nothing touches window until the provider mounts, so it is SSR-safe in Next.js, Remix and Vite apps.

Prop

publishableKey

Your public tracking token (required). Never an rk_ key.

program

A program handle, used by <PartnerSignup>.

baseUrl

Default https://referalli.com.

track

Default true. Set it to false until the visitor gives consent.

resolvePartner

Default true. Looks up the partner's display name, for "Invited by Dana".

initialReferral

A referral read on the server with getReferral().

persist, cookieDays, firstTouch, cookieDomain, params

Tracker options. persist: 'referral-only' stores nothing for visitors who arrive without a referral.

Hooks

'use client';
import { useReferral, useTrackSignup } from '@referalli/react';

export function InviteBanner() {
  const { code, partner, ready } = useReferral();
  if (!ready || !code) return null;
  return <p>Invited by {partner?.partnerDisplayName ?? 'a friend'}</p>;
}

export function useSignup() {
  const trackSignup = useTrackSignup();
  return async (user: { id: string; email: string }) => {
    await trackSignup({ customerRef: user.id, email: user.email }); // never throws
  };
}

trackSignup hashes the email in the browser before sending it. It is a convenience signal; for attribution you can rely on, call attributeSignup on the server as shown above.

import { PartnerSignup, ReferralLink } from '@referalli/react';

<ReferralLink url={link.shortUrl} styled />
<ReferralLink code="DANA" destination="https://example.com/pricing" />
<ReferralLink code="DANA">
  {({ url, copy, copied }) => <button onClick={copy}>{copied ? 'Copied!' : url}</button>}
</ReferralLink>

<PartnerSignup handle="acme" styled />

The components are unstyled by default. Each element has a data-referalli attribute (referral-link, referral-link-copy, partner-signup-cta, …) to target with CSS. Pass styled for the built-in look, themed with the CSS variables --referalli-accent, --referalli-radius, --referalli-border and --referalli-font. <PartnerSignup> shows a program's public pitch and links to the hosted partner signup page.

Receive webhooks

// app/api/webhooks/referalli/route.ts
import { createWebhookHandler } from '@referalli/next';

export const POST = createWebhookHandler({
  secret: process.env.REFERALLI_WEBHOOK_SECRET!,
  on: {
    'referral.created': async (event) => {
      console.log('referred', event.data.customerRef, 'by', event.data.partnerId);
    },
    'commission.earned': async (event) => {
      console.log('earned', event.data.amountMinor, event.data.currency);
    },
  },
});

The handler reads the raw body, checks the signature and timestamp, and calls the handler for the event's topic, with event.data typed for that topic. It answers 400 when verification fails, 200 when the event is handled, and 500 when your handler throws, which makes Referalli retry the delivery. For full control, use const event = await verifyWebhook(req, { secret }).

Locally, run referalli listen --forward-to http://localhost:3000/api/webhooks/referalli and set REFERALLI_WEBHOOK_SECRET to the secret it prints. See the CLI and Webhooks.

Updated