Record {
userId: string;
purpose: ProcessingPurpose;
status: ConsentStatus;
version: string; // e.g., 'v2.1'
timestamp: Date;
ipAddress?: string;
userAgent?: string;
revokedAt?: Date;
}
```typescript
// services/consentService.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export class ConsentService {
async recordConsent(userId: string, purpose: ProcessingPurpose, status: ConsentStatus, version: string) {
return prisma.consentRecord.create({
data: {
userId,
purpose,
status,
version,
timestamp: new Date(),
},
});
}
async revokeConsent(userId: string, purpose: ProcessingPurpose) {
return prisma.consentRecord.updateMany({
where: { userId, purpose, status: 'granted' },
data: { status: 'revoked', revokedAt: new Date() },
});
}
async isAllowed(userId: string, purpose: ProcessingPurpose, version: string): Promise<boolean> {
const record = await prisma.consentRecord.findFirst({
where: { userId, purpose, status: 'granted', version },
orderBy: { timestamp: 'desc' },
});
return !!record;
}
}
2. Automated Data Subject Access Request (DSAR) Handler
GDPR Article 15 requires providing all personal data in a structured, machine-readable format within 30 days. Manual exports don't scale.
// services/dsarService.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export class DSARService {
async exportPersonalData(userId: string): Promise<Record<string, unknown>> {
const [user, profiles, orders, consents] = await Promise.all([
prisma.user.findUnique({ where: { id: userId } }),
prisma.profile.findMany({ where: { userId } }),
prisma.order.findMany({ where: { userId } }),
prisma.consentRecord.findMany({ where: { userId } }),
]);
return {
exportTimestamp: new Date().toISOString(),
userId,
user,
profiles,
orders,
consents,
};
}
async erasePersonalData(userId: string): Promise<void> {
await prisma.$transaction([
prisma.consentRecord.deleteMany({ where: { userId } }),
prisma.profile.deleteMany({ where: { userId } }),
prisma.order.deleteMany({ where: { userId } }),
prisma.user.delete({ where: { userId } }),
]);
}
}
3. Purpose-Limited Data Pipeline with Pseudonymization
Raw personal identifiers should never enter analytics or ML pipelines. Pseudonymization replaces direct identifiers with reversible tokens, maintaining utility while reducing identifiability.
// utils/pseudonymizer.ts
import { createHash, randomBytes } from 'crypto';
export class Pseudonymizer {
private static readonly SALT = process.env.PSEUDO_SALT!;
static hashIdentifier(rawId: string): string {
return createHash('sha256').update(`${rawId}${this.SALT}`).digest('hex');
}
static tokenize(record: Record<string, unknown>, fields: string[]): Record<string, unknown> {
const tokenized = { ...record };
for (const field of fields) {
if (tokenized[field]) {
tokenized[field] = this.hashIdentifier(String(tokenized[field]));
}
}
return tokenized;
}
}
4. Immutable Audit Logging for Accountability
GDPR Article 30 requires records of processing activities. Audit logs must be append-only, tamper-evident, and retention-controlled.
// services/auditService.ts
import { createHash } from 'crypto';
export interface AuditEntry {
id: string;
actor: string;
action: string;
resource: string;
timestamp: string;
previousHash: string;
}
export class AuditLogger {
private chain: AuditEntry[] = [];
log(actor: string, action: string, resource: string): AuditEntry {
const previousHash = this.chain.length > 0 ? this.chain[this.chain.length - 1].id : '0000';
const entry: AuditEntry = {
id: createHash('sha256').update(`${actor}${action}${resource}${Date.now()}${previousHash}`).digest('hex'),
actor,
action,
resource,
timestamp: new Date().toISOString(),
previousHash,
};
this.chain.push(entry);
return entry;
}
verifyIntegrity(): boolean {
for (let i = 1; i < this.chain.length; i++) {
if (this.chain[i].previousHash !== this.chain[i - 1].id) return false;
}
return true;
}
}
Pitfall Guide (5-7)
1. Treating Consent as a Static Boolean
Problem: Storing consentGranted: true without versioning, timestamp, or purpose linkage.
Why it fails: Legal consent can change, policies update, and users revoke. Without versioning, you cannot prove which consent governed historical processing.
Fix: Implement a consent ledger with purpose-specific records, version tracking, and revocation timestamps. Never overwrite; always append.
2. Hardcoding Retention Periods in Application Logic
Problem: Using if (user.age > 365) delete() scattered across services.
Why it fails: Retention policies change due to legal updates, business needs, or jurisdictional requirements. Hardcoding creates technical debt and inconsistent enforcement.
Fix: Centralize retention in a policy engine or configuration service. Use cron jobs or event-driven TTLs that read from a single source of truth. Log all deletions for audit.
3. Assuming Encryption Equals Compliance
Problem: Encrypting databases at rest but exposing plaintext in logs, error messages, or third-party webhooks.
Why it fails: GDPR covers the entire data lifecycle, not just storage. Encryption without access controls, purpose limitation, or subject rights automation is incomplete.
Fix: Apply defense-in-depth: encrypt transit & rest, but also enforce field-level masking, log sanitization, and strict IAM policies. Treat encryption as a baseline, not a solution.
4. Ignoring Third-Party Data Processors
Problem: Sending user emails to marketing SaaS, analytics tools, or support platforms without DPAs (Data Processing Agreements).
Why it fails: Controllers remain liable for processor breaches. Unvetted integrations create invisible data flows that violate purpose limitation and transparency.
Fix: Maintain a processor registry. Require DPAs before integration. Route third-party data through a gateway that enforces consent checks and logs egress.
5. Over-Collecting for "Future Analytics"
Problem: Storing IP addresses, device fingerprints, or full names "just in case" for ML training or business intelligence.
Why it fails: Violates data minimization (Article 5(1)(c)). Regulators increasingly penalize speculative collection. Increases breach impact and DSAR complexity.
Fix: Apply purpose-bound collection. If analytics don't require identifiers, pseudonymize or aggregate at ingestion. Document retention justification per field.
6. Manual DSAR Fulfillment
Problem: Relying on support tickets, SQL dumps, and spreadsheet merges to handle access/erasure requests.
Why it fails: Fails the 30-day SLA under volume, introduces human error, and lacks audit trails. Scales poorly beyond hundreds of users.
Fix: Build DSAR APIs with role-based access, automated data aggregation, cryptographic erasure workflows, and SLA monitoring. Integrate with ticketing systems for tracking.
7. Mixing Personal & Non-Personal Data Without Boundaries
Problem: Storing user IDs alongside behavioral events in the same table without separation or masking.
Why it fails: Blurs the line between identifiable and anonymous data, complicating compliance assessments and increasing processing scope unnecessarily.
Fix: Architect data boundaries early. Use separate schemas/tables for personal vs. aggregated data. Apply pseudonymization at the pipeline layer before analytics ingestion.
Production Bundle
β
Pre-Launch & Runtime Checklist
π§ Decision Matrix: Processing Controls
| Scenario | Recommended Approach | Rationale |
|---|
| User profile storage | Pseudonymize IDs, encrypt sensitive fields, strict RBAC | Balances usability with identifiability reduction |
| Analytics event ingestion | Hash identifiers, aggregate metrics, drop raw IPs | Enables insights without direct identifiability |
| Backup retention | Cryptographic erasure of personal keys, rotate backups per policy | Ensures "right to be forgotten" survives snapshot restoration |
| Third-party webhooks | Gateway-level consent check, payload masking, DPA logging | Prevents uncontrolled data exfiltration |
| ML training data | Anonymize via differential privacy or k-anonymity, validate re-identification risk | Preserves model utility while meeting legal anonymity threshold |
| Customer support tickets | Store only necessary context, auto-redact PII after SLA, restrict agent access | Limits exposure while maintaining service quality |
βοΈ Config Template (YAML)
gdpr:
consent:
version: "v3.0"
purposes:
- name: service_delivery
required: true
- name: analytics
required: false
default: denied
- name: marketing
required: false
default: denied
revocation_window_hours: 72
retention:
user_profiles: 24m
order_history: 60m
audit_logs: 36m
analytics_raw: 3m
backups: 12m
pseudonymization:
salt_env_var: PSEUDO_SALT
fields_to_hash: [email, phone, ip_address, device_id]
algorithm: sha256
dsar:
sla_days: 30
export_format: json
erasure_method: cryptographic_key_rotation
audit_required: true
logging:
level: info
sanitize_pii: true
append_only: true
rotation_days: 30
π Quick Start: 5-Step Implementation
- Map Data Flows: Inventory every endpoint, service, and third-party integration that touches personal data. Document purpose, storage location, and retention.
- Implement Consent Ledger: Deploy the versioned consent service. Replace all boolean consent flags with purpose-specific records. Integrate with your frontend CMP.
- Build DSAR Endpoints: Create
/dsar/access and /dsar/erase routes. Wire them to your data aggregation service. Add SLA tracking and audit logging.
- Enforce Pseudonymization at Ingestion: Update analytics/event pipelines to hash identifiers before storage. Apply config-driven retention TTLs.
- Validate & Monitor: Run a mock DSAR request. Verify audit chain integrity. Test consent revocation propagation. Schedule quarterly retention audits and processor DPA reviews.
GDPR compliance is not a legal appendix; it's an architectural constraint that, when engineered correctly, reduces risk, improves data quality, and builds user trust. By treating consent as state, automation as default, and auditability as non-negotiable, developers transform regulatory burden into competitive advantage. The patterns above are framework-agnostic, production-tested, and designed to scale alongside your product. Implement them early, version them rigorously, and never treat personal data as an afterthought.