React and Next.js
Capture referrals, show who invited a visitor, attribute signups and receive webhooks with @referalli/react and @referalli/next.
Two kits cover a React or Next.js app:
@referalli/reactruns 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/nextruns 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 secretrk_key through thereferalliclient.
npm install @referalli/react @referalli/next referalliBoth 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 | |
|---|---|---|
| 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. |
|
| Cookie lifetime, 1 to 400 days. |
|
| Keep the first referral until it expires instead of the latest (for |
| the host | Share the cookie with subdomains, such as |
|
| 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 | |
|---|---|
| Your public tracking token (required). Never an |
| A program handle, used by |
| Default |
| Default |
| Default |
| A referral read on the server with |
| Tracker options. |
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.
Share links and partner signup
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.