Skip to content

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-warsaw residency 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 only

Data classification

ClassExamplesRetention
Identityemail, displayName, OAuth sub linkage, internal walkerIdLifetime of account; deleted within 30d of delete request; anonymized after 12 months inactivity
Gameplay statewalker level, allocations, quest progress, faction rep, inventoryLifetime of account; same delete/anonymize rules
Step dataStepLog, StreakState, totalLifetimeStepsLifetime of account; aggregated daily totals retained (no individual sample windows beyond 90 days for forensic)
Attestation tokensApp Check token IDs + verdicts30 days then auto-purged (operational ceiling)
Anti-cheat logsverdict + reason codes (no step counts, no PII)90 days then auto-purged
Socialguild membership, chat messages, co-op session historyChat messages retained 30 days (operational ceiling — see “ChatMessage retention” below); other social data lifetime of account
Operational logsCloud Logging request logs30 days then auto-purged
Audit logs (GDPR delete confirmations, anonymization runs)metadata only — what was deleted/anonymized, when, by whom7 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

  1. Walker request lands at POST /walker/delete. NestJS verifies walker session + writes User.deleteRequestedAt = now().
  2. Cloud Tasks job gdpr-delete-soft enqueued with 24h delay.
  3. Walker receives email confirmation with cancellation link valid 24h (“changed your mind? click here”).
  4. Mobile app on next foreground call sees deleteRequestedAt set, shows “Your account is scheduled for deletion in <countdown>” banner.

Day 1 — soft delete (cancellation window expires)

gdpr-delete-soft runs:

  1. If deleteRequestedAt was cleared (cancellation) — abort, no-op.
  2. Walker session JWTs are invalidated immediately.
  3. User account moved to SOFT_DELETED state — read-only, no logins, no new data.
  4. Cloud Tasks job gdpr-delete-hard enqueued 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 sub mapping).
  • 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:

  1. Find walkers where lastActiveAt < now() - 12 months AND deleteRequestedAt IS NULL.
  2. 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:

  1. Reads all rows for the walker across all tables.
  2. Writes a JSON bundle to Cloud Storage gs://walkrpg-gdpr-exports/<walkerHash>/<timestamp>.json.
  3. Bundle is encrypted with CMEK using the KMS key in europe-central2.
  4. Generates a signed URL valid 7 days.
  5. 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.

  1. Purpose limitation. Is every data point we collect tied to a documented gameplay or operational purpose? (No “we might use it later” fields.)
  2. Data minimization. For each collected field: can we drop it and still ship the feature?
  3. Lawful basis. Each personal data category mapped to one of: contract (gameplay), legitimate interest (anti-cheat), or consent (marketing communications — opt-in only).
  4. Retention. Does every personal data category have an explicit retention ceiling and an active purge mechanism?
  5. Transparency. Privacy policy enumerates every data category, retention period, and the third parties involved (Firebase US for federation, App Check verification).
  6. 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).
  7. Cross-border transfers. The only cross-border data is the opaque OAuth sub claim transiting Firebase Auth US. SCCs in place via Google Cloud DPA.
  8. Security. Encryption at rest (Cloud SQL + KMS), encryption in transit (HTTPS + TLS), least-privilege IAM, secret rotation policy (Secret Manager 90d rotation), audit logging.
  9. Profiling / automated decisions. Anti-cheat banning (Layer 3) IS automated decision-making affecting walker; appeal path documented; human review available.
  10. Children. App store age rating set to 13+. Walker self-declares age at signup; under-13 accounts are blocked.
  11. Processor agreements. Google Cloud (Firebase + GCP) DPA executed. Apple / Google Play in-app purchase agreements executed.
  12. 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.