Rekomi Docs
For brandsInstall tracking
Brands

Install on Next.js App Router

Use next/script in app/layout.tsx with strategy="afterInteractive". Raw script tags in metadata get stripped. Pass the affiliate slug from the client via window.Rekomi.getReferral().

Next.js 16's App Router serializes the document differently from the Pages Router, and the metadata API doesn't accept raw <script> tags (they get stripped on serialization). The supported install is the next/script component in your root layout. App-Router-specific patterns also apply for Server Actions, Streaming SSR, and the Content Security Policy if you have one configured.

Install in app/layout.tsx

Edit app/layout.tsx and drop a <Script> component at the end of <body>:

import Script from "next/script";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script
          id="rekomi"
          src="https://api.rekomi.com/api/v1/r/loader.js"
          strategy="afterInteractive"
          data-program-id={process.env.NEXT_PUBLIC_REKOMI_PROGRAM_ID}
        />
      </body>
    </html>
  );
}

strategy="afterInteractive" is the right choice for click capture: it loads after hydration but before the user idles, so the referral is captured before the visitor clicks any CTAs. strategy="lazyOnload" would defer too long; strategy="beforeInteractive" would block the critical render path.

Set NEXT_PUBLIC_REKOMI_PROGRAM_ID in your .env.local (and Vercel/Netlify env vars for production). The NEXT_PUBLIC_ prefix is required for Next to expose the variable to the browser bundle.

Recording the sale

The browser pixel no longer records sales, so there is no convert call to fire from a client component on the success route. A sale is recorded through one of these paths:

  • Your payment gateway. Connect it once in the Connect payment gateway step at /dashboard/setup/connect-sales (Stripe, Paddle, Braintree, Shopify, Wix, Lemon Squeezy, Chargebee, Polar, Recurly, Gumroad, Creem, or Dodo Payments) and sales, refunds, and cancellations record automatically.
  • Server-to-server. POST the sale from a Route Handler or Server Action to Rekomi's S2S endpoint with an HMAC signature. See Server-to-server tracking.
  • Coupon codes. Give each affiliate a unique discount code (Campaign > Coupons); a redeemed code credits them at checkout.

Carry the referral into checkout with window.Rekomi.getReferral() from a client component.

Server Actions: pass the affiliate slug from the client

The Rekomi loader persists the referral to localStorage on your domain, not to a server-readable cookie, so a Server Action cannot read it from the request cookie jar. Instead, read the slug in a client component with window.Rekomi.getReferral() and pass it into the Server Action as an argument. Your action then stamps it on the subscription's metadata as rekomi_affiliate_slug, which is what Rekomi reads for attribution:

// app/actions.ts
"use server";

import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function createCheckoutSession(priceId: string, referral?: string) {
  const session = await stripe.checkout.sessions.create({
    mode: "subscription",
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
    subscription_data: referral
      ? { metadata: { rekomi_affiliate_slug: referral } }
      : undefined,
  });

  return session.url;
}
// a client component that triggers checkout
"use client";

import { createCheckoutSession } from "./actions";

export function BuyButton({ priceId }: { priceId: string }) {
  async function checkout() {
    const referral = window.Rekomi?.getReferral?.() ?? undefined;
    const url = await createCheckoutSession(priceId, referral);
    if (url) window.location.href = url;
  }

  return <button onClick={checkout}>Subscribe</button>;
}

Passing the referral as an explicit argument (or an HTTP header to a Route Handler) is the correct pattern; there is no rekomi_via request cookie to read server-side.

Capturing leads (optional)

window.Rekomi.convert() is now a LEAD capture call, not a sale. On a signup route you can tie a customer email to the referral before payment:

"use client";

import { useEffect } from "react";

export default function SignupThanks({ email }: { email: string }) {
  useEffect(() => {
    if (email) window.Rekomi?.ready?.(() => window.Rekomi.convert(email));
  }, [email]);

  return <div>Thanks for signing up!</div>;
}

Browser lead capture works on every plan; the server-side lead endpoint requires Starter or higher. See Track leads and signups.

Content Security Policy

If you have a strict CSP configured in next.config.ts headers, allow Rekomi's script source and tracking endpoints:

// next.config.ts
const ContentSecurityPolicy = `
  default-src 'self';
  script-src 'self' https://api.rekomi.com;
  connect-src 'self' https://api.rekomi.com;
  img-src 'self' https://api.rekomi.com data:;
`;

export default {
  async headers() {
    return [{
      source: "/(.*)",
      headers: [{
        key: "Content-Security-Policy",
        value: ContentSecurityPolicy.replace(/\s{2,}/g, " ").trim(),
      }],
    }];
  },
};

Without script-src permission, the browser silently blocks the loader. Without connect-src, the click POSTs fail.

Middleware: usually unnecessary

Click capture happens entirely client-side (the loader runs after hydration, persists the referral to localStorage, and exposes window.Rekomi). You only need Rekomi logic in middleware.ts if you want server-side handling before the page renders. For standard SaaS flows, the client-side capture is sufficient.

Quirks worth knowing

Don't put raw <script> in metadata.other. App Router strips raw script tags from metadata for security. Use next/script in app/layout.tsx instead.

Metadata in client components is ignored. Next 16 only respects metadata exports from server components. The app/layout.tsx is server by default; keep it that way.

strategy="afterInteractive" is the right choice. Other strategies (beforeInteractive, worker, lazyOnload) have different trade-offs. After-interactive is correct for click-capture analytics because it loads after the page is usable but before idle.

The referral is client-side only. Because the loader writes to localStorage, read it with window.Rekomi.getReferral() in a client component and pass it into Server Actions or Route Handlers. There is no server-readable rekomi_via cookie.

Troubleshooting

Script tag appears in HTML but loader doesn't execute. CSP blocking. Check the browser console for CSP violation errors and add api.rekomi.com to script-src.

rekomi_affiliate_slug is missing from the subscription's metadata. The referral did not reach your Server Action. Confirm the client component reads window.Rekomi.getReferral() and passes it as the argument, and that the loader captured a referral on the landing page (check getReferral() in the console).

useSearchParams returns null in production but works locally. App Router requires useSearchParams to be wrapped in <Suspense> for static optimization. Wrap the consuming component's body in <Suspense fallback={null}> if you're hitting this.

Hydration mismatch error after adding <Script>. Make sure you're not putting <Script> outside <html> or <body>. The component goes inside <body> (typically at the end, so it doesn't block render).