# Add FounderHQ to Next.js (https://www.getfounderhq.com/docs/analytics/recipes/nextjs)

Measure your Next.js app end to end: pageviews from the browser, and
facts only your server knows. About 10 minutes.

<Callout title="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.
</Callout>

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.

## 1. Install the browser package

```npm
npm install @founderhq/events
```

## 2. 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.

```tsx
"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.

## 3. Render it once in the root layout

```tsx
// 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.

## 4. Identify the person when they sign in

```ts
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.

## 5. 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
npm install @founderhq/events-node
```

```ts
// 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.

```ts
// 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.

## 6. Attribute your revenue

Taking payments with Stripe? Follow
[Stripe end to end](/analytics/recipes/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 **Sequences → Events**. You see two `$pageview`
   events, each with its channel.
3. Call the route handler once.
4. Open **Sequences → Contacts** 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](/analytics/concepts/identity-and-contacts).

## Next

- [Web SDK options](/analytics/sdks/web)
- [Node SDK reference](/analytics/sdks/node)
