Documentation
Recipes

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 Analytics → Settings → Events SDK. Create one key for Browser or mobile (fhq_pk_...) and one for Backend (fhq_sk_...). Each full key is shown only once. Keep the secret key on your server.

Nothing is sent anywhere. A publishable key is saved in this browser. A secret key is kept only until you reload.

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/events

Initialize 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, page views, 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 page views.

Using a cookie banner? Pass { consent_default: "pending" } to init, and call founderhq.consent("granted") when the visitor accepts. Until then FounderHQ counts page views only, and ignores identify and your own browser events.

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

  1. Run the app and load two pages.
  2. In FounderHQ, open Analytics → Events & Visits. You see two $pageview events, each with its channel.
  3. Call the route handler once.
  4. Open Analytics → People and open Jane. You see subscription.upgraded with plan: growth on 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.

Next

AI agent or LLM? Read this page as markdown

On this page