Answers the ISO 27001 A.8.10 finding from the compliance swarm on #1242,
which read the storage inventory as saying the two localStorage keys
persist forever with no deletion mechanism. Half of that was our
omission: the inventory never said what logout does.
ph_conv_<token> IS deleted on logout. posthog.reset() resets the
conversations manager, which removes that single known key, and
resetAnalyticsIdentity() already runs in both logout handlers before
signOut(). That is what stops a shared device carrying one user's
support-ticket session into the next user's. Recorded in
lib/analytics/reset.ts too, because it now makes that call load-bearing
for a cross-user concern rather than mere tidiness. Verified by reading
the SDK, not by executing a logout, and the docs say so.
seenSurvey_<id> genuinely has no deletion trigger and cannot have one:
no PostHog bundle enumerates localStorage (zero occurrences of
localStorage.key( or Object.keys(localStorage) across module.js,
surveys.js and conversations.js), so nothing can discover the keys to
remove them. Stated as the accepted retention position rather than left
silent: the value is "true" under an opaque survey id with no personal
data, and clearing it on logout would re-prompt every survey to the next
person on the device and produce false survey responses.
Adds the review date the A.5.9 remediation asked for, and reframes the
review trigger as a pre-launch step, since Support was caught post-hoc
and left the privacy page inaccurate in the interval.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Recapt shuts down in four days, taking product analytics and session
replay with it. This adds PostHog Cloud EU alongside it; the Recapt
removal follows separately so events can be confirmed landing first.
Wiring choices that are not the tutorial defaults:
- Same-origin reverse proxy (/rl -> eu.i.posthog.com) instead of adding
PostHog hosts to the CSP. connect-src 'self' and script-src 'self'
already cover it, tracking blockers have no third-party host to match,
and the Recapt allowlist entries in next.config.ts get replaced by
nothing at all when they go. Needs skipTrailingSlashRedirect, since
PostHog sends trailing-slash API requests; verified that trailing-slash
URLs on normal routes still resolve 200 rather than 404.
- /rl is excluded from the proxy.ts matcher. Middleware runs BEFORE
next.config rewrites, so without this updateSession() treats an
ingestion POST as an unknown protected path and 307s it to /login.
Verified with a control: /zz/flags/ -> 307 /login, /rl/flags/ -> 200
from PostHog. This fails silently otherwise, because asset loads keep
working through the rewrite while no events arrive.
- persistence: 'memory' so nothing is written to the device and no
cookie-consent banner is required. Everything post-login is unaffected:
AnalyticsIdentify re-identifies on each dashboard load.
- session_recording.maskTextSelector: '*'. PostHog masks inputs but not
text by default, and this app renders org numbers (which for an
enskild firma ARE the owner's personnummer), customer names and
balances as ordinary text. Replays show where a user gets stuck, never
what their books say. buildGroupProperties() also refuses to send
org_number at all, with a test pinning it.
- Error tracking registers through the existing lib/observability sink
rather than bypassing it, so every error-level createLogger() line is
captured already redacted. instrumentation.ts onRequestError covers
what escapes uncaught.
Analytics is hosted-only: isAnalyticsEnabled() short-circuits on
NEXT_PUBLIC_SELF_HOSTED and no Docker sentinel is added, so self-hosted
runs with zero third-party runtime code. Recapt got that outcome only by
accident, via a missing sentinel; here it is explicit and tested.
vitest.config.ts aliases 'server-only' to a stub: it is a build-time
guard whose real entry point always throws, which broke 48 test files the
moment a server-only module entered the graph. request-context.ts was
already carrying the same latent trap.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>