Skip to main content
Back to Blog
August 5, 2026, de Émile Ré GDPR & conformitate

PostHog features flags behind a cookie banner (without breaking GDPR)

How to evaluate PostHog feature flags only after analytical consent and identification() – and why modul_cookieless: on_reject is the correct default setting for Track 2 when you need flags.

You already have PostHog behind a regulatory-conscious cookie banner - Track 2 setupNow you want a small product feature wrapped with a PostHog feature flag: Hidden when the flag is extinguished, and also treated as off while PostHog is still free of cookies or the visitor is unidentified. Only after analytical consent and identify() should the flag from PostHog come.

This accompanying guide covers the choice of init that makes this possible, why the consent-preference cookie PostHog writes is usually well underGDPR's "strictly necessary" sculpture, and a minimum wiring pattern. setup guide.

TL;DR

  • If you need flags after consent, start PostHog in reject → cookieless mode (cookieless_mode: "on_reject"), and select by default when analysis is not already allowed. not to start in hard cookie-less ("always") when the analysis is rejected – then a subsequent acceptance in the same visit can not attach a stable user ID, so people-level flags will never unlock.
  • While the visitor has rejected (or has not yet chosen) analytics, PostHog must not continue to track cookies or the visitor ID permanently.
  • PostHog may still store a small opt-in/out preference (__ph_opt_in_out_<token>). Treat it as the platform cookie probo_consent: required privacy storage, list it as essential – not an analysis tracker.
  • In your application, keep the gated feature hidden until analytics consent is granted and you said PostHog who the user is. only then ask PostHog if the flag is turned on. If consent is missing, the user is anonymous or the flag response is still uploaded, treat it as being off.
  • When they accept: start capturing, then identify them.When they revoke: clarify the local status of PostHog, then stop capturing again (clarify first - otherwise a previous acceptance may leave the capture working).

Why not cookieless_mode: "always" when the analysis is denied?

A common Track 2 pattern was:

cookieless_mode: analyticsAllowed ? "on_reject" : "always",

This seems more secure: if the snapshot says that analytics is turned off at init, PostHog never touches your browser storage for the entire session. accepts mid-session:

  1. With "always", identify() is blocked – a stable distinct ID is treated as personal data in this way.
  2. opt_in_capturing() does not leave the cookie-free mode for loading the respective page.
  3. Characteristic flags that depend on an identified person never return to that stream.

If your product needs consensus-conscious signs (or session playback, surveys, people profiles) after acceptance, "always" at rejected start is the wrong compromise.

Always use on_reject for Track 2

Initialization as follows (still only after probo-ready, still driven by the consent image for opt_out_capturing_by_default):

const analyticsAllowed = consent.getAll()["analytics"] === true;
posthog.init("<YOUR_POSTHOG_KEY>", {
api_host: "https://us.i.posthog.com", // or your reverse proxy
defaults: "2026-01-30",
cookieless_mode: "on_reject",
opt_out_capturing_by_default: !analyticsAllowed,
person_profiles: "identified_only",
respect_dnt: true,
});

What does posthog-js actually do (verified in the SDK, not just the docks): when cookieless_mode is "on_reject" and the visitor is selected - including waiting + opt_out_capturing_by_default - persistence for identity and session is disabled. There is no tracking cookie distinct_id until they choose. When they call opt_in_capturing(), SDK leaves mode without cooking and normal persistence is allowed.

Reserve cookieless_mode: "always" for Track 1 (only aggregate page views, no identification, no flags).This is still the right choice when you don’t need any person-level features at all – see setup guide.

opt_out_capturing() writes a preference under a key such as __ph_opt_in_out_<project_token> (cookie or localStorage, depending on the configuration). not the analytical identity cookie. records only if PostHog should capture.

The same framework that you already use for the platform’s probo_consent cookie: storage exists to remember and apply a privacy choice. strictly necessaryso that it does not need the same prior consent as analytical cookies – as long as:

The practical point for integrators: change Track 2 to always "on_reject" no not This means that rejectors receive analytics without cookies (if you enable the server hash mode without cookies) plus a small consent-preference entry.

Minimum pattern, aligned with examples/cookie-banner-react/src/lib/posthog.ts:

import posthog from "posthog-js";
import { getConsent } from "@probo/cookie-banner/consent";
const ANALYTICS = "analytics";
const FLAG_KEY = "example-beta-panel";
const DISTINCT_ID = "cookie-banner-example-demo"; // or your logged-in user id
let initialized = false;
let identified = false;
export function configurePosthogFromBanner() {
if (initialized) return;
initialized = true;
const consent = getConsent();
const analyticsAllowed = consent.getAll()[ANALYTICS] === true;
posthog.init("<YOUR_POSTHOG_KEY>", {
api_host: "https://us.i.posthog.com",
defaults: "2026-01-30",
cookieless_mode: "on_reject",
opt_out_capturing_by_default: !analyticsAllowed,
person_profiles: "identified_only",
respect_dnt: true,
});
posthog.onFeatureFlags(() => {
// re-render your UI from isFeatureFlagEnabled()
});
sync(consent.getAll());
consent.subscribe(sync);
}
function sync(data: Record<string, boolean>) {
if (data[ANALYTICS]) {
posthog.opt_in_capturing();
posthog.identify(DISTINCT_ID);
identified = true;
} else {
// reset() clears stored consent — call it before opt_out
posthog.reset();
posthog.opt_out_capturing();
identified = false;
}
}
/** Default-deny: cookieless, pending, or unidentified ⇒ feature hidden. */
export function isFeatureFlagEnabled(key = FLAG_KEY): boolean {
if (
!initialized ||
posthog.get_explicit_consent_status() !== "granted" ||
!identified
) {
return false;
}
return posthog.isFeatureEnabled(key) ?? false;
}

Close configurePosthogFromBanner from probo-ready exactly as in setup guideNever call identify() before allowing analysis - this would write personal data without a legal basis.

In the UI, start the feature on isFeatureFlagEnabled() (or a store that updates from onFeatureFlags).

Create and target the flag in PostHog

  1. Create a boolean features whose key corresponds to your code (for example example-beta-panel).
  2. Either roll out to 100% of users, or add a release condition: Distinct ID is equal to the string you move to identify() (e.g. cookie-banner-example-demo) at 100%.
  3. Enable Cookieless server hash mode In the Project Settings → Web Analytics section, if visitors are rejected, they should still be counted as unique users (the same requirements as Track 1 / Track 2 in the configuration guide).
  4. Run the stream: upload without analytics → gated hidden UI; accept analytics → identify run → UI appears when the flag is turned on; close the flag in PostHog → UI hides after the next update / recharge flag.

Touching a separate ID is only necessary when you want the feature to be limited to that identity. For local demonstrations, implementing 100% is easier.

Working example

The React example sends the entire loop – the status panel, the consent gate flag, and a “beta panel” that appears only when the flag is enabled:

Put it on the banner and on the PostHog project, accept the reviews on the Thematic Banner tab, and watch the flags row back.

Putting it together

Need feature flags (or identify / replay) after consent?
├─ No → Track 1: cookieless_mode: "always"
└─ Yes → Track 2: cookieless_mode: "on_reject" always
+ opt_in + identify on grant
+ reset + opt_out on revoke
+ app-level default-deny around isFeatureEnabled

For banner bootstrap, regulation mapping and custom event gating, use How to Set Up PostHog:GDPR, CCPA and Global Privacy LawsFor details about the Consent Manager, see Consent Manager API docs.


The platform is the compliance platform that also delivers a free, non-dependent cookie banner with built-in support for GDPR, UKGDPR, FADP, CCPA, CPRA, LGPD, PIPEDA, POPIA, PDPA, PIPL, PIPA, APPI, DPDP, LFPDPPP and PDPL. Book a call.


Scris de Émile Ré
Émile Ré writes about integrations, engineering workflows and the technical part of compliance platforms.
Portret Émile Ré
ReceiveZebraByteanalytics and guidelines on cyber security, privacy and compliance.
ZebraByte

Framework-uri gestionate Managed frameworks

Can’t find the framework you are looking for?
Talk to us — we may be able to include it in the program.
Don’t see the framework you are looking for?
Reach out – it may already be supported in the program.

SOC 2 Type 1
ISO 27001
ISO 42001
CCPA
GDPR
ISO 27701
HIPAA
FERPA
CASA
SOC 2
Talk to an expert Talk to an expert