Privacy regulations are expanding globally. GDPR in Europe, CCPA in California, PIPEDA in Canada—the list grows every year. But compliance is just the baseline. Good privacy practices build user trust and reduce business risk.
This guide covers practical patterns we apply to every project. They're not expensive or complex—they just require intention.
The core principle: minimize what you collect
The best way to protect data is to not have it in the first place.
Before adding any data collection, ask three questions:
- Why do we need this specific data?
- How long do we need to keep it?
- What's the risk if it's exposed?
If you can't answer all three clearly, don't collect it.
Example: User registration
Naive approach:
interface UserRegistration {
email: string;
password: string;
firstName: string;
lastName: string;
dateOfBirth: Date;
phoneNumber: string;
address: string;
// ... 20 more fields
}
Privacy-first approach:
interface UserRegistration {
email: string; // Required for account access
password: string; // Required for authentication
displayName: string; // User-controlled identity
// Everything else is optional, collected when needed
}
Collect the minimum for core functionality. Add fields only when there's a specific, documented reason.
Pattern 1: Separate PII from operational data
Personal Identifiable Information (PII) should be isolated from operational data. This limits blast radius if either is compromised.
┌─────────────────────────────────────────────────┐
│ Application │
└─────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────────────┐
│ PII Service │ │ Operations Database │
│ (Encrypted) │ │ (user_id references) │
└─────────────────┘ └─────────────────────────┘
│ │
│ - email │ - orders
│ - name │ - preferences
│ - address │ - activity_logs
│ - phone │ - sessions
└───────────────────────┴─────────────────────────┘
Benefits:
- Operations team can debug without accessing PII
- PII access can be strictly audited
- Different retention policies per datastore
- Compliance with data residency requirements
Implementation example
// PII Service - separate database/service
class PIIService {
async createUser(email: string, name: string): Promise<string> {
const userId = generateSecureId();
// Store PII separately
await piiDatabase.users.create({
id: userId,
email: encrypt(email),
name: encrypt(name),
createdAt: new Date(),
});
// Log access
await auditLog.record({
action: "pii_create",
userId,
accessor: getCurrentUser(),
timestamp: new Date(),
});
return userId;
}
async getUser(userId: string, accessor: string): Promise<User> {
await auditLog.record({
action: "pii_read",
userId,
accessor,
timestamp: new Date(),
});
const user = await piiDatabase.users.findById(userId);
return {
id: user.id,
email: decrypt(user.email),
name: decrypt(user.name),
};
}
}
// Operations database only stores references
interface Order {
id: string;
userId: string; // Reference to PII service
items: OrderItem[];
total: number;
status: string;
createdAt: Date;
}
Pattern 2: Encrypt at rest, always
Data at rest should be encrypted, but encryption quality matters.
Application-level encryption (recommended for sensitive fields):
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
class FieldEncryption {
private algorithm = "aes-256-gcm";
encrypt(plaintext: string, key: Buffer): EncryptedField {
const iv = randomBytes(16);
const cipher = createCipheriv(this.algorithm, key, iv);
let encrypted = cipher.update(plaintext, "utf8", "hex");
encrypted += cipher.final("hex");
const authTag = cipher.getAuthTag();
return {
ciphertext: encrypted,
iv: iv.toString("hex"),
authTag: authTag.toString("hex"),
};
}
decrypt(encrypted: EncryptedField, key: Buffer): string {
const decipher = createDecipheriv(
this.algorithm,
key,
Buffer.from(encrypted.iv, "hex")
);
decipher.setAuthTag(Buffer.from(encrypted.authTag, "hex"));
let decrypted = decipher.update(encrypted.ciphertext, "hex", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
}
}
Key management: Never hardcode encryption keys. Use:
- AWS KMS, GCP KMS, or Azure Key Vault for cloud deployments
- HashiCorp Vault for self-managed infrastructure
- Environment variables as a minimum (not ideal but better than hardcoding)
Pattern 3: Comprehensive audit logging
Every access to sensitive data should be logged. This isn't just for compliance—it's essential for incident response.
interface AuditLogEntry {
timestamp: Date;
action: "read" | "write" | "delete" | "export";
resource: string;
resourceId: string;
accessor: {
userId: string;
role: string;
ipAddress: string;
userAgent: string;
};
context: {
reason?: string; // Why was this access needed?
ticketId?: string; // Support ticket reference
};
outcome: "success" | "denied" | "error";
}
class AuditLogger {
async log(entry: Omit<AuditLogEntry, "timestamp">) {
const fullEntry: AuditLogEntry = {
...entry,
timestamp: new Date(),
};
// Write to immutable log (S3, dedicated logging service)
await this.writeToImmutableStore(fullEntry);
// Alert on suspicious patterns
await this.checkAlertConditions(fullEntry);
}
private async checkAlertConditions(entry: AuditLogEntry) {
// Alert if bulk export
if (entry.action === "export") {
await this.alert("Data export detected", entry);
}
// Alert if unusual access time
const hour = entry.timestamp.getHours();
if (hour < 6 || hour > 22) {
await this.alert("After-hours data access", entry);
}
}
}
What to log:
- Who accessed the data (user ID, role)
- What data was accessed (resource type, ID)
- When (timestamp)
- Where (IP address, user agent)
- Why (business reason if available)
- Outcome (success, denied, error)
What NOT to log:
- The actual sensitive data values
- Credentials or tokens
Pattern 4: Implement data retention policies
Data should have an expiration date. Implement automated cleanup.
interface RetentionPolicy {
dataType: string;
retentionDays: number;
action: "delete" | "anonymize" | "archive";
}
const policies: RetentionPolicy[] = [
{ dataType: "session_logs", retentionDays: 30, action: "delete" },
{ dataType: "analytics_events", retentionDays: 365, action: "anonymize" },
{ dataType: "user_data", retentionDays: 730, action: "archive" },
{ dataType: "audit_logs", retentionDays: 2555, action: "archive" }, // 7 years
];
class RetentionEnforcer {
async enforceAll() {
for (const policy of policies) {
await this.enforce(policy);
}
}
private async enforce(policy: RetentionPolicy) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - policy.retentionDays);
switch (policy.action) {
case "delete":
await this.deleteOlderThan(policy.dataType, cutoffDate);
break;
case "anonymize":
await this.anonymizeOlderThan(policy.dataType, cutoffDate);
break;
case "archive":
await this.archiveOlderThan(policy.dataType, cutoffDate);
break;
}
await this.logRetentionAction(policy, cutoffDate);
}
}
Run retention jobs regularly (daily or weekly) and monitor their execution.
Pattern 5: Handle data subject requests
GDPR, CCPA, and PIPEDA all grant individuals rights over their data. Build these capabilities from the start.
Right to access (data export)
class DataExporter {
async exportUserData(userId: string): Promise<ExportPackage> {
// Gather data from all services
const [profile, orders, preferences, activityLogs] = await Promise.all([
this.piiService.getUserProfile(userId),
this.orderService.getUserOrders(userId),
this.preferenceService.getUserPreferences(userId),
this.activityService.getUserActivity(userId),
]);
// Format in portable format
return {
exportDate: new Date().toISOString(),
user: profile,
orders: orders,
preferences: preferences,
activity: activityLogs,
metadata: {
format: "json",
version: "1.0",
retentionPolicy: "See privacy policy for details",
},
};
}
}
Right to deletion (data erasure)
class DataEraser {
async eraseUserData(userId: string): Promise<ErasureReport> {
const report: ErasureReport = {
userId,
requestedAt: new Date(),
actions: [],
};
// Delete from all services
const deletions = [
this.piiService.deleteUser(userId),
this.orderService.anonymizeOrders(userId),
this.preferenceService.deletePreferences(userId),
this.activityService.deleteActivity(userId),
];
const results = await Promise.allSettled(deletions);
results.forEach((result, index) => {
report.actions.push({
service: ["pii", "orders", "preferences", "activity"][index],
status: result.status === "fulfilled" ? "completed" : "failed",
error: result.status === "rejected" ? result.reason.message : undefined,
});
});
// Audit log the erasure
await this.auditLog.record({
action: "data_erasure",
userId,
report,
});
return report;
}
}
Quick implementation checklist
For every new project:
- [ ] Document what data you collect and why
- [ ] Separate PII from operational data
- [ ] Encrypt sensitive fields at application level
- [ ] Encrypt all databases at rest
- [ ] Implement TLS for all connections
- [ ] Set up comprehensive audit logging
- [ ] Define retention policies per data type
- [ ] Build data export capability
- [ ] Build data deletion capability
- [ ] Review access controls quarterly
- [ ] Run privacy impact assessment for new features
The business case for privacy
Beyond compliance, privacy-first design provides:
- Reduced breach impact: Less data means smaller blast radius
- Simpler compliance: Same patterns work across GDPR, CCPA, PIPEDA
- User trust: Privacy-conscious users increasingly choose privacy-respecting products
- Lower costs: Less data to store, process, and protect
Privacy isn't a constraint on your product—it's a competitive advantage.
Building a system that handles sensitive data? Let's discuss how we can help you get the architecture right from the start.