Security

GDPR compliance engineering: a developer's practical guide.

February 25, 2026 ยท14 min read ยท47Network Engineering

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.


โ† Back to Blog Audit Trail Guide โ†’