Add FounderHQ to Next.js
Install FounderHQ in a Next.js App Router app, in the browser and on the server.
Measure your Next.js app end to end: pageviews from the browser, and facts only your server knows. About 10 minutes.
Before you start
You need a FounderHQ account and two keys. In the app, open
Sequences → Settings → Event API keys. Create one key for
Browser/mobile (fhq_pk_...) and one for Backend
(fhq_sk_...). Each full key is shown only once. Keep the secret key on
your server.
This page covers the App Router. Pages Router? Call founderhq.init once
in pages/_app.tsx instead, and the rest of the page still applies.
Install the browser package
npm install @founderhq/eventsInitialize in a client component
The App Router renders on the server, so init belongs in a client
component that you render once in your root layout.
"use client";
import { useEffect } from "react";
import { founderhq } from "@founderhq/events";
export function FounderHqAnalytics() {
useEffect(() => {
founderhq.init("fhq_pk_XXXX");
}, []);
return null;
}founderhq is a shared client. Import it anywhere. Calling init a
second time does nothing, so a hot reload is safe.
Render it once in the root layout
// app/layout.tsx
import { FounderHqAnalytics } from "./founderhq-analytics";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<FounderHqAnalytics />
{children}
</body>
</html>
);
}From here on, pageviews, page leaves, sessions, clicks, rage clicks, dead clicks, outbound clicks, scroll depth, and Core Web Vitals are captured for you. App Router navigations count as pageviews.
Identify the person when they sign in
founderhq.identify("user_8421", {
email: "jane@acme.com",
plan: "pro",
});Pass your own stable user ID, not an email. Call founderhq.reset() when
the person signs out.
Send server-truth events from route handlers
Your server knows facts the browser cannot prove: a validated signup, a plan change, a webhook you received. Send those with the Node SDK.
npm install @founderhq/events-node// lib/analytics.ts
import { FounderHqNode } from "@founderhq/events-node";
export const events = new FounderHqNode(process.env.FOUNDERHQ_SECRET_KEY!);Put FOUNDERHQ_SECRET_KEY=fhq_sk_XXXX in your server environment, with no
NEXT_PUBLIC_ prefix. That prefix would ship the secret to the browser.
// app/api/upgrade/route.ts
import { events } from "@/lib/analytics";
export async function POST() {
events.capture({
contact: { externalId: "user_8421", email: "jane@acme.com" },
event: "subscription.upgraded",
properties: { plan: "growth", mrr: 99 },
});
await events.flush();
return Response.json({ ok: true });
}Use the same externalId you pass to identify in the browser. That is
what keeps one contact instead of two.
Serverless functions exit fast, so await events.flush() before you
return. Otherwise the batch dies with the function.
Attribute your revenue
Taking payments with Stripe? Follow Stripe end to end to connect the webhook and carry attribution through checkout.
Verify
- Run the app and load two pages.
- In FounderHQ, open Sequences → Events. You see two
$pageviewevents, each with its channel. - Call the route handler once.
- Open Sequences → Contacts and open Jane. You see
subscription.upgradedwithplan: growthon her timeline.
Events can take a few seconds to appear. The SDK batches them.
Troubleshoot
founderhq is not defined during a build
init ran on the server. Keep it inside the client component with
"use client" at the top.
Events fire twice in development
React Strict Mode runs effects twice. init is safe to call twice, but a
capture inside an effect is not. Capture on the user's action, not on
render.
A second contact appeared for the same person
Your browser code and your server code use different IDs. Both must use the same stable user ID. See Identity and contacts.