supapulsedocs
Integration guidesNext.js

Next.js

Add Supapulse to a Next.js app with the App Router or the Pages Router, track events, and record revenue from a route handler.

Install

npm install @supapulse/core @supapulse/react
.env.local
NEXT_PUBLIC_SUPAPULSE_TOKEN=pub_your_public_token
SUPAPULSE_SECRET_TOKEN=sec_your_secret_token

App Router

Wrap the provider in a client component. Recent releases of @supapulse/react include a 'use client' directive, but the wrapper works with every version:

app/analytics.tsx
'use client';

import { SupapulseProvider } from '@supapulse/react';
import type { ReactNode } from 'react';

const config = {
  apiToken: process.env.NEXT_PUBLIC_SUPAPULSE_TOKEN!,
  debug: process.env.NODE_ENV === 'development',
};

export function Analytics({ children }: { children: ReactNode }) {
  return <SupapulseProvider config={config}>{children}</SupapulseProvider>;
}
app/layout.tsx
import { Analytics } from './analytics';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Analytics>{children}</Analytics>
      </body>
    </html>
  );
}

Server components can sit inside <Analytics>; only components that call the hooks need 'use client'. App Router navigations use pushState, so each route change is a pageview with no extra code.

Without React

The script tag works too, with next/script:

app/layout.tsx
import Script from 'next/script';

<Script
  src="https://cdn.jsdelivr.net/npm/@supapulse/core@1/dist/browser.js"
  data-token={process.env.NEXT_PUBLIC_SUPAPULSE_TOKEN}
  strategy="afterInteractive"
/>;

Pages Router

pages/_app.tsx
import { SupapulseProvider } from '@supapulse/react';
import type { AppProps } from 'next/app';

const config = { apiToken: process.env.NEXT_PUBLIC_SUPAPULSE_TOKEN! };

export default function App({ Component, pageProps }: AppProps) {
  return (
    <SupapulseProvider config={config}>
      <Component {...pageProps} />
    </SupapulseProvider>
  );
}

Events

app/pricing/plan-button.tsx
'use client';

import { useEvent } from '@supapulse/react';

export function PlanButton({ plan }: { plan: string }) {
  const trackEvent = useEvent();
  return <button onClick={() => trackEvent('plan_selected', { plan })}>Choose {plan}</button>;
}

Revenue

1. Identify the customer

Call identify() once the user is known. Wait for isReady, because a call made before the provider has initialized the tracker is dropped.

app/identify-user.tsx
'use client';

import { SupapulseContext, useIdentify } from '@supapulse/react';
import { useContext, useEffect } from 'react';

export function IdentifyUser({ userId }: { userId: string }) {
  const identify = useIdentify();
  const isReady = useContext(SupapulseContext)?.isReady;

  useEffect(() => {
    if (isReady) identify(userId);
  }, [identify, isReady, userId]);

  return null;
}

2. Connect Stripe

Add the endpoint from the site's settings in the Stripe dashboard. See the Stripe guide. To link a payment to the visit directly, pass the visitor details in the checkout metadata:

app/pricing/checkout-button.tsx
'use client';

import { useSupapulse } from '@supapulse/react';

export function CheckoutButton({ priceId }: { priceId: string }) {
  const supapulse = useSupapulse();

  async function checkout() {
    const response = await fetch('/api/checkout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        priceId,
        fingerprint: await supapulse.getVisitorFingerprint(),
        sessionId: supapulse.getSessionId(),
      }),
    });
    window.location.href = (await response.json()).url;
  }

  return <button onClick={checkout}>Buy</button>;
}
app/api/checkout/route.ts
import Stripe from 'stripe';

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

export async function POST(request: Request) {
  const { priceId, fingerprint, sessionId } = await request.json();
  const user = await currentUser(); // your auth
  const metadata = {
    supapulse_customer_id: user.id,
    supapulse_visitor_fingerprint: fingerprint ?? '',
    supapulse_session_id: sessionId ?? '',
  };

  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/thanks`,
    metadata,
    subscription_data: { metadata },
  });
  return Response.json({ url: session.url });
}

3. Other processors

Send revenue from your own route handler. The handler is in Payment webhooks; read the raw body with request.text() so the signature check works.

Common issues

The token is undefined in the browser. Only NEXT_PUBLIC_ variables reach client code, and they're inlined at build time. Rebuild after changing them.

Importing SupapulseProvider in layout.tsx errors. Server components can't use context. Use the client wrapper above.

Two pageviews per navigation. Remove any usePageview() call, or set autoPageviews: false and keep only the hook.