What PostHog sees, what it doesn’t, and how a user can opt out.
PostHog collects anonymous behavioural events. It
does not see PII. The application database (Postgres / SQLite) is the
only place that holds any volunteered email or customer data. The two
stores are deliberately decoupled — a SQL drop on
demo_sessions removes every captured email; PostHog still
has the (anonymous) funnel.
✅ Event names (homepage_viewed,
cta_clicked, …) ✅ Anonymous user identifiers (UUID
generated by posthog-js, held in persistence: 'memory' —
tab-scoped, no cookie, no localStorage, regenerates on page reload.
Switched to memory persistence on 2026-05-18 so the privacy policy can
truthfully claim “essential cookies only” and no PECR consent banner is
required. Trade-off: uniques are slightly inflated because a page reload
counts as a new anonymous visitor.) ✅ Non-PII properties
(e.g. cta_name, archetype_id,
step_name, transaction_count) ✅ Page paths
(/demo, /demo/abc-123/circumstances) ✅ Click
coordinates / element selectors (only if autocapture is on — currently
off in instrumentation-client.ts) ✅
Time-on-page, scroll depth bucketed to 25/50/75/100% ✅ Browser / device
type / OS
❌ Full email addresses — we send email_domain
(“gmail.com”) only, computed via emailDomain() (front) /
email_domain() (back). ❌ Names (first, last, full) ❌
Phone numbers ❌ Postal addresses or postcodes ❌ Financial amounts
(income, transaction value, balance) ❌ Transaction descriptions /
merchant strings of the customer’s bank ❌ IP addresses — the SDK is
configured with ip: false
Three layers of stripping:
analytics.track() (and frontend track())
refuses to forward properties whose names look like PII.instrumentation-client.ts
sets a sanitize_properties callback + a
property_denylist so anything the application accidentally
passes is filtered before the network call.If any one layer fails, the next catches the leak.
PostHog Cloud is GDPR-compliant when configured to send anonymous events. The current configuration:
autocapture: false — no implicit DOM-event
capture.disable_session_recording: true — no replay in the
MVP.ip: false — server doesn’t see the visitor IP.capture_pageview: true +
capture_pageleave: true — needed for funnel analysis.Two paths:
Programmatic. Call optOut() from
app/lib/analytics.ts. Wired by the cookie banner’s “Reject”
button (TODO — see below). Persists in localStorage under
the key posthog_optout. The loaded callback in
instrumentation-client.ts checks this on every page load
and calls posthog.opt_out_capturing() if set.
Browser-level. Visitors with Do Not Track or
third-party-cookie blockers will simply not load posthog-js scripts at
all (Netlify serves posthog-js from PostHog Cloud’s edge,
which respects these signals).
Not yet implemented. The spec called for a simple one-line banner with a Reject button. To stay within the launch time budget we deferred it. Today the privacy story is:
optOut()
helper — but there’s no UI surface for it yet.Pre-launch action item: add the footer note.
Week 1 post-launch action item: add the cookie banner
with Reject wired to optOut().
PostHog is for behavioural analytics. The structured logs
from back/logger.py (e.g. voting_decided,
parallel_executor_run, engine_routed) are sent
via structlog to stdout (and to Render’s log aggregator in prod). Those
logs CAN contain merchant strings + amounts because they live behind
authentication on a private infrastructure endpoint. The bright line
is:
| Store | PII allowed? | Authentication? |
|---|---|---|
| PostHog Cloud | ❌ Never | None (anonymous events) |
| Render logs / Sentry | ⚠️ Sparingly — only when needed for debugging | Yes (internal access) |
| Application DB | ✅ Yes (encrypted at rest in prod) | Yes |
A user who wants their data removed needs both:
docs/email-capture-guide.md
known limitations). Manual SQL via the admin operator works today.posthog.reset() from the frontend (no UI for
this yet — Week 1 post-launch).PostHog itself supports a per-user delete via their dashboard or API if a user provides their distinct_id.
If a customer / press / regulator asks for the privacy posture: