> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getzenstep.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Next.js

> Install Zenstep in a Next.js application — App Router and Pages Router patterns.

## App Router

Use `next/script` with `strategy="afterInteractive"` in your root layout. This loads the snippet after the page is interactive without blocking hydration.

```tsx app/layout.tsx theme={null}
import Script from "next/script";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script
          src="https://cdn.getzenstep.com/v1/snippet.js"
          data-zenstep={process.env.NEXT_PUBLIC_ZENSTEP_KEY}
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}
```

Add to `.env.local`:

```bash .env.local theme={null}
NEXT_PUBLIC_ZENSTEP_KEY=your_snippet_key_here
```

***

## Calling identify (App Router)

Create a client component that reads the authenticated user and calls `identify()`. Server Components cannot access `window` — the identify call must happen in a `"use client"` component.

```tsx components/ZenstepIdentify.tsx theme={null}
"use client";

import { useEffect } from "react";

interface Props {
  userId: string;
  email: string;
  plan: string;
}

export function ZenstepIdentify({ userId, email, plan }: Props) {
  useEffect(() => {
    window.zenstep?.identify(userId, { email, plan });
  }, [userId]);

  return null;
}
```

Use it in any Server Component by passing user data as props:

```tsx app/(dashboard)/layout.tsx theme={null}
import { createClient } from "@/lib/supabase/server";
import { ZenstepIdentify } from "@/components/ZenstepIdentify";

export default async function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const supabase = await createClient();
  const {
    data: { user },
  } = await supabase.auth.getUser();

  return (
    <>
      {user && (
        <ZenstepIdentify
          userId={user.id}
          email={user.email ?? ""}
          plan={user.user_metadata?.plan ?? "free"}
        />
      )}
      {children}
    </>
  );
}
```

***

## Pages Router

Add the snippet in `_app.tsx`:

```tsx pages/_app.tsx theme={null}
import Script from "next/script";
import type { AppProps } from "next/app";
import { useEffect } from "react";
import { useRouter } from "next/router";

export default function App({ Component, pageProps }: AppProps) {
  const { user } = pageProps; // however you pass auth state

  useEffect(() => {
    if (user?.id) {
      window.zenstep?.identify(user.id, {
        email: user.email,
        plan: user.plan,
      });
    }
  }, [user?.id]);

  return (
    <>
      <Component {...pageProps} />
      <Script
        src="https://cdn.getzenstep.com/v1/snippet.js"
        data-zenstep={process.env.NEXT_PUBLIC_ZENSTEP_KEY}
        strategy="afterInteractive"
      />
    </>
  );
}
```

***

## TypeScript types

```ts types/global.d.ts theme={null}
interface ZenstepAPI {
  identify: (
    userId: string,
    attributes?: Record<string, string | number | boolean>,
  ) => void;
  track: (event: string, data?: Record<string, unknown>) => void;
}

declare global {
  interface Window {
    zenstep?: ZenstepAPI;
  }
}

export {};
```

***

## Middleware considerations

If you use Next.js middleware for auth redirects, the snippet is client-side only and is not affected by middleware. Zenstep flows are evaluated in the browser after the page loads.

<Note>
  When using Next.js Middleware edge runtime for session management, ensure
  `NEXT_PUBLIC_ZENSTEP_KEY` is set on the client-side environment (prefixed with
  `NEXT_PUBLIC_`). Middleware cannot read `NEXT_PUBLIC_` variables at the edge —
  use the `process.env` reference only in client components.
</Note>
