| name | gdpr-privacy |
| description | GDPR and data privacy implementation patterns: Right to Erasure, data retention policies, PII detection and anonymization, consent management, Data Subject Access Requests (DSAR), audit logs, and data minimization. Required for any EU-facing product. |
GDPR & Data Privacy Skill
When to Activate
- Building any product that processes EU personal data
- Implementing "Delete my account" or "Export my data" features
- Setting up data retention policies
- Adding analytics or tracking (consent required)
- Handling PII in logs, databases, or third-party services
- Building a DSAR (Data Subject Access Request) workflow
Core Concepts
| Term | Meaning |
|---|
| Personal Data | Any data that can identify a person (name, email, IP, cookie ID, etc.) |
| Data Subject | The individual whose data you process |
| Controller | Your company — decides purposes and means of processing |
| Processor | Third parties processing data on your behalf (AWS, Stripe, etc.) |
| Lawful Basis | Why you're allowed to process: Consent, Contract, Legitimate Interest, Legal Obligation |
| Data Minimization | Only collect what you actually need |
| Purpose Limitation | Only use data for the purpose it was collected |
Right to Erasure (Right to be Forgotten)
The most complex GDPR requirement technically. Plan it before you build the data model.
interface ErasureResult {
userId: string;
deletedAt: Date;
steps: ErasureStep[];
}
interface ErasureStep {
resource: string;
action: 'deleted' | 'anonymized' | 'retained' | 'failed';
reason?: string;
}
async function eraseUser(userId: string): Promise<ErasureResult> {
const steps: ErasureStep[] = [];
const hasOpenInvoices = await db.query.invoices.findFirst({
where: and(eq(invoices.userId, userId), eq(invoices.status, 'open')),
});
await db.delete(sessions).where(eq(sessions.userId, userId));
steps.push({ resource: 'sessions', action: 'deleted' });
await db.delete(notifications).where(eq(notifications.userId, userId));
steps.push({ resource: 'notifications', action: 'deleted' });
if (hasOpenInvoices) {
steps.push({ resource: 'invoices', action: 'retained', reason: 'Open invoice — legal obligation' });
} else {
await db
.update(invoices)
.set({
userEmail: null,
userName: 'Deleted User',
userId: null,
anonymizedAt: new Date(),
})
.where(eq(invoices.userId, userId));
steps.push({ resource: 'invoices', action: 'anonymized' });
}
await db
.update(auditLogs)
.set({ actorEmail: null, actorName: null, ipAddress: null })
.where(eq(auditLogs.actorId, userId));
steps.push({ resource: 'audit_logs', action: 'anonymized' });
await db.delete(users).where(eq(users.id, userId));
steps.push({ resource: 'user', action: 'deleted' });
await Promise.allSettled([
stripe.customers.del(user.stripeCustomerId),
intercom.delete('/contacts', { email: user.email }),
]);
steps.push({ resource: 'third_party_processors', action: 'deleted' });
await db.insert(erasureLog).values({
userId,
requestedAt: user.erasureRequestedAt,
completedAt: new Date(),
steps: JSON.stringify(steps),
});
return { userId, deletedAt: new Date(), steps };
}
Data Retention Policies
async function enforceRetention() {
const policies: RetentionPolicy[] = [
{
table: 'request_logs',
column: 'created_at',
retainDays: 90,
action: 'delete',
},
{
table: 'audit_logs',
column: 'created_at',
retainDays: 365 * 7,
action: 'archive',
},
{
table: 'users',
column: 'deleted_at',
retainDays: 30,
action: 'hard_delete',
condition: sql`deleted_at IS NOT NULL`,
},
{
table: 'events',
column: 'created_at',
retainDays: 730,
action: 'anonymize',
anonymizeColumns: [, , ],
},
];
( policy policies) {
(policy);
}
}
PII in Logs — Prevention
const PII_PATTERNS = [
{ name: 'email', regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, replace: '[EMAIL]' },
{ name: 'phone', regex: /\+?[\d\s\-().]{10,}/g, replace: '[PHONE]' },
{ name: 'credit_card', regex: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, replace: '[CARD]' },
{ name: 'ssn', regex: /\b\d{3}-\d{2}-\d{4}\b/g, replace: '[SSN]' },
];
function scrubPII(obj: unknown): unknown {
if (typeof obj === 'string') {
return PII_PATTERNS.reduce(
(str, p) => str.replace(p.regex, p.replace),
obj
);
}
if (Array.isArray(obj)) return obj.map(scrubPII);
if (obj && typeof obj === 'object') {
.(
.(obj).( [k, (v)])
);
}
obj;
}
= ([
, , , ,
, , ,
]);
(): <, > {
.(
.(obj).( [
k,
.(k.()) ? : v,
])
);
}
Consent Management
type ConsentPurpose = 'analytics' | 'marketing' | 'personalization' | 'functional';
interface ConsentRecord {
userId: string;
purposes: Record<ConsentPurpose, boolean>;
grantedAt: Date;
ipAddress: string;
userAgent: string;
consentVersion: string;
}
async function recordConsent(
userId: string,
purposes: Record<ConsentPurpose, boolean>,
req: Request
): Promise<void> {
await db.insert(consentRecords).values({
userId,
purposes,
grantedAt: new Date(),
ipAddress: req.ip,
userAgent: req.headers['user-agent'],
consentVersion: CURRENT_CONSENT_VERSION,
});
}
(): <> {
latest = db...({
: (consentRecords., userId),
: (consentRecords.),
});
latest?.[purpose] === ;
}
() {
(! (userId, )) ;
analytics.(event);
}
Data Subject Access Request (DSAR)
async function generateDSAR(userId: string): Promise<DSARPackage> {
const [user, orders, events, sessions, consentHistory] = await Promise.all([
db.query.users.findFirst({ where: eq(users.id, userId) }),
db.query.orders.findMany({ where: eq(orders.userId, userId) }),
db.query.events.findMany({
where: and(eq(events.userId, userId), gt(events.createdAt, subDays(new Date(), 730))),
}),
db.query.sessions.findMany({ where: eq(sessions.userId, userId) }),
db.query.consentRecords.findMany({ where: eq(consentRecords.userId, userId) }),
]);
{
: (),
: {
: user,
consentHistory,
},
: orders,
: events,
};
}
Data Minimization Checklist
Anti-Patterns
Logging PII Directly
Wrong:
logger.info('User logged in', { email: user.email, ip: req.ip, name: user.fullName });
Correct:
logger.info('User logged in', { userId: user.id, country: req.geoCountry });
Why: Log aggregators typically retain data far longer than your GDPR retention policy allows, and PII in logs is nearly impossible to erasure-comply with.
Using Email as Primary Key
Wrong:
CREATE TABLE users (
email TEXT PRIMARY KEY,
name TEXT NOT NULL
);
Correct:
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL
);
Why: Email is PII that must be erasable; if it's a primary key, every FK reference across the schema must also be updated or nulled during erasure, making Right to Erasure exponentially harder to implement correctly.
Collecting Consent Once and Never Re-Collecting After Policy Changes
Wrong:
const consented = await db.consentRecords.findFirst({ where: { userId } });
if (!consented) return false;
Correct:
const CURRENT_CONSENT_VERSION = '2024-06-01';
const consented = await db.consentRecords.findFirst({
where: { userId, consentVersion: CURRENT_CONSENT_VERSION, purposes: { analytics: true } },
orderBy: { grantedAt: 'desc' },
});
return !!consented;
Why: GDPR requires fresh consent whenever the purpose or scope of data processing materially changes — old consent does not cover new processing purposes.
Deleting the User Row Without Cascading to Processors
Wrong:
await db.delete(users).where(eq(users.id, userId));
Correct:
await Promise.allSettled([
stripe.customers.del(user.stripeCustomerId),
intercom.delete('/contacts', { email: user.email }),
sendgridSuppressions.add(user.email),
]);
await db.delete(users).where(eq(users.id, userId));
await db.insert(erasureLog).values({ userId, completedAt: new Date() });
Why: GDPR Art. 17(2) requires you to instruct all processors who received the data to erase it — deleting only your own copy is non-compliant.
Tracking Analytics Without Explicit Consent
Wrong:
export function trackPageView(userId: string, page: string) {
analytics.track({ userId, event: 'page_view', page });
}
Correct:
export async function trackPageView(userId: string, page: string) {
if (!await hasConsent(userId, 'analytics')) return;
analytics.track({ userId, event: 'page_view', page });
}
Why: Processing personal data for analytics without a lawful basis (typically consent for non-essential tracking) is a core GDPR violation and the basis for most DPA fines.
GDPR Compliance Checklist