Next.js: @posthog/next

Contents

Pre-release

@posthog/next is currently in pre-release. The API may change before the stable release. For production apps, see the standard Next.js setup guide.

@posthog/next is a unified package for integrating PostHog into your Next.js app. It provides a simplified interface that handles common patterns out of the box:

  • Synchronized analytics identity across client and server, so both sides share the same user automatically
  • Trusted server-side identity resolution from your authentication session for server events and feature flag evaluations
  • Server-side feature flag bootstrapping so hooks return real values immediately with no flicker
  • Eager client initialization ensures PostHog is always available when your components render
  • Built-in API proxy that routes SDK traffic through your domain to avoid ad blockers
  • Automatic browser exception capture and event flushing with no manual shutdown() or flush() calls needed

This guide covers both the App Router and Pages Router. You'll need a PostHog instance (Cloud or self-hosted) and a Next.js 13.0.0+ application.

  1. Install @posthog/next

    Required
    npm install --save @posthog/next
  2. Add your environment variables

    Required

    Add these to your .env.local file and to your hosting provider (e.g. Vercel, Netlify, AWS). You can find your project token in your project settings.

    .env.local
    NEXT_PUBLIC_POSTHOG_KEY=<ph_project_api_key>
    NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

    NEXT_PUBLIC_POSTHOG_HOST is optional and defaults to https://us.i.posthog.com.

  3. Add the middleware

    Recommended

    The middleware seeds an identity cookie on first visit so client and server share the same user, and optionally proxies API requests through your domain.

    middleware.ts
    import { postHogMiddleware } from '@posthog/next'
    export default postHogMiddleware({ proxy: true })
    export const config = {
    matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
    }

    Setting proxy: true routes PostHog API calls through your domain at /ingest, which helps avoid ad blockers. You can customize the path prefix by passing proxy: { pathPrefix: '/analytics' } instead.

    Note: Middleware requires a server runtime and is not available with output: 'export' (fully static sites). If you can't use middleware, you can set up a reverse proxy separately to route traffic through your domain.

  4. Wrap your app with PostHogProvider

    Required

    Add PostHogProvider and PostHogPageView to your root layout:

    app/layout.tsx
    import { PostHogProvider, PostHogPageView } from '@posthog/next'
    export default function RootLayout({ children }: { children: React.ReactNode }) {
    return (
    <html lang="en">
    <body>
    <PostHogProvider clientOptions={{ api_host: '/ingest' }} bootstrapFlags>
    <PostHogPageView />
    {children}
    </PostHogProvider>
    </body>
    </html>
    )
    }

    PostHogProvider is a React Server Component. When bootstrapFlags is enabled, it evaluates feature flags server-side and passes the results to the client so hooks return real values immediately.

    Note: Enabling bootstrapFlags opts the route into dynamic rendering (incompatible with static generation / ISR). Without it, the provider does not call any dynamic Next.js APIs and is safe for static pages.

  5. Verify events are captured

    Checkpoint
    Confirm that you can see events in your PostHog project

    Visit your app and click around. Within a minute or two, you should see $pageview and autocaptured events (like clicks) appear in the activity tab.

  6. Access PostHog in client components

    Required

    All hooks must be used in client components ('use client').

    Capture events:

    TSX
    'use client'
    import { usePostHog } from '@posthog/next'
    export function TrackButton() {
    const posthog = usePostHog()
    return <button onClick={() => posthog.capture('button_clicked')}>Track</button>
    }

    Feature Flags:

    TSX
    'use client'
    import { useFeatureFlag } from '@posthog/next'
    export function MyComponent() {
    const flag = useFeatureFlag('new-feature')
    return flag?.enabled ? <NewFeature /> : <OldFeature />
    }

    Conditional rendering with PostHogFeature:

    TSX
    'use client'
    import { PostHogFeature } from '@posthog/next'
    export function NewBanner() {
    return (
    <PostHogFeature flag="show-banner" match={true}>
    <div>New feature available!</div>
    </PostHogFeature>
    )
    }

    You can also read the full posthog-js documentation for all the usable functions.

  7. Identify your user

    Recommended

    Call posthog.identify() on the client after login to link events to a known user. This connects event captures, session replays, LLM traces, and Error Tracking to the same person across client and server.

    TSX
    'use client'
    import { usePostHog } from '@posthog/next'
    export function LoginButton() {
    const posthog = usePostHog()
    const handleLogin = async () => {
    const user = await signIn()
    posthog.identify(user.id, {
    email: user.email,
    name: user.name,
    })
    }
    return <button onClick={handleLogin}>Log in</button>
    }

    See our guide on identifying users for more details.

    For server-side events and feature flag evaluations, PostHog can use the client-provided identity cookie and tracing headers to link analytics across the request. These values come from the client. When you need a strict guarantee that analytics uses the identity from your authenticated session, provide an authoritative distinct ID directly when capturing an event or evaluating feature flags, or configure getDistinctId in createPostHog() to resolve it from your server session by default.

  8. Use PostHog on the server

    Recommended

    Create a shared PostHog module with createPostHog(). Use the returned getPostHog to evaluate feature flags in Server Components and capture events from request or action handlers.

    lib/posthog.ts
    import 'server-only'
    import { createPostHog } from '@posthog/next'
    import { auth } from '@/auth'
    export const { getPostHog } = createPostHog({
    // Optional: resolve a trusted distinct ID from your auth session.
    // Return null or undefined to fall back to the client identity.
    getDistinctId: async () => (await auth())?.user?.id,
    })
    app/dashboard/page.tsx
    import { getPostHog } from '@/lib/posthog'
    export default async function DashboardPage() {
    const posthog = await getPostHog()
    const flags = await posthog.getAllFlags()
    return (
    <div>{flags['new-dashboard'] ? <NewDashboard /> : <OldDashboard />}</div>
    )
    }

    Without getDistinctId, PostHog can use the identity cookie and tracing headers supplied by the client as request context. When you need a strict guarantee that analytics uses the identity from your authenticated session, pass an authoritative distinct ID directly when capturing events or evaluating feature flags. When the resolver returns an ID, PostHog uses it as the request default ahead of client-provided identity, while an ID passed directly to a capture or flag method still takes precedence. Session and device linkage can remain connected to the browser. The resolver runs at most once per request and is not called for users who opted out of capture.

    Note: getPostHog() calls cookies() and headers() internally, which opts the route into dynamic rendering. Pages using it cannot be statically generated. Keep import 'server-only' in the shared module so accidental imports from Client Components fail at build time.

    Event flushing: On Vercel, @posthog/next auto-detects waitUntil from @vercel/functions so events are flushed after the response is sent without blocking it. No manual shutdown() call is needed. In other environments, pass a custom waitUntil in the server client options with createPostHog({ options: { waitUntil } }), or events will be batched and flushed normally.

    Note: Avoid capturing page views or other navigation events while rendering a Server Component or running getServerSideProps. Rendering doesn't prove that a navigation completed. The App Router can execute render code while prefetching, and Next.js can retry renders. Use PostHogPageView for page views, and capture server-side events from handlers invoked for the action you want to measure.

    Dynamic rendering keeps personalized flag decisions out of Next.js's shared Full Route Cache. The App Router can still reuse a React Server Component payload from its client Router Cache. After an authentication or identity change, use router.refresh() when the current route's server-rendered flag decisions must be evaluated again.

  9. Capture server-side request errors

    Recommended

    @posthog/next enables automatic browser exception capture by default. On Next.js 15 and later, capture server-side request errors by exporting onRequestError from your root instrumentation.ts file:

    instrumentation.ts
    export { onRequestError } from '@posthog/next'

    Next.js calls this hook when a Server Component, Route Handler, or Server Action throws during a request. When available, the PostHog identity cookie links the exception to the same user and session. Import this hook from @posthog/next for both App Router and Pages Router applications.

    Use createOnRequestError() when you need to add properties, filter errors, or customize the server client. You can disable automatic browser exception capture with clientOptions={{ capture_exceptions: false }}.

  10. Configure advanced options

    Optional

    Bootstrap feature flags

    In the App Router, set bootstrapFlags to evaluate feature flags on the server and pass the results to the client, which avoids flag flicker on first render:

    TSX
    <PostHogProvider bootstrapFlags>{children}</PostHogProvider>

    You can also limit the flags evaluated and provide properties used for evaluation:

    TSX
    <PostHogProvider
    bootstrapFlags={{
    flags: ['new-ui', 'pricing-v2'],
    groups: { company: 'posthog' },
    personProperties: { plan: 'enterprise' },
    groupProperties: {
    company: { industry: 'tech' },
    },
    }}
    >
    {children}
    </PostHogProvider>

    Enabling bootstrapFlags calls cookies() internally, which opts every page route rendered through that provider into dynamic rendering. If the provider is in your root layout, this includes every page in your app. To preserve static rendering elsewhere, enable bootstrapFlags in the narrowest layout that needs server-evaluated flags. In the Pages Router, evaluate feature flags inside getServerSideProps and pass the result through clientOptions.bootstrap as shown in the server setup above.

    Tracing headers

    @posthog/next adds X-POSTHOG-DISTINCT-ID and X-POSTHOG-SESSION-ID headers to same-origin browser requests by default. This connects server events and feature flag evaluations to the browser user and session.

    Pass clientOptions.tracing_headers to customize the hostnames that receive these headers, or use an empty array to disable them:

    TSX
    <PostHogProvider clientOptions={{ tracing_headers: [] }}>{children}</PostHogProvider>

    If your app requires consent before setting cookies, disable anonymous cookie seeding in middleware:

    middleware.ts
    export default postHogMiddleware({
    proxy: true,
    seedAnonymousCookie: false,
    })

    Then initialize the client with capture disabled until you opt in:

    TSX
    <PostHogProvider clientOptions={{ opt_out_capturing_by_default: true }}>{children}</PostHogProvider>

    API proxy options

    Pass an object to proxy to customize the ingest path or host:

    middleware.ts
    export default postHogMiddleware({
    proxy: {
    pathPrefix: '/analytics',
    host: 'https://eu.i.posthog.com',
    },
    })

    When you change the proxy path, use the same path in clientOptions.api_host:

    TSX
    <PostHogProvider clientOptions={{ api_host: '/analytics' }}>{children}</PostHogProvider>

Community questions

Was this page useful?