Storage and Cookie Disclosure
This document was fact-checked against the current codebase, not drafted by a lawyer. It is not legal advice. The consent-exemption reasoning in §2 follows standard, well-established EDPB/CNIL guidance for functional storage, which is a reasonable basis to publish on; if you want a paid second opinion, get one, but nothing here should block launch.
Last updated: 2026-08-30 Applies to: PartyF frontend (partyf-frontend)
Executive Summary
Corrected 2026-08-30: the WS-token claim in §3 was stale — the query-string transport it described was replaced by Sec-WebSocket-Protocol (see the corrected §3 below), and §1.2 was missing a third key (partyf.lastNickname) that src/services/sessionStorage.ts has stored since before this document's last edit. Verified against src/services/wsClient.ts and src/services/sessionStorage.ts directly, not assumed.
Corrected 2026-08-13: the 2026-08-10 draft claimed PartyF uses only one form of client-side storage (the locale preference). That was wrong — src/services/sessionStorage.ts also persists the session token, room code, and player ID to localStorage for session-restore-on-refresh, and this contradicted PRIVACY-NOTICE.md's claim that tokens are "stored in memory (not persisted to disk or localStorage)." Verified against the actual source, not assumed. PartyF uses two forms of client-side storage. The consent-banner conclusion below is unchanged (both are functionally necessary, neither is tracking/profiling — see §2), but the underlying facts and the token's retention window needed correcting.
Corrected 2026-08-22: §6.1 below also claimed "no analytics or error tracking is deployed." That's no longer accurate: @sentry/vue (error tracking, plus a new in-app "Feedback" form for bug reports/feature requests) and posthog-js (product analytics) are both wired into the frontend, each activating only when a given deployment supplies its vendor key/DSN at build time — see privacy.md §6 for the data-collection disclosure. PostHog's default client-side initialization also writes its own entries to localStorage, which this storage-specific document had not previously accounted for — see the new §1.4. Verified against src/telemetry/ and src/main.ts, not assumed.
1. Storage Used
1.1 localStorage — Language Preference
Storage key: partyf.locale
Storage duration: Indefinite (persists across sessions)
Data stored: A single locale code (en, es, fr, it, ca, or eu)
Set by: Frontend JavaScript in src/i18n/index.ts
Purpose: To remember your language preference so you do not have to re-select it every time you visit PartyF.
1.2 localStorage — Session Restore
Storage keys: roomCode, token, youId, sessionRestoredAt
Storage duration: Until the app explicitly clears it (clearSavedSession() — e.g. on leaving/ending a session) or the user clears their browser storage. This is a materially different retention profile than the backend session itself: the backend enforces a hard 6-hour TTL and a 30-minute idle reap (docs/architecture.md), but a stale copy of the token can persist in the browser longer than the session it refers to remains valid server-side. A leftover token becomes inert once the backend session expires or is deleted (the backend rejects it), but it is not proactively wiped from the browser at that point — only on explicit leave/end or a manual browser clear.
Data stored: The session's bearer token (a credential — see partyf-backend/CLAUDE.md's "Tokens are credentials" section), the room code, the player's own ID, and a restoration timestamp.
Set by: src/services/sessionStorage.ts (saveSession/markSessionRestored), called from the session join/restore flow in src/store.ts.
Purpose: So a player who refreshes the page or briefly loses connectivity can rejoin their in-progress session without re-entering a room code, rather than being dropped from the game.
1.3 localStorage — Remembered Nickname
Storage key: partyf.lastNickname Storage duration: Indefinite (persists across sessions, never cleared automatically — unlike §1.2, clearSavedSession() does not touch this key) Data stored: The free-text display name you last typed when creating or joining a room. Per PRIVACY-NOTICE.md §1, this is personal data if you typed your real name. Set by: src/services/sessionStorage.ts (saveLastNickname), called from src/store.ts on game creation and join; read by LandingScreen.vue to pre-fill the name field on your next visit.
Purpose: So a returning player doesn't have to retype their name every visit.
This one is materially different from §1.1/§1.2 and needs its own line in the Privacy Notice (added below): it is the only client-side storage that (a) can hold personal data and (b) is never automatically cleared. It's still likely "strictly necessary"-exempt under the same functional-convenience reasoning as §1.1 (see §2), but it belongs in the disclosure regardless.
1.4 localStorage — Product Analytics (PostHog, deployment-dependent)
Storage keys: Set and managed internally by the posthog-js client library (its own distinctid, session, and locally-queued-event keys) — not keys PartyF's own code defines.
Storage duration: Per posthog-js's own default browser-storage behavior; legal counsel should confirm whether these defaults are acceptable or need overriding (e.g. shorter persistence, or a different posthog-js storage mode) before enabling PostHog on any deployment.
Data stored: An anonymous distinct ID and locally-queued analytics events, written by PostHog's client library, not by PartyF's application code.
Set by: posthog-js, initialized in src/telemetry/postHogAnalytics.ts — only on deployments built with a VITEPOSTHOG_KEY; a deployment without one never loads this library and writes nothing.
Purpose: Lets PostHog attribute anonymous product-analytics events to the same browser across a visit/session. Unlike §1.1–1.2, this is third-party (vendor) storage serving an analytics purpose, not PartyF's own functional storage — see the note at the end of §2 for why this changes the consent analysis.
2. Consent Analysis
2.1 Why a Consent Banner Is NOT Required
Under the ePrivacy Directive (2002/58/EC, as amended) and GDPR, consent is required for storage on a user's device unless the storage is strictly necessary for the operation of the requested service.
All three first-party localStorage uses (§1.1 language preference, §1.2 session restore, §1.3 remembered nickname) qualify as strictly necessary under the same exemption — session-restore storage exists purely to let a player keep playing the game they explicitly joined after a refresh or connectivity blip, and the remembered nickname exists purely to save a returning player from retyping a name they already gave the service once; neither is used for tracking, profiling, or any purpose beyond that specific convenience. The reasoning below was originally written for the locale preference alone; it applies equally to §1.2 and §1.3:
1. It is functionally essential to the service: The user explicitly selects a language. Without localStorage, the language preference cannot be saved. The user would have to re-select their language every single visit—a poor user experience and arguably a failure of the service.
2. It contains no tracking, profiling, or analytics: The locale code is not used to track user behavior, build profiles, or serve targeted content. It is read locally in the browser and used only to display content in the selected language.
3. It is exempt under GDPR Recital 32: Storage necessary to provide a service explicitly requested by a user does not require consent. A user's language preference is a request-specific setting, not a persistent profile or tracking mechanism.
4. EU guidance supports this: The European Data Protection Board (EDPB) and Member State guidance (e.g., CNIL in France) explicitly exempt language preferences and similar UX settings from consent requirements, provided they are not linked to analytics or profiling.
Conclusion: A consent banner for language preference storage is unnecessary and would degrade UX without providing meaningful privacy protection.
This §2 analysis covers only §1.1–§1.3. It was written before §1.4 (PostHog analytics storage) existed and does not extend to it: PostHog's storage serves an analytics purpose rather than delivering the specific service the user requested, so the "strictly necessary" exemption reasoning above may not apply the same way.
Live gap, verified against the deployed production bundle, not just the repo (2026-08-30): src/main.ts calls analytics.init() unconditionally on every page load; createAnalytics() (src/telemetry/index.ts) uses the real PostHog adapter whenever VITEPOSTHOGKEY is non-empty at build time, with no check of the AdSense Funding Choices consent signal or any other consent state first. This is not hypothetical: the production Cloud Run frontend has a live PostHog project key baked into its shipped JS bundle right now, pointed at eu.i.posthog.com, and fires for every visitor — EEA/UK included — with no consent gate. (Checking the repo for the key and finding none there was not sufficient to conclude it isn't set — it's injected as a build secret at deploy time, outside this repo's files.)
Fixed 2026-08-30, redeployed and confirmed live 2026-09-01: src/main.ts now gates analytics.init() behind the IAB TCF consent signal the Funding Choices script exposes (window.tcfapi), only starting PostHog once the visitor is outside GDPR/UK-GDPR scope (tcData.gdprApplies === false) or has given TCF Purpose 1 consent. It fails closed: if the CMP script never loads (blocked, unsupported browser) it stays off rather than assuming consent. Verified 2026-09-01 by checking the exact commit SHA of the image tag running on the live partyf-frontend Cloud Run service (via gcloud run services describe) — the gate is in that commit, so the code-side gap described above no longer applies to the currently-running production bundle. Remaining manual step, not code — blocked, not neglected (confirmed 2026-09-01): the Funding Choices message itself must actually be published for pub-8800374421096613 in the AdSense dashboard's "Privacy & messaging" section for tcfapi to ever report a real consent decision. That publication step is gated on Google approving the site for AdSense in the first place, which is still pending — the section isn't available to configure until approval comes through, so there's nothing to do here yet beyond waiting and re-checking. Until it's confirmed, the code fix above is a no-op (never starts PostHog) rather than an active risk — safe by default, just not necessarily what's intended long-term.
2.2 Recommended Disclosure Instead
Rather than a consent banner, PartyF should include a clear, plain-language disclosure of localStorage use in:
1. The Privacy Notice (see PRIVACY-NOTICE.md) — include a section on client-side storage. 2. A footer link or in-app help text — a brief line: "We save your language preference locally on your device so you don't have to re-select it."
This satisfies the transparency requirement of GDPR Art. 13–14 without the friction of a consent banner.
3. No Cookies
PartyF does not use HTTP cookies. Session tokens are:
- Sent as HTTP
Authorization: Bearer <token>headers in REST requests, and over
WebSocket as a Sec-WebSocket-Protocol subprotocol value rather than in the connection URL's query string (src/services/wsClient.ts, ws/handler.go's extractToken) — corrected 2026-08-30: this document previously said the token was still in the query string; that was the pre-fix behavior tracked as GO-LIVE-CHECKLIST.md H4, which shipped 2026-08-13 (see that file's "Progress 2026-08-13" entry). Keeping the token out of the URL means it isn't captured in platform-level request logs (e.g. Cloud Run's), which is the whole point — verify against the two files above if this is ever load-bearing for a decision, since it's the kind of fix that's easy to silently revert.
- Persisted to
localStorage, not memory-only (corrected 2026-08-13 — see §1.2)
for session-restore purposes.
- Never written to cookies.
Tokens are credentials and are treated as such in the backend; see PRIVACY-NOTICE.md and partyf-backend/CLAUDE.md for details. The Privacy Notice's data-retention table should be read alongside §1.2 above — the backend's TTL bounds how long a token remains valid, not how long a copy of it can sit in a user's browser storage.
4. Third-Party Tracking
Corrected 2026-08-22: the list below previously said PartyF uses no analytics platform at all. That's no longer true:
- PostHog (
posthog-js) is now integrated for anonymous product analytics — the
same general category (like Mixpanel/Amplitude) this section used to rule out entirely. It only runs on deployments built with VITEPOSTHOGKEY; see §1.4 for the storage it writes.
- Sentry (
@sentry/vue) is now integrated for error tracking and an in-app
feedback form. It only runs on deployments built with VITESENTRYDSN.
PartyF still does not use Google Analytics, Facebook Pixel, Hotjar, Drift, Intercom, or session-replay tooling (no Sentry.replayIntegration() or equivalent is configured). This section's framing already reflects PostHog's presence (updated above); the code-side consent gate in §2.1 is resolved (see the 2026-09-01 update there) — the only open item is the AdSense dashboard "Privacy & messaging" publication step, which this repo can't verify.
Corrected 2026-08-17: the "no other third-party script or CDN references" claim below no longer holds — index.html now loads Google AdSense (pagead2.googlesyndication.com) and Google's Funding Choices consent script (fundingchoicesmessages.google.com), added in the AdSense integration (PR #8). These set their own cookies/storage on the visitor's device for ad delivery, measurement, and (pre-consent, in the EEA/UK) the consent state itself — this storage is not first-party localStorage under PartyF's control and is out of scope for the "strictly necessary" analysis in §2, which only ever covered locale + session-restore. AdSense storage requires its own consent basis; see privacy.md §6 for the disclosure and Google's Privacy & messaging tool (wired in index.html, must also be configured in the AdSense dashboard) for the actual EEA/UK consent collection.
Fonts remain self-hosted (10 woff2 subsets bundled with the app); confirmed no fonts.googleapis.com/fonts.gstatic.com references remain in the codebase or built image.
5. Implementation Guidance
5.1 What NOT to Do
- ❌ Do not add a cookie consent banner for localStorage.
- ❌ Do not gate language preference selection behind a consent flow.
- ❌ Do not copy a generic "cookie notice" from a template.
5.2 What TO Do
- ✅ Add a clear sentence to the Privacy Notice disclosing localStorage use and its purpose.
- ✅ Optionally, add an in-app tooltip or help text explaining language preference storage.
- ✅ If third-party tracking is ever added in the future, conduct a fresh legal review before deployment.
6. Assumptions and Caveats
This analysis assumes:
1. ~~No analytics or error tracking is deployed~~ — no longer the case, corrected 2026-08-22. Frontend error handlers (app.config.errorHandler, window.onunhandledrejection) still always log to console.error first, but @sentry/vue and posthog-js are now integrated (see §1.4 and §4) and, when a deployment supplies the relevant vendor key, also send data off the browser. This assumption no longer holds; the fresh legal review and consent-mechanism question it anticipated is exactly what §2's and §4's new caveats above are flagging, not something already resolved here.
2. Fonts are self-hosted — confirmed, see §4.
3. No further undisclosed data flows beyond §1.1–§1.4 as of 2026-08-30 (the nickname key in §1.3 was the one gap found in this pass). If the frontend begins storing anything else in localStorage, re-run the §7 audit steps below and update this document.
4. Jurisdiction is EU/EEA. This analysis applies to visitors in the EU and EEA. If you serve significant traffic from other jurisdictions (e.g., UK, Switzerland, Canada, California), their respective data protection laws may impose additional requirements. Consult local counsel.
7. Maintenance
When you next update this analysis (e.g., after adding new features):
1. Re-audit the code: grep -r localStorage src/ and check for any new storage. 2. Check for analytics: Search for gtag, posthog, segment, amplitude, dataLayer, and other tracking libraries. 3. Review third-party requests: Check index.html for <script> tags and CDN links. 4. Update this document with findings. 5. Have legal counsel review any changes to data flows.
Status: Nothing here blocks publication. Two concrete follow-ups from this pass, neither requiring paid legal advice: (1) add the §1.3 nickname key to PRIVACY-NOTICE.md's client-side storage bullet (done in this pass — see that file), and (2) ~~don't set VITEPOSTHOGKEY on any EEA/UK-facing deployment until PostHog's init is gated behind a consent check~~ — done, see §2.1's 2026-09-01 update: the consent gate is in code and confirmed live on the production revision; the one thing left is confirming the AdSense "Privacy & messaging" dashboard step (§2.1), which governs whether the gate ever actually opens, not whether it exists. If you later want a paid second opinion on the consent-exemption reasoning for a specific jurisdiction outside the EU/EEA, that's the one item worth spending money on — everything else here is a verifiable fact about the code, not a judgment call.