# Next.js

URL: https://supapulse.app/docs/guides/nextjs

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 [#install]

```bash
npm install @supapulse/core @supapulse/react
```

```bash title=".env.local"
NEXT_PUBLIC_SUPAPULSE_TOKEN=pub_your_public_token
SUPAPULSE_SECRET_TOKEN=sec_your_secret_token
```

## App Router [#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:

```tsx title="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>;
}
```

```tsx title="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 [#without-react]

The script tag works too, with `next/script`:

```tsx title="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-router]

```tsx title="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 [#events]

```tsx title="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 [#revenue]

### 1. Identify the customer [#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.

```tsx title="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 [#2-connect-stripe]

Add the endpoint from the site's settings in the Stripe dashboard. See the [Stripe guide](https://supapulse.app/docs/guides/stripe). To link a payment to the visit directly, pass the visitor details in the checkout metadata:

```tsx title="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>;
}
```

```ts title="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 [#3-other-processors]

Send revenue from your own route handler. The handler is in [Payment webhooks](https://supapulse.app/docs/revenue/webhooks#paddle); read the raw body with `request.text()` so the signature check works.

## Common issues [#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.
