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.
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:
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:
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
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.