# SDK — Next.js (/docs/sdk/nextjs)



{/* AGENT-4-OUTPUT-PENDING: confirm package name (`@layers/nextjs` vs `@layers/client`) and exact `filesToCreate` list once Agent 4's nextjs install spec lands. */}

The Layers SDK works in Next.js apps with the standard `@layers/client` package — there is no Next.js-specific package today. This guide shows the App Router pattern (the default in Next.js 13+) and the Pages Router fallback for legacy projects.

## Install [#install]

```bash
npm i @layers/client
```

## App Router (Next.js 13+) [#app-router-nextjs-13]

The SDK runs **only in client components** — it depends on `window` for the event queue. Wrap the SDK init in a client provider that mounts at the root layout.

### `app/layers-provider.tsx` [#applayers-providertsx]

```tsx title="app/layers-provider.tsx"
'use client';

import { LayersClient } from '@layers/client';
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
import { usePathname } from 'next/navigation';

const LayersContext = createContext<LayersClient | null>(null);

export function LayersProvider({ children }: { children: ReactNode }) {
  const [client, setClient] = useState<LayersClient | null>(null);
  const pathname = usePathname();

  useEffect(() => {
    const c = new LayersClient({
      apiKey: 'unused-but-required-by-type',
      appId: process.env.NEXT_PUBLIC_LAYERS_APP_ID!,
      environment: process.env.NODE_ENV === 'production' ? 'production' : 'development',
    });
    c.init().then(() => setClient(c));
  }, []);

  // Auto-fire screen() on route changes — App Router uses `usePathname()`.
  useEffect(() => {
    if (client && pathname) client.screen(pathname).catch(() => {});
  }, [client, pathname]);

  return <LayersContext.Provider value={client}>{children}</LayersContext.Provider>;
}

export function useLayers() {
  return useContext(LayersContext);
}
```

### `app/layout.tsx` [#applayouttsx]

```tsx title="app/layout.tsx"
import { LayersProvider } from './layers-provider';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <LayersProvider>{children}</LayersProvider>
      </body>
    </html>
  );
}
```

### Track events from a client component [#track-events-from-a-client-component]

```tsx title="app/checkout/page.tsx"
'use client';
import { useLayers } from '../layers-provider';

export default function CheckoutPage() {
  const layers = useLayers();
  return (
    <button
      onClick={() => layers?.track('purchase_success', { revenue: 9.99, currency: 'USD' })}
    >
      Buy
    </button>
  );
}
```

## Pages Router (legacy) [#pages-router-legacy]

Wrap `_app.tsx` and use `next/router` for the route-change hook:

```tsx title="pages/_app.tsx"
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { LayersClient } from '@layers/client';

export default function App({ Component, pageProps }) {
  const [client, setClient] = useState<LayersClient | null>(null);
  const router = useRouter();

  useEffect(() => {
    const c = new LayersClient({
      apiKey: 'unused',
      appId: process.env.NEXT_PUBLIC_LAYERS_APP_ID!,
      environment: process.env.NODE_ENV === 'production' ? 'production' : 'development',
    });
    c.init().then(() => setClient(c));
  }, []);

  useEffect(() => {
    if (!client) return;
    const onRouteChange = (url: string) => client.screen(url).catch(() => {});
    router.events.on('routeChangeComplete', onRouteChange);
    return () => router.events.off('routeChangeComplete', onRouteChange);
  }, [client, router]);

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

## SSR caveats [#ssr-caveats]

* The SDK's event queue uses `localStorage` for persistence. **Never** initialize the client outside `useEffect` or before `'use client'` boundary — Server Components / Edge runtime do not have `window`.
* For server-side event firing (post-purchase webhooks, etc.), use [`POST /v1/events`](/docs/api/reference/events/forward) — the partner-API forwarding surface — or `@layers/node` running in a Node server route. Do not import `@layers/client` from `app/api/*` route handlers.
* The `next/script` strategy is `afterInteractive` by default — fine for Layers, since the SDK initializes on a `useEffect` after hydration. If you need pre-hydration tracking (rare), use `beforeInteractive` and fire a manual `track()` once the client is ready.

## Environment variables [#environment-variables]

| Var                                  | Purpose                                                                                    | Build-time?                                                                           |
| ------------------------------------ | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |
| `NEXT_PUBLIC_LAYERS_APP_ID`          | The SDK app id from [`POST /sdk-apps`](/docs/api/reference/sdk-apps/create-sdk-app).       | **Yes — must be a build-arg.** Next.js inlines `NEXT_PUBLIC_*` at build, not runtime. |
| `NEXT_PUBLIC_LAYERS_INGEST_ENDPOINT` | Override `https://in.layers.com/events`. Use for [custom domain](/docs/sdk/custom-domain). | Yes.                                                                                  |

## Verifying the install [#verifying-the-install]

After deploying, hit the active probe to confirm tracking is healthy:

```bash
curl -X POST https://api.layers.com/v1/projects/$PROJECT_ID/sdk-apps/$APP_ID/verify-tracking \
  -H "Authorization: Bearer $LAYERS_API_KEY"
```

Returns `{ status: "healthy" | "missing_events" | "schema_drift" | "no_install", … }` plus per-check breakdown. See [`verify-tracking`](/docs/api/reference/sdk-apps/verify-tracking) and [SDK health](/docs/api/concepts/sdk-health).

## Common pitfalls [#common-pitfalls]

* **`window is not defined`** — you imported `LayersClient` outside a client component. Move to a `'use client'` file or wrap in `useEffect`.
* **Events fire but Meta sees zero** — your `NEXT_PUBLIC_LAYERS_APP_ID` is a build-arg. If you set it after the build, redeploy.
* **Screen events double-fire on Strict Mode in dev** — React 18 Strict Mode mounts effects twice in development. The SDK dedupes the duplicate `screen()` server-side; in production it's a no-op.

## See also [#see-also]

* [SDK overview](/docs/sdk/overview)
* [Standard events](/docs/sdk/standard-events)
* [Custom events](/docs/sdk/custom-events)
* [Custom domain](/docs/sdk/custom-domain)
* [Server-side forwarding (`POST /v1/events`)](/docs/api/reference/events/forward)
* [Verify tracking](/docs/api/reference/sdk-apps/verify-tracking)
