PostHog analytics is an open-source product analytics platform that gives you event tracking, session recording, feature flags, A/B testing, and funnels — all in one tool. For a SaaS product, it is one of the most practical choices available: self-hostable if you need it, hosted in the EU if you need GDPR compliance, and deeply integrated with the React ecosystem.
This post covers how PostHog analytics is set up in Plainform — a production-ready Next.js SaaS starter. You will see the actual initialization code, how events are captured, how users are identified, and how the EU reverse proxy works to keep analytics data away from ad blockers and compliant with GDPR.
Why PostHog for a SaaS Product
Most analytics tools are built for marketing teams. PostHog analytics is built for product teams. The events you track are the same ones you use to build funnels, replay sessions, run A/B tests, and control feature flags. Everything lives in one place, with one SDK.
For a Next.js SaaS app specifically, PostHog has first-class React support, autocapture for page views and clicks, and a clean TypeScript API. The EU-hosted cloud option (eu.i.posthog.com) makes GDPR compliance straightforward without self-hosting.
The alternative — wiring up Mixpanel, a separate session recorder, and a feature flag service — costs more, requires more integrations, and gives you fragmented data across three dashboards.
Installing PostHog
Install the main library and the React bindings:
npm install posthog-js @posthog/reactYou will need two environment variables:
NEXT_PUBLIC_POSTHOG_KEY=phc_...
NEXT_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.comBoth are public client-side variables — they are safe to expose in the browser. The API key identifies your PostHog project. The host points to the EU cloud if you need EU data residency, or the US cloud (https://app.posthog.com) if you do not.
Initializing PostHog in Next.js
Using instrumentation-client.ts
The cleanest way to initialize PostHog analytics in a Next.js App Router project is through instrumentation-client.ts. This file runs once on the client side when the app loads, before any page renders.
Here is how Plainform does it:
// instrumentation-client.ts
import posthog from 'posthog-js';
import { env } from '@/env';
posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: '/relay-sdK3',
defaults: '2025-05-24',
cookieless_mode: 'on_reject',
});A few things worth explaining here:
api_host: '/relay-sdK3'points to a local Next.js rewrite path rather than directly to PostHog's servers. This is the reverse proxy setup covered below.defaults: '2025-05-24'pins PostHog's default configuration to a specific date, so behaviour does not change unexpectedly when PostHog updates their defaults.cookieless_mode: 'on_reject'runs PostHog without cookies until the user accepts the cookie banner. This is how GDPR compliance is handled without blocking all analytics by default.
Why not a Provider component
Some PostHog guides recommend wrapping your app in a PostHogProvider component in layout.tsx. That works, but it requires adding a Client Component wrapper to your root layout, which means the entire layout tree must be client-rendered.
The instrumentation-client.ts approach avoids that. The file runs at the framework level, the root layout stays a Server Component, and PostHog is still available everywhere in the app through the posthog-js singleton.
Setting Up the EU Reverse Proxy
PostHog analytics requests are frequently blocked by ad blockers and privacy extensions. The standard fix is to route PostHog traffic through your own domain — a reverse proxy — so the requests look like first-party traffic.
Configuring Next.js rewrites
In next.config.mjs, add rewrites that forward PostHog paths to the EU cloud:
async rewrites() {
return [
{
source: '/relay-sdK3/static/:path*',
destination: 'https://eu-assets.i.posthog.com/static/:path*',
},
{
source: '/relay-sdK3/:path*',
destination: 'https://eu.i.posthog.com/:path*',
},
];
},
// Required for PostHog trailing slash API requests
skipTrailingSlashRedirect: true,The path /relay-sdK3 is deliberately non-obvious — using something like /posthog or /analytics is trivially easy for blockers to detect and block. An opaque path like this is much harder to fingerprint.
The skipTrailingSlashRedirect: true option is required because PostHog makes some requests with trailing slashes, and Next.js's default behaviour strips those before the rewrite matches.
Why EU hosting matters
If your users are in Europe, routing data to eu.i.posthog.com means their analytics data is stored in the EU and never leaves it. That is a meaningful GDPR consideration, and it is why Plainform uses the EU endpoint rather than the default US one.
Tracking Custom Events
Page views are captured automatically by PostHog analytics. For everything else — button clicks, form submissions, feature usage, purchases — you use posthog.capture.
In Client Components
The usePostHog hook gives you access to the PostHog instance inside any Client Component:
'use client';
import { usePostHog } from 'posthog-js/react';
export function PricingButton() {
const posthog = usePostHog();
const handleClick = () => {
posthog.capture('pricing_cta_clicked', {
plan: 'pro',
page: window.location.pathname,
});
};
return <button onClick={handleClick}>Get Started</button>;
}The first argument to capture is the event name — use a consistent naming convention across your app (noun_verb works well). The second argument is a properties object. You can pass anything serializable: strings, numbers, booleans, nested objects.
Common events to track in a SaaS product
// Feature usage
posthog.capture('feature_used', { feature_name: 'export_csv' });
// Form submissions
posthog.capture('form_submitted', { form_name: 'contact', success: true });
// Upgrade intent
posthog.capture('upgrade_clicked', { current_plan: 'free', target_plan: 'pro' });
// Purchase completed
posthog.capture('purchase_completed', { amount: 99, plan: 'pro', currency: 'usd' });
// Error encountered
posthog.capture('error_encountered', { error_type: 'payment_failed', page: '/checkout' });Tracking docs feedback
Plainform uses PostHog analytics directly in the docs feedback component to capture how users rate documentation pages. It also calls posthog.identify before capturing the event, so the feedback is tied to the logged-in user:
import posthog from 'posthog-js';
function onSubmit() {
const feedback = {
opinion, // 'good' | 'bad'
message,
url,
};
posthog.identify(user?.primaryEmailAddress?.emailAddress || undefined);
posthog.capture('on_rate_docs', feedback);
}This means every piece of docs feedback in PostHog shows which user submitted it, what page they were on, and what they said — without building any custom backend infrastructure.
Identifying Users
Anonymous events are useful for measuring traffic. But in a SaaS product, you usually want to know which specific user did what, and what plan they are on.
Calling posthog.identify
After a user signs in, call posthog.identify with their unique ID and any traits you want attached to them in PostHog:
'use client';
import { usePostHog } from 'posthog-js/react';
import { useUser } from '@clerk/nextjs';
import { useEffect } from 'react';
export function UserIdentifier() {
const posthog = usePostHog();
const { user } = useUser();
useEffect(() => {
if (user) {
posthog.identify(user.id, {
email: user.emailAddresses[0]?.emailAddress,
name: user.fullName,
created_at: user.createdAt,
});
}
}, [user, posthog]);
return null;
}Once identified, all subsequent events from that browser session are linked to that user in PostHog. You can look up a specific user in the People section and see their full event history.
Resetting identity on sign-out
When a user signs out, reset the PostHog identity so the next user on the same device starts a fresh anonymous session:
import posthog from 'posthog-js';
function handleSignOut() {
posthog.reset();
// then proceed with sign-out
}Without this, if two different users share a device (a shared computer, for example), their events would be merged into one profile.
GDPR and Cookieless Mode
The cookieless_mode: 'on_reject' option in the initialization config tells PostHog analytics to avoid setting cookies unless the user has explicitly accepted. Until consent is given, PostHog operates without persistent identifiers.
This is the behaviour Plainform's cookie banner is built around. When a user accepts cookies, PostHog switches to full tracking mode with a persistent anonymous ID. When they reject, PostHog keeps running but stays cookieless — session data is not persisted across page loads.
What cookieless mode means in practice
In cookieless mode:
- No cookies or localStorage entries are written for tracking
- Sessions are not stitched together across page loads
- Events are still captured and sent, but without a persistent user identifier
- User identification via
posthog.identifystill works within a single session
For most SaaS products where you care most about signed-in user behaviour, this is a reasonable trade-off. The analytics you care about — what your paying users do — is captured even in cookieless mode, because you identify them when they sign in.
Viewing Your Data in PostHog
Once events are flowing, the PostHog dashboard gives you several ways to make sense of them.
Insights
Insights are queries over your event data. The most common ones for a SaaS product:
- Trends: How many times was
purchase_completedfired this week? How is that trending over time? - Funnels: Of users who viewed the pricing page, what percentage clicked the CTA, and what percentage completed checkout?
- Retention: Of users who signed up in a given week, what percentage were still active 30 days later?
Session replays
PostHog can record sessions and let you replay exactly what a user did. This is invaluable for understanding confusing behaviour in your event data — if you see users dropping off at a specific step, you can watch a replay to see why.
Feature flags
Feature flags in PostHog analytics let you ship features to a percentage of users, to specific cohorts, or by user property. Because flags are evaluated on the same platform as your event data, you can immediately see how a flag change affects your metrics without switching tools.
How Plainform Uses PostHog
In Plainform, PostHog analytics is initialized once in instrumentation-client.ts, runs through a reverse proxy on /relay-sdK3, and operates in cookieless mode until the user accepts the cookie banner. User identification happens automatically after sign-in via the Clerk integration.
The only custom event captured by default is docs feedback via posthog.capture('on_rate_docs', feedback). Everything else — page views, clicks, navigation — is captured automatically.
When you build your product on Plainform, this infrastructure is already in place. You add posthog.capture calls wherever you want visibility into user behaviour, and the data flows into PostHog without any additional setup.
Summary
PostHog analytics in a Next.js SaaS app comes down to a few core pieces:
- Initialize with
instrumentation-client.tsto keep the root layout as a Server Component - Use a reverse proxy via Next.js rewrites to avoid ad blockers and keep EU data in the EU
- Set
cookieless_mode: 'on_reject'to handle GDPR without blocking all analytics - Use
posthog.capturewith consistent event names and meaningful properties - Call
posthog.identifyafter sign-in to link events to real users - Reset with
posthog.reset()on sign-out to avoid cross-user contamination
Get this right once and you have a solid foundation for understanding what your users actually do — not just how many of them showed up.
