> For the complete documentation index, see [llms.txt](https://docs.millimetric.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.millimetric.ai/sdks/frameworks/nextjs.md).

# Next.js

## Install

```bash
npm i @millimetric/track          # browser
npm i @millimetric/track-node     # server-side
```

## App Router — the recommended pattern

A tiny client component you mount in the root layout:

```tsx
// app/_components/Analytics.tsx
"use client";

import { useEffect } from "react";
import { init } from "@millimetric/track";

export function Analytics() {
  useEffect(() => {
    init({
      key: process.env.NEXT_PUBLIC_AOA_KEY!,
      host: process.env.NEXT_PUBLIC_AOA_HOST ?? "https://api.millimetric.ai"
    });
  }, []);
  return null;
}
```

```tsx
// app/layout.tsx
import { Analytics } from "./_components/Analytics";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <Analytics />
        {children}
      </body>
    </html>
  );
}
```

The SDK patches `history.pushState`, so App Router navigations auto-fire `$pageview` without any extra hook.

## Identify after auth resolves

```tsx
// app/_components/IdentifyOnAuth.tsx
"use client";

import { useEffect } from "react";
import { identify } from "@millimetric/track";
import { useUser } from "@/lib/auth";

export function IdentifyOnAuth() {
  const user = useUser();
  useEffect(() => {
    if (user) identify(user.id, { email: user.email, plan: user.plan });
  }, [user?.id]);
  return null;
}
```

Mount once next to `<Analytics />`. With Clerk / Auth.js / Supabase Auth, replace `useUser()` with the appropriate hook.

## Server-side events from a Route Handler

Use `@millimetric/track-node` with an `sk_*` key. **Never put `sk_*` in a `NEXT_PUBLIC_*` env var** — it'll ship to the browser bundle.

```ts
// app/api/checkout/route.ts
import { NextResponse } from "next/server";
import { init, track, flush } from "@millimetric/track-node";

init({
  key: process.env.AOA_SK!,
  host: process.env.AOA_HOST!,
  flushAt: 1
});

export async function POST(req: Request) {
  const { amount_cents, currency, anonymous_id, user_id } = await req.json();

  track({
    event: "purchase",
    anonymous_id,
    user_id,
    properties: { amount_cents, currency }
  });

  await flush();   // important — function freezes after this returns
  return NextResponse.json({ ok: true });
}
```

For long-running route handlers (rare in serverless), `flushAt: 50` + a periodic `flush()` is fine.

## Server Actions

Same pattern. Init the Node SDK once at module scope; call `flush()` before the action returns.

```ts
"use server";

import { track, flush } from "@millimetric/track-node";

export async function createTeam(formData: FormData) {
  // ... your logic ...
  track({
    event: "team_created",
    user_id: session.userId,
    properties: { size: 1 }
  });
  await flush();
}
```

## Edge Runtime

`@millimetric/track-node` works on the edge — it uses global `fetch`, no Node built-ins. Just set `runtime`:

```ts
export const runtime = "edge";
```

## Pages Router

```tsx
// pages/_app.tsx
import { useEffect } from "react";
import { init } from "@millimetric/track";
import type { AppProps } from "next/app";
import { useRouter } from "next/router";
import { page } from "@millimetric/track";

export default function App({ Component, pageProps }: AppProps) {
  const router = useRouter();

  useEffect(() => {
    init({ key: process.env.NEXT_PUBLIC_AOA_KEY! });
  }, []);

  useEffect(() => {
    const onChange = (url: string) => page(url);
    router.events.on("routeChangeComplete", onChange);
    return () => router.events.off("routeChangeComplete", onChange);
  }, [router.events]);

  return <Component {...pageProps} />;
}
```

(With Pages Router you do need to wire pageviews manually — `next/router` events don't go through `history.pushState` in the same way.)

## Don't init in middleware / RSC

Middleware runs per-request — initialising the browser SDK there does nothing useful. RSC components run on the server — they don't have `localStorage`. Keep all SDK calls inside `"use client"` boundaries.

## Env vars cheat-sheet

```bash
# .env.local
NEXT_PUBLIC_AOA_KEY=pk_live_...
NEXT_PUBLIC_AOA_HOST=https://api.millimetric.ai
AOA_SK=sk_live_...                # server-only
AOA_HOST=https://api.millimetric.ai
```

`NEXT_PUBLIC_*` ships to the browser. The plain `AOA_SK` does not — keep your secret keys naked, no prefix.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.millimetric.ai/sdks/frameworks/nextjs.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
