PostHog features flags behind a cookie banner (without breaking GDPR)
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.
__ph_opt_in_out_* storage of required consent
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:
- keep the purpose restricted (only the state of consent),
- list it in your cookie policy / inventory as essential,
- Do not use it for profiling or advertising purposes.
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.
Wire consent → identify → flags
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
- Create a boolean features whose key corresponds to your code (for example
example-beta-panel). - 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%. - 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).
- Run the stream: upload without analytics → gated hidden UI; accept analytics →
identifyrun → 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:
-
getprobo/probo/examples/cookie-banner-react - Wiring:
src/lib/posthog.ts - Env:
PUBLIC_POSTHOG_FEATURE_FLAG,PUBLIC_POSTHOG_DEMO_DISTINCT_ID(see.env.example)
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 isFeatureEnabledFor 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.