Most GDPR guides are written by lawyers for lawyers. This one is written for the engineers who have to implement the technical controls. GDPR isn't primarily a legal problem โ it's a data architecture problem. The regulation requires that you know what personal data you have, where it is, who can access it, why you have it, and how to delete it on request. None of those requirements are satisfied by a privacy policy. They're satisfied by data mapping, access controls, audit logging, and deletion pipelines. This post covers the engineering patterns that make compliance defensible, not just documented.
Data mapping: know what you actually have
Before you can implement any technical controls, you need a data inventory. For each personal data category, document: what data, which database table and column, which service owns it, the lawful basis for processing, the retention period, and which third parties receive it. Keep this in version control alongside your code โ it's a living document that should be updated whenever a new field is added:
# data-map.yaml โ commit this to your infrastructure repo
personal_data:
users:
table: users
service: auth-service
fields:
email:
type: contact
lawful_basis: contract # Required for account creation
retention_days: 1095 # 3 years after account deletion
third_parties: [sendgrid] # Used for transactional email
ip_address_last_login:
type: technical
lawful_basis: legitimate_interest
retention_days: 90
third_parties: []
full_name:
type: identity
lawful_basis: contract
retention_days: 1095
third_parties: []
order_history:
table: orders
service: orders-service
fields:
billing_address:
type: contact
lawful_basis: contract
retention_days: 2555 # 7 years for accounting obligations
third_parties: [stripe, courier-api]
Lawful basis: get this right first
Every processing activity needs a lawful basis. The most common in product development:
- Contract (Art. 6(1)(b)): processing necessary to fulfil a contract with the data subject. This covers account data, order data, payment data. Does not cover marketing.
- Legitimate interests (Art. 6(1)(f)): your interests must be balanced against the data subject's rights. Covers security logging, fraud prevention, and some analytics. Requires a Legitimate Interests Assessment (LIA) document.
- Consent (Art. 6(1)(a)): freely given, specific, informed, unambiguous. Required for marketing email, tracking cookies, and optional analytics. Must be as easy to withdraw as to give.
- Legal obligation (Art. 6(1)(c)): tax records, anti-money-laundering data. You're required to keep this โ which is also why "right to erasure" doesn't always apply.
Legitimate interests is not a catch-all. Many teams default to "legitimate interests" for everything they can't articulate a clear basis for. Regulators are aware of this pattern. If you can't write a two-paragraph LIA explaining exactly why your interest overrides the data subject's privacy rights for a specific processing activity, use a different basis or don't collect the data.
Right to erasure: implementing it correctly
The right to erasure (Art. 17) requires you to delete personal data on request โ but only where no overriding legal obligation to retain it exists. The engineering challenge: personal data is rarely in one place. It's in your primary database, your analytics platform, your email provider, your backups, your search index, and your logs.
// erasure-pipeline.ts โ orchestrate deletion across all systems
interface ErasureRequest {
userId: string;
requestedAt: Date;
verifiedAt: Date;
}
async function executeErasure(req: ErasureRequest): Promise {
const report: ErasureReport = { userId: req.userId, steps: [] };
// 1. Pseudonymise rather than delete where retention is legally required
// Orders must be kept for 7 years (legal obligation) โ replace PII with a token
await db.query(`
UPDATE orders
SET billing_name = '[REDACTED]',
billing_email = $1,
billing_address = '[REDACTED]'
WHERE user_id = $2
AND created_at > NOW() - INTERVAL '7 years'
`, [`erased-${req.userId}@deleted.invalid`, req.userId]);
report.steps.push({ system: 'orders-db', action: 'pseudonymised', status: 'ok' });
// 2. Delete where no retention obligation exists
await db.query('DELETE FROM users WHERE id = $1', [req.userId]);
report.steps.push({ system: 'users-db', action: 'deleted', status: 'ok' });
// 3. Delete from third-party processors
await sendgridClient.deleteContact(req.userId);
report.steps.push({ system: 'sendgrid', action: 'deleted', status: 'ok' });
// 4. Remove from search indexes
await searchIndex.delete({ userId: req.userId });
report.steps.push({ system: 'search', action: 'deleted', status: 'ok' });
// 5. Backups cannot be selectively modified โ document the obligation
// When the backup retention period expires, the data will be gone
report.backupNote = 'Data remains in encrypted backups until rotation at 90 days';
// 6. Audit the erasure itself
await auditLog.record({
event: 'user.erasure.completed',
userId: req.userId,
requestId: req.requestedAt.toISOString(),
steps: report.steps,
});
return report;
}
Consent management: technical implementation
-- Consent records: immutable log of every grant and withdrawal
CREATE TABLE consent_records (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
purpose TEXT NOT NULL, -- 'marketing_email', 'analytics', etc.
granted BOOLEAN NOT NULL,
granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
source TEXT NOT NULL, -- 'signup_form', 'preferences_page', etc.
ip_address INET,
user_agent TEXT
);
-- Never UPDATE consent records โ always INSERT a new row
-- To check current consent: get the most recent record for that purpose
CREATE VIEW current_consent AS
SELECT DISTINCT ON (user_id, purpose)
user_id, purpose, granted, granted_at
FROM consent_records
ORDER BY user_id, purpose, granted_at DESC;
Data minimisation at the schema level
The easiest personal data to protect is data you never collect. Review every table column and every API response field: does this need to exist? Common targets for removal or pseudonymisation: full IP addresses (often only the /24 prefix is needed), precise geolocation (city-level is usually sufficient), full user agents, and device identifiers beyond what's needed for session management.
47Network products are built with GDPR as a design constraint. PassVault's zero-knowledge architecture means we literally cannot access vault contents โ not a policy decision, a cryptographic one. 47mail's delivery logs store recipient domains but not full email addresses by default. The Trust page documents the specific data handling decisions across our product range. The fintech zero-trust engagement included a full GDPR audit as part of the zero-trust infrastructure overhaul.