| name | compliance-patterns |
| description | GDPR data handling, audit logging, data classification, retention policies, and consent management for regulatory compliance. |
Compliance Patterns
Data governance and regulatory compliance patterns for software systems.
Data Classification
enum DataClass {
PUBLIC = 'public',
INTERNAL = 'internal',
CONFIDENTIAL = 'confidential',
RESTRICTED = 'restricted',
}
interface UserRecord {
id: string
email: string
displayName: string
passwordHash: string
dateOfBirth: string
preferences: object
createdAt: Date
}
const ENCRYPTED_FIELDS: Record<string, DataClass> = {
'user.email': DataClass.CONFIDENTIAL,
'user.dateOfBirth': DataClass.RESTRICTED,
'user.ssn': DataClass.RESTRICTED,
'payment.cardNumber': DataClass.RESTRICTED,
}
function shouldEncryptAtRest(fieldPath: string): boolean {
const classification = ENCRYPTED_FIELDS[fieldPath]
return classification === DataClass.RESTRICTED
}
function shouldMaskInLogs(fieldPath: string): boolean {
const classification = ENCRYPTED_FIELDS[fieldPath]
return classification === DataClass.CONFIDENTIAL || classification === DataClass.RESTRICTED
}
Audit Logging
interface AuditEvent {
id: string
timestamp: string
actor: {
id: string
type: 'user' | 'system' | 'admin'
ip?: string
}
action: string
resource: {
type: string
id: string
}
changes?: {
field: string
oldValue: unknown
newValue: unknown
}[]
metadata?: Record<string, unknown>
result: 'success' | 'failure' | 'denied'
reason?: string
}
class AuditLogger {
constructor(private store: AuditStore) {}
async log(event: Omit<AuditEvent, 'id' | 'timestamp'>): Promise<void> {
const auditEvent: AuditEvent = {
...event,
id: crypto.randomUUID(),
timestamp: new Date().toISOString(),
changes: event.changes?.map(c => ({
...c,
oldValue: shouldMaskInLogs(c.field) ? '[REDACTED]' : c.oldValue,
newValue: shouldMaskInLogs(c.field) ? '[REDACTED]' : c.newValue,
})),
}
await this.store.append(auditEvent)
}
}
function auditMiddleware(auditLogger: AuditLogger) {
return async (req: Request, res: Response, next: NextFunction) => {
const originalJson = res.json.bind(res)
res.json = function(body: any) {
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
auditLogger.log({
actor: { id: req.user?.id ?? 'anonymous', type: 'user', ip: req.ip },
action: `${req.method.toLowerCase()}.${req.path}`,
resource: { type: req.path.split('/')[2], id: req.params.id ?? 'N/A' },
result: res.statusCode < 400 ? 'success' : 'failure',
}).catch(err => console.error('Audit log failed:', err))
}
return originalJson(body)
}
next()
}
}
GDPR Data Subject Rights
async function exportUserData(userId: string): Promise<DataExport> {
const user = await db.user.findUnique({ where: { id: userId } })
const orders = await db.order.findMany({ where: { userId } })
const activityLog = await db.activityLog.findMany({ where: { userId } })
const consents = await db.consent.findMany({ where: { userId } })
return {
exportDate: new Date().toISOString(),
subject: { id: userId, email: user?.email },
personalData: {
profile: sanitizeForExport(user),
orders: orders.map(sanitizeForExport),
activity: activityLog,
consents,
},
format: 'JSON',
version: '1.0',
}
}
async function deleteUserData(userId: string): Promise<DeletionReport> {
const report: DeletionReport = { userId, deletedAt: new Date(), items: [] }
await db.user.update({
where: { id: userId },
data: { email: `deleted_${userId}@deleted.local`, deletedAt: new Date() }
})
report.items.push({ type: 'user_profile', action: 'anonymized' })
const { count } = await db.activityLog.deleteMany({ where: { userId } })
report.items.push({ type: 'activity_logs', action: 'deleted', count })
await db.order.updateMany({
where: { userId },
data: { customerName: '[DELETED]', customerEmail: '[DELETED]' }
})
report.items.push({ type: 'orders', action: 'anonymized_pii' })
await auditLogger.log({
actor: { id: userId, type: 'user' },
action: 'user.data.erased',
resource: { type: 'user', id: userId },
result: 'success',
})
return report
}
Consent Management
interface ConsentRecord {
userId: string
purpose: string
granted: boolean
grantedAt: Date
expiresAt?: Date
source: string
version: string
}
async function checkConsent(userId: string, purpose: string): Promise<boolean> {
const consent = await db.consent.findFirst({
where: {
userId,
purpose,
granted: true,
OR: [
{ expiresAt: null },
{ expiresAt: { gt: new Date() } },
],
}
})
return consent !== null
}
async function sendMarketingEmail(userId: string): Promise<void> {
const hasConsent = await checkConsent(userId, 'marketing')
if (!hasConsent) {
console.log(`Skipping marketing email for ${userId}: no consent`)
return
}
await emailService.send(userId, marketingTemplate)
}
Retention Policies
const RETENTION_POLICIES: RetentionPolicy[] = [
{ dataType: 'session_logs', retentionDays: 90, action: 'delete' },
{ dataType: 'audit_logs', retentionDays: 2555, action: 'archive' },
{ dataType: 'user_activity', retentionDays: 365, action: 'anonymize' },
{ dataType: 'deleted_accounts', retentionDays: 30, action: 'purge' },
]
async function enforceRetention(): Promise<RetentionReport> {
const report: RetentionReport = { executedAt: new Date(), actions: [] }
for (const policy of RETENTION_POLICIES) {
const cutoffDate = new Date()
cutoffDate.setDate(cutoffDate.getDate() - policy.retentionDays)
const count = await executeRetentionAction(policy.dataType, cutoffDate, policy.action)
report.actions.push({
dataType: policy.dataType,
action: policy.action,
recordsAffected: count,
cutoffDate,
})
}
return report
}
Checklist
Anti-Patterns
- Logging PII in application logs (searchable by anyone with log access)
- Hard-deleting without audit trail (no proof of compliance)
- Single "I agree" checkbox for all purposes (GDPR requires granular consent)
- Retaining data indefinitely "just in case" (violates data minimization)
- Consent version not tracked: consent invalidated when terms change
- No data inventory: unable to locate all PII for subject access requests