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/reactNEXT_PUBLIC_SUPAPULSE_TOKEN=pub_your_public_token
SUPAPULSE_SECRET_TOKEN=sec_your_secret_tokenApp 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:
'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>;
}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:
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
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
'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.
'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:
'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>;
}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.