Skip to main content

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.

App Router

Use next/script with strategy="afterInteractive" in your root layout. This loads the snippet after the page is interactive without blocking hydration.
app/layout.tsx
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:
.env.local
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.
components/ZenstepIdentify.tsx
"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:
app/(dashboard)/layout.tsx
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:
pages/_app.tsx
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

types/global.d.ts
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.
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.