payload-posthog

Client (Public Site)

posthog-js on your public Next.js frontend — visitor pageviews, autocapture, and consent gating.

The admin client is scoped to the Payload admin panel: it reads @payloadcms/ui config and identifies the logged-in admin. That's correct for /admin, but it can't be reused on your public Next.js frontend — outside the admin there's no @payloadcms/ui context, and it would tag anonymous visitors with the admin's distinct id.

For visitor-facing analytics, use the separate payload-posthog/react entry instead. It has no @payloadcms/ui dependency, reads a public key/host from props or NEXT_PUBLIC_POSTHOG_KEY / NEXT_PUBLIC_POSTHOG_HOST, and tracks anonymous visitors as themselves.

Setup

Drop the provider into your root layout:

app/layout.tsx
import { PostHogProvider } from 'payload-posthog/react'

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

Then provide the key and host through the environment:

.env
NEXT_PUBLIC_POSTHOG_KEY=phc_...
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

Or pass them as props (apiKey, host), which take precedence over the environment variables. If no key can be resolved, the provider renders its children and skips initialization (with a dev-only warning) rather than throwing.

Pageviews

Pageviews are captured on the initial load and on every App Router client-side navigation, automatically — the provider initializes posthog-js with capture_pageview: 'history_change', which patches the History API (pushState/replaceState) and listens for popstate. No manual usePathname tracker is required.

For a consent-management platform (c15t, cookie-consent, etc.), start opted out and flip capturing on once consent is granted. The declarative consent prop keeps posthog-js in sync — true opts in, false opts out, undefined leaves the current state untouched (e.g. the visitor hasn't chosen yet):

app/providers.tsx
'use client'
import { PostHogProvider } from 'payload-posthog/react'
import { useCookieConsent } from 'your-cmp'

export function Analytics({ children }: { children: React.ReactNode }) {
  const { hasConsent } = useCookieConsent()
  return (
    <PostHogProvider optOutCapturingByDefault consent={hasConsent}>
      {children}
    </PostHogProvider>
  )
}

optOutCapturingByDefault maps to posthog-js's opt_out_capturing_by_default, so nothing is captured until consent flips it on.

You can also drive consent imperatively, or reach the underlying instance, with the exported hooks:

'use client'
import { usePostHog, usePostHogConsent } from 'payload-posthog/react'

function ConsentBanner() {
  const { optIn, optOut } = usePostHogConsent()
  return (
    <>
      <button onClick={optIn}>Accept</button>
      <button onClick={optOut}>Reject</button>
    </>
  )
}

function SignupButton() {
  const posthog = usePostHog() // the posthog-js instance, or null before init
  return <button onClick={() => posthog?.capture('signup_clicked')}>Sign up</button>
}

Props

PropTypeDescription
apiKeystringProject API key. Falls back to NEXT_PUBLIC_POSTHOG_KEY.
hoststringIngestion host. Falls back to NEXT_PUBLIC_POSTHOG_HOST, then https://us.i.posthog.com.
optOutCapturingByDefaultbooleanStart opted out until consent is granted. Default false.
consentbooleanReactive CMP signal; syncs opt-in/opt-out on change.
optionsPartial<PostHogConfig>Passed verbatim to posthog.init(); overrides every default above.

PostHogProviderProps is exported from payload-posthog/types.

Using the reverse proxy

If you enabled the reverse proxy, point the provider's host at the same path so ingestion routes through your own domain instead of i.posthog.com:

<PostHogProvider host="/ingest">{children}</PostHogProvider>

Is it safe to expose the API key?

Yes — the same as the admin client. PostHog project API keys (phc_...) are write-only and public by design. personalApiKey is a server-only secret and is never used here.

On this page