Status: Accepted
Date: 2026-04-21
Deciders: product owner (user), software-architect
Related: ADR 0001, ADR 0002, docs/architecture/auth.md
TradeMasterAPI will process personal data of EU users (email address, display name, IP prefix for audit, paper-trade history, device/passkey metadata). GDPR applies from the first EU user. Retrofitting compliance is expensive and often impossible; we design for it from day one.
The hard asks of GDPR (simplified): lawful basis, purpose limitation, data minimization, defined retention, accuracy (rectification), right of access + portability, right of erasure, breach notification within 72 hours, and maintainable records of processing.
This ADR records the design decisions that discharge each requirement.
GDPR compliance is built into the core — not a feature flag, not a plugin, not a separate "EU mode".
| Right | Endpoint / Mechanism | SLA |
|---|---|---|
| Access | POST /api/gdpr/export |
Asynchronous; bundle available within 30 days (GDPR Art. 12 allows up to 1 month). We target 24h. |
| Portability | Same bundle: JSON + CSV machine-readable | — |
| Erasure ("right to be forgotten") | POST /api/gdpr/erase — requires fresh WebAuthn step-up |
Soft-delete immediately; PII purge after 30-day cooling period; audit rows retained with pseudonymized actor id for 2 years. |
| Rectification | PATCH /api/gdpr/profile (display_name + email-change flow with re-verification) |
Immediate |
| Restriction | Account freeze: admin-only; user can request via email | Case-by-case |
| Objection | Handled at the point of processing (no marketing opt-outs because we do not do marketing) | — |
| Data | Retention | Why |
|---|---|---|
users.email (active) |
Lifetime of account | Contact channel |
webauthn_credentials.* |
Lifetime of account | Required for auth |
sessions |
30 days after expires_at or revoked_at |
Audit trail for anomalous-session investigation |
email_verifications (consumed or expired) |
30 days | Anti-replay + support |
audit_log (security events) |
2 years | DPA / regulatory requirement for financial-adjacent service |
audit_log (trade-affecting events) |
7 years | Brokerage regulatory norms — we align even though we are not the broker of record |
| Paper-trade history | 3 years (proposed; open question in auth.md §10) | Product need balanced against minimization |
| Server logs | 90 days | Ops |
| Breach-notification records | 6 years | Art. 33(5) accountability |
A background retention job (backend_v2/jobs/retention.py, to be created in implementation sub-card) runs nightly, scans tables against this schedule, and deletes/pseudonymizes. The job writes a single audit_log row per run summarizing counts, never per-record PII.
On POST /api/gdpr/erase (step-up verified):
users.deleted_at = now(), users.email = null, users.display_name = null.deleted_at + 30 days removes paper-trade history, email_verifications, and any JSON fields referencing the user.audit_log rows keep the action row but replace actor_user_id with sha256(actor_user_id || per-user-salt) — a one-way pseudonym that preserves the record without identifying the person. The salt is stored encrypted and destroyed at the end of the 2-year audit retention, after which the pseudonym is irreversible even to us.The 30-day cooling period exists so a user who changes their mind (or whose account was erased after credential compromise) can request reinstatement. After 30 days it is gone.
Every state-changing action writes an audit_log row. Redaction rules:
context./24 (IPv4) or /48 (IPv6) prefixes; full IP is discarded at ingress.A nightly hash-chain summary (audit_log_digest — a small table holding (date, sha256_of_day_rows)) gives us tamper-evidence without the cost of a per-row chain. If a future row disagrees with its digest, the integrity alarm fires.
audit_log row with action breach.detected.backend_v2/jobs/breach_notifier.py) files a severity:critical issue, pages on-call, and starts a 72-hour timer.docs/agents/security_response.md is extended with the GDPR-Art.-33 notification template and the supervisory-authority contact list. We do not duplicate it here.A living document at docs/architecture/gdpr-ropa.md (to be filed in a sub-card) enumerates: categories of data subjects, categories of data, purposes, retention, cross-border transfers, security measures. This ADR does not duplicate it.
Given that we will process financial-adjacent behavioral data and route real orders, a DPIA is appropriate at the live-trading unlock milestone, not at multi-user MVP. Tracked as a follow-up card.
Rejected. Impossible to reliably detect jurisdiction, and retrofitting export/erasure to "all users who flip on GDPR mode" just means we did the work twice.
Rejected for v1. Adds a processor (more compliance surface), is overkill for our data volume, and locks us in. Revisit at scale.
Rejected. We have a legal obligation under financial-adjacent rules to retain certain logs. Pseudonymization is the compromise.