ADR-0005 — GDPR data flows + retention
ADR-0005 — GDPR data flows + retention
Status: draft
Date: 2026-05-18
Owner: tech-architect + game-director (legal sign-off pre-launch)
Related canon: D-009 (strict GDPR), D-008 (GCP-native)
Cross-ref: ops/decisions/2026-05-18-firebase-eu-residency-verification.md (Path B)
Status note (2026-05-18): This ADR describes the production target per D-009. Phase 8b implementation uses the simplified test-phase scope per ADR-0006 — no Firebase Auth federation broker, no Cloud SQL
europe-central2-warsawresidency provisioning, no 30-day delete-on-demand pipeline, no 12-month inactivity anonymization sweep, no GDPR export bundle generator, no 90-day anti-cheat log retention sink, no ChatMessage 30-day retention (no chat in test phase). Tester data lives in a local Postgres container on the CEO laptop and is wiped between test cohorts at the CEO’s discretion. The full GDPR posture described below is deferred to the production-migration phase, currently paused; testers are informed of the test-phase data-handling at recruit time.
Context
D-009 mandates strict GDPR: all data resident in europe-central2-warsaw, delete-on-demand within 30 days, 12-month inactivity anonymization, no profiling, no behavioural ads. This ADR turns that posture into concrete data flows, retention schedules, and a DPIA-light checklist that future tech-architect and CEO can run before each major release.
Path B summary (residency)
Per the pre-flight verification, Firebase Auth is used only as a federation broker for Google Sign-In / Apple Sign-In. Email/Password is disabled at MVP. The federation broker holds only the opaque sub claim from Google/Apple. All PII (email, display name, walker state, social data) lives in Cloud SQL europe-central2-warsaw.
USER PII (email, displayName, etc.) Cloud SQL europe-central2-warsaw <-- canonical store, EU only
OAuth sub claim (opaque, no PII) Firebase Auth federation broker (US-managed) <-- minimal foothold
Step data, walker state, allocations, guild, chat Cloud SQL europe-central2-warsaw
Attestation tokens Cloud SQL europe-central2-warsaw, 30d retention
Anti-cheat logs (no PII, no step counts) Cloud Logging EU sink, 90d retention
Session JWTs Signed by KMS key in europe-central2; sent over HTTPS only
GDPR export bundles (zip of all walker data) Cloud Storage europe-central2 bucket, CMEK-encrypted, signed-URL access onlyData classification
| Class | Examples | Retention |
|---|---|---|
| Identity | email, displayName, OAuth sub linkage, internal walkerId | Lifetime of account; deleted within 30d of delete request; anonymized after 12 months inactivity |
| Gameplay state | walker level, allocations, quest progress, faction rep, inventory | Lifetime of account; same delete/anonymize rules |
| Step data | StepLog, StreakState, totalLifetimeSteps | Lifetime of account; aggregated daily totals retained (no individual sample windows beyond 90 days for forensic) |
| Attestation tokens | App Check token IDs + verdicts | 30 days then auto-purged (operational ceiling) |
| Anti-cheat logs | verdict + reason codes (no step counts, no PII) | 90 days then auto-purged |
| Social | guild membership, chat messages, co-op session history | Chat messages retained 30 days (operational ceiling — see “ChatMessage retention” below); other social data lifetime of account |
| Operational logs | Cloud Logging request logs | 30 days then auto-purged |
| Audit logs (GDPR delete confirmations, anonymization runs) | metadata only — what was deleted/anonymized, when, by whom | 7 years (regulatory retention floor; no PII inside) |
No collected categories (explicit drop list):
- GPS location (ADR-0004 defers GPS sanity)
- Behavioural analytics for ads
- Cross-app tracking identifiers
- Demographic profiling
- Health data beyond step counts (no heart rate, no sleep, no calories — even though HealthKit/HC exposes them)
ChatMessage retention (30 days)
Guild chat is operationally retained 30 days. Rationale:
- Long chat history is not core to gameplay.
- Storing chat indefinitely increases delete-on-demand cost and forensic complexity.
- 30 days covers “what did they say last week” recency; older context is recoverable by the participants themselves (no server obligation).
This is documented in the Guild Master sub UX so subscribers know upfront. UI shows “messages auto-delete after 30 days” in the chat header.
Phase 11 may revisit (e.g. extend to 90 days for paid sub tiers) but the rule starts strict.
Delete-on-demand pipeline (30-day SLA)
Triggered by: walker hits “Delete my account” in mobile settings, or emails [email protected] (CEO-managed at MVP).
Day 0 — request received
- Walker request lands at
POST /walker/delete. NestJS verifies walker session + writesUser.deleteRequestedAt = now(). - Cloud Tasks job
gdpr-delete-softenqueued with 24h delay. - Walker receives email confirmation with cancellation link valid 24h (“changed your mind? click here”).
- Mobile app on next foreground call sees
deleteRequestedAtset, shows “Your account is scheduled for deletion in<countdown>” banner.
Day 1 — soft delete (cancellation window expires)
gdpr-delete-soft runs:
- If
deleteRequestedAtwas cleared (cancellation) — abort, no-op. - Walker session JWTs are invalidated immediately.
- User account moved to
SOFT_DELETEDstate — read-only, no logins, no new data. - Cloud Tasks job
gdpr-delete-hardenqueued with 29-day delay.
Day 30 — hard delete
gdpr-delete-hard runs as a Postgres transaction:
-- Pseudocode; actual implementation lives in walkrpg-jobs (phase 11)BEGIN; DELETE FROM "TreeAllocation" WHERE walkerId = $1; DELETE FROM "KeystoneAllocation" WHERE walkerId = $1; DELETE FROM "QuestProgress" WHERE walkerId = $1; DELETE FROM "FactionRep" WHERE walkerId = $1; DELETE FROM "StepLog" WHERE walkerId = $1; DELETE FROM "AttestationLog" WHERE walkerId = $1; DELETE FROM "StreakState" WHERE walkerId = $1; DELETE FROM "GuildMembership" WHERE walkerId = $1; DELETE FROM "ChatMessage" WHERE senderId = $1; -- or anonymize sender to '[deleted]' — see policy DELETE FROM "CoOpSession" WHERE hostWalkerId = $1 OR walkerId IN session.joined; DELETE FROM "Subscription" WHERE walkerId = $1; DELETE FROM "Walker" WHERE id = $1; DELETE FROM "User" WHERE id = $1; INSERT INTO "AuditLog" (type, walkerHash, completedAt) VALUES ('GDPR_HARD_DELETE', sha256($1), now());COMMIT;After commit:
- Firebase Auth federation broker entry is deleted via Admin SDK (removes the opaque
submapping). - Cloud Logging entries naming the walker are purged via Log Router exclusion sink. (Cloud Logging individual record deletion is best-effort; the 30-day operational retention naturally completes purge within ~60 days of hard delete.)
- Cloud Storage GDPR export bundles for this walker are deleted from the bucket.
Chat message author handling (policy choice): at MVP, chat messages from deleted users are anonymized in place to [former walker] rather than deleted, so other guild members’ chat history stays coherent. The author binding is severed; the message text remains. Walker is informed of this in the privacy notice. Alternate stricter policy (hard delete of all messages) is a 1-line config flip if legal review requires it.
Cancellation
Walker has 24h to cancel via the email link. Cancellation triggers User.deleteRequestedAt = NULL, walker logs back in normally. After Day 1 (soft delete), cancellation requires a CEO-handled support request — not impossible, but not self-serve.
Inactivity anonymization (12-month rule)
Cloud Scheduler runs gdpr-anonymize-inactive weekly:
- Find walkers where
lastActiveAt < now() - 12 monthsANDdeleteRequestedAt IS NULL. - For each: anonymize identity fields, preserve gameplay shell for leaderboard/regional event history.
Anonymization SQL (per walker):
UPDATE "User" SET email = 'anon-' || sha256(id) || '@anonymized.walkrpg', displayName = 'Wanderer-' || substring(sha256(id) for 6), firebaseExternalSub = NULL, -- breaks login linkage permanently anonymizedAt = now() WHERE id = $1;-- Walker, Guild memberships, etc. remain — they're not PII once email/displayName are scrubbed.-- ChatMessage rows have author preserved as anonymized displayName.Anonymized walkers cannot log in again (the firebaseExternalSub linkage is gone). If they reach out, the only path is “create a new account”. This is an explicit tradeoff: the alternative (keep linkage indefinitely) violates the data-minimization principle.
GDPR export (“right to access”)
GET /walker/gdpr-export triggers a Cloud Tasks job that:
- Reads all rows for the walker across all tables.
- Writes a JSON bundle to Cloud Storage
gs://walkrpg-gdpr-exports/<walkerHash>/<timestamp>.json. - Bundle is encrypted with CMEK using the KMS key in
europe-central2. - Generates a signed URL valid 7 days.
- Emails walker the signed URL.
Bundle is auto-deleted from the bucket after 30 days (operational ceiling).
DPIA-light checklist (run pre-launch + pre-major-release)
This is a 12-question checklist for tech-architect + CEO. Document outcomes in the L1 decisions log.
- Purpose limitation. Is every data point we collect tied to a documented gameplay or operational purpose? (No “we might use it later” fields.)
- Data minimization. For each collected field: can we drop it and still ship the feature?
- Lawful basis. Each personal data category mapped to one of: contract (gameplay), legitimate interest (anti-cheat), or consent (marketing communications — opt-in only).
- Retention. Does every personal data category have an explicit retention ceiling and an active purge mechanism?
- Transparency. Privacy policy enumerates every data category, retention period, and the third parties involved (Firebase US for federation, App Check verification).
- Subject rights. Self-serve flows exist for: access (export), rectification (settings page), erasure (delete), portability (export bundle is JSON, machine-readable), restriction of processing (delete request triggers soft-delete state).
- Cross-border transfers. The only cross-border data is the opaque OAuth
subclaim transiting Firebase Auth US. SCCs in place via Google Cloud DPA. - Security. Encryption at rest (Cloud SQL + KMS), encryption in transit (HTTPS + TLS), least-privilege IAM, secret rotation policy (Secret Manager 90d rotation), audit logging.
- Profiling / automated decisions. Anti-cheat banning (Layer 3) IS automated decision-making affecting walker; appeal path documented; human review available.
- Children. App store age rating set to 13+. Walker self-declares age at signup; under-13 accounts are blocked.
- Processor agreements. Google Cloud (Firebase + GCP) DPA executed. Apple / Google Play in-app purchase agreements executed.
- Incident response. Breach notification SOP exists; CEO is the data protection point of contact at MVP; named DPO recruited pre-launch if user count exceeds the GDPR Art.37 threshold (Poland: 250+ employees OR systematic monitoring at scale — we don’t trigger either, but DPO is good practice).
Consequences
- The GDPR posture is operationally enforceable, not just a marketing claim. Every retention rule has a purge job; every data category has a documented purpose.
- Path B’s 30-day chat retention is a noticeable UX tradeoff sold as a privacy feature (“auto-clearing chat”) — narrative-designer can lore-bind this if useful (Cech Niedokończonych Wypraw don’t keep gossip logs).
- Inactivity anonymization is one-way — anonymized walkers cannot recover their accounts. This is the data-minimization tax.
- DPIA-light checklist is the pre-launch gate. tech-architect + CEO sign before any release that adds a new data category.