June 27, 2025

User Identification in Next.js with Clerk and PostHog

Guide

I have recently been really enjoying using Clerk when building my side projects to get authentication completed in an easy but scalable way. Authentication isn't a differentiator for my projects, so an off-the-shelf solution is the best option for starting quickly and scaling. I have also been using PostHog as an all-in-one solution for metrics, event, error tracking, and feature flags. To get it all integrated together, I combined a few examples into a provider component that tracks user identification using Clerk. The code below is a dropping component to enable all the functionality required for tracking a simple project.

Adding User Identification

  1. Add the PostHogProvider.tsx component to your project.
"use client";

import { useAuth, useUser } from "@clerk/nextjs";
import { usePathname, useSearchParams } from "next/navigation";
import posthog from "posthog-js";
import { PostHogProvider as PHProvider, usePostHog } from "posthog-js/react";
import { Suspense, useEffect } from "react";

export function PostHogProvider({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
      api_host: "/ingest",
      ui_host: "https://us.posthog.com",
      capture_pageview: false, // We capture pageviews manually
      capture_pageleave: true, // Enable pageleave capture
      capture_exceptions: true, // This enables capturing exceptions using Error Tracking, set to false if you don't want this
      debug: process.env.NODE_ENV === "development",
    });
  }, []);

  return <PHProvider client={posthog}>{children}</PHProvider>;
}

function PostHogPageView() {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const posthogClient = usePostHog();

  const { isSignedIn, userId } = useAuth();
  const { user } = useUser();

  // Track pageviews
  useEffect(() => {
    if (pathname && posthogClient) {
      let url = window.origin + pathname;
      const search = searchParams.toString();
      if (search) {
        url += "?" + search;
      }
      posthogClient.capture("$pageview", { $current_url: url });
    }
  }, [pathname, searchParams, posthogClient]);

  // Handle user authentication and identification
  useEffect(() => {
    // Check the sign in status and user info,
    // and identify the user if they aren't already
    if (isSignedIn && userId && user && !posthogClient._isIdentified()) {
      // Identify the user
      posthogClient.identify(userId, {
        email: user.primaryEmailAddress?.emailAddress,
        username: user.username,
        name: user.fullName,
      });
    }

    // Reset the user if they sign out
    if (!isSignedIn && posthogClient._isIdentified()) {
      posthogClient.reset();
    }
  }, [posthogClient, isSignedIn, userId, user]);

  return null;
}

export function SuspendedPostHogPageView() {
  return (
    <Suspense fallback={null}>
      <PostHogPageView />
    </Suspense>
  );
}

PostHogProvider.tsx

  1. Wrap PostHogProvider around your layout file and add SuspendedPostHogPageView inside the PostHogProvider & Clerk Provider
    <PostHogProvider>
      <ClerkProvider>
        {children}
        <SuspendedPostHogPageView />
      </ClerkProvider>
    </PostHogProvider>