Most GDPR articles are written for lawyers and compliance officers. They tell you that you need a “lawful basis” and that data must be “processed securely”, then leave you staring at your codebase wondering which migration, which cron job and which middleware you are supposed to write on Monday morning.
This GDPR compliance checklist for developers takes the opposite approach. Every item below answers three questions: what to build, where it usually breaks in real projects, and how to verify it before launch. No legal theory, no fine-print recitals. Just the technical work that regulators, auditors and your own security team will eventually ask about.
Note: this is engineering guidance, not legal advice. Your DPO or legal counsel still decides your lawful bases and retention periods. Your job is to implement them correctly.
The 18 requirements at a glance
| # | Requirement | Layer | Typical effort |
|---|---|---|---|
| 1 | Personal data inventory generated from the schema | Data model | 1 to 2 days |
| 2 | Lawful basis recorded per processing activity | Config | Half a day |
| 3 | Transparency at the point of collection | Frontend | 1 day |
| 4 | Block non-essential scripts before consent | Frontend | 2 to 4 days |
| 5 | Consent UI with symmetric accept and reject | Frontend | 1 to 2 days |
| 6 | Versioned consent proof storage | Backend | 1 day |
| 7 | Data minimization in forms, APIs and logs | Full stack | Ongoing |
| 8 | Encryption in transit, HSTS, secure cookie flags | Infra | Half a day |
| 9 | Encryption at rest with managed keys | Infra / DB | 2 to 5 days |
| 10 | Pseudonymization, hashing and IP truncation | Backend | 1 to 3 days |
| 11 | Automated retention and purge jobs | Backend | 3 to 5 days |
| 12 | Data export endpoint (access and portability) | Backend | 3 to 5 days |
| 13 | Right to erasure endpoint with cascade rules | Backend | 5 to 10 days |
| 14 | Rectification and restriction of processing | Backend | 2 to 3 days |
| 15 | Audit logging of personal data access | Backend | 3 to 5 days |
| 16 | Least privilege access and clean non-prod data | Infra / process | 2 to 5 days |
| 17 | Processor register, DPAs and transfer safeguards | Process | 1 to 2 days |
| 18 | Breach detection and 72 hour notification readiness | Ops | 2 to 4 days |

Part 1: Know what you actually hold
1. Generate a personal data inventory from your schema
What to build. A machine-readable map of every column, document field, log stream and cache key that contains personal data. Do not maintain it in a spreadsheet that dies after two sprints. Annotate it in the code itself so it stays in sync. Related reading: The Only GDPR Compliance Checklist You’ll Ever Need.
# data_classification.yml
users:
email: { pii: true, category: contact, retention: account_lifetime + 30d }
password_hash:{ pii: true, category: credential, retention: account_lifetime }
last_login_ip:{ pii: true, category: network, retention: 90d }
locale: { pii: false }
orders:
shipping_address: { pii: true, category: contact, retention: 10y_legal_invoice }
Where it breaks. Teams map the primary database and forget the periphery: Redis sessions, Elasticsearch indexes, S3 uploads, webhook payloads stored for debugging, CSV exports in a shared drive, and the analytics warehouse that quietly replicates everything.
How to verify. Run a grep for email-like patterns across your object storage and log aggregator. If you find personal data in a system that is not in your inventory, the inventory is wrong. Add a CI check that fails when a new migration adds a column without a classification entry.
2. Record a lawful basis for every processing activity
What to build. Each feature that touches personal data should be tied to one of the six lawful bases (consent, contract, legal obligation, vital interests, public task, legitimate interests). Keep it as configuration next to the feature, not in a Confluence page.
This matters technically because the basis determines the code path:
- Consent requires a consent check before execution and a full withdrawal path.
- Contract requires no consent gate but strict purpose limitation.
- Legal obligation usually overrides deletion requests, so your erasure logic must know about it.
Where it breaks. Marketing emails sent under “legitimate interests” while the frontend still shows a consent checkbox, producing two contradictory sources of truth. Or a soft-deleted user who keeps receiving newsletters because the mailing list lives in a third-party tool.
How to verify. Pick three features at random and trace the lawful basis from the config file to the actual runtime check. If the check does not exist in code, it does not exist.
3. Put transparency at the point of collection
What to build. Every form that collects personal data needs a visible, contextual explanation of purpose and retention, linked to the full privacy notice. Version your privacy notice and expose the version in the page markup so you can correlate it with stored consent.
Where it breaks. The signup form is compliant, but the newsletter modal, the support chat widget, the abandoned-cart email capture and the in-app survey are not. Each was shipped by a different team.
How to verify. Crawl your own site and list every <form> and third-party widget that accepts input. Match each one against your inventory. Anything unmatched is a gap.

Part 2: Cookie consent that actually holds up
4. Block non-essential scripts before consent, not after
This is the single most common failure in real audits. A banner that appears while Google Analytics, Meta Pixel and a heatmap tool have already fired is not consent, it is decoration.
What to build. Load tags in a blocked state and release them only on an explicit opt-in event.
<!-- Blocked by default: type is not executable -->
<script type="text/plain" data-consent-category="analytics"
src="https://cdn.example.com/analytics.js"></script>
<script>
window.addEventListener('consent:updated', (e) => {
const granted = e.detail.categories; // ['necessary','analytics']
document
.querySelectorAll('script[type="text/plain"][data-consent-category]')
.forEach((node) => {
if (!granted.includes(node.dataset.consentCategory)) return;
const s = document.createElement('script');
if (node.src) s.src = node.src; else s.textContent = node.textContent;
node.replaceWith(s);
});
});
</script>
If you use a tag manager, drive everything through consent signals and make the default state denied for analytics, ad storage and personalization.
Where it breaks.
- Server-side rendered pages inject tracking in the HTML head before hydration.
- Embedded YouTube, Google Maps and Intercom iframes set cookies on load. Use placeholder components with click-to-load.
- A/B testing and session replay tools are classified as “necessary” because they “help the product”. They are not.
- Prefetch and preconnect hints to tracking domains still open the connection.
How to verify. Open a fresh incognito window, load the page, and before touching the banner inspect the Application tab: only strictly necessary cookies and local storage keys should exist. Then check the Network tab for requests to third-party domains. Automate it:
// playwright pre-consent audit
const thirdParty = [];
page.on('request', r => {
const host = new URL(r.url()).hostname;
if (!host.endsWith('coding4.net')) thirdParty.push(host);
});
await page.goto('https://coding4.net/', { waitUntil: 'networkidle' });
const cookies = await context.cookies();
expect(cookies.filter(c => !ALLOWED.includes(c.name))).toHaveLength(0);
expect(thirdParty.filter(h => TRACKERS.includes(h))).toHaveLength(0);
Run this test in CI on every deploy. Consent regressions are almost always introduced by a marketing tag added after launch.
5. Make rejecting as easy as accepting
What to build. A first-layer banner with Accept all and Reject all as equally prominent buttons, plus a link to granular settings. No pre-ticked boxes. No “legitimate interest” toggles that stay on for advertising purposes. A persistent way to reopen settings and withdraw consent at any time (a footer link or a small floating button).
Where it breaks. Reject hidden behind “Manage preferences”, a greyed-out reject button, a cookie wall on a site with no alternative, or a banner that reappears on every page because the consent cookie is scoped to the wrong path or domain.
How to verify. Click Reject all, then reload the page five times across three different pages. No analytics requests, no banner reappearance, and the preference must survive a browser restart. Then click withdraw and confirm the previously set third-party cookies are actively deleted, not just ignored.
6. Store consent proof, versioned and timestamped
What to build. A consent record is evidence. Store it server-side, append-only.
CREATE TABLE consent_events (
id BIGSERIAL PRIMARY KEY,
subject_key TEXT NOT NULL, -- user id or pseudonymous visitor id
categories JSONB NOT NULL, -- {"analytics":true,"ads":false}
policy_version TEXT NOT NULL, -- '2026-04-11'
ui_version TEXT NOT NULL, -- banner build hash
method TEXT NOT NULL, -- 'banner_accept_all' | 'settings_save' | 'withdrawal'
ip_truncated INET, -- 203.0.113.0
user_agent_hash TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Where it breaks. Consent lives only in a browser cookie, so it disappears when the user clears storage and you can prove nothing. Or you overwrite the previous row on each change and lose the history.
How to verify. Pick a random user and reconstruct their full consent timeline. If you cannot answer “what exactly did this person agree to, on which banner version, on which date”, the implementation is incomplete.

Part 3: Security requirements you can measure
7. Enforce data minimization at the API layer
What to build. Explicit allow-lists in serializers instead of dumping model objects. Drop fields you do not need. Stop asking for date of birth, gender or phone number when the feature does not use them.
// Bad: leaks everything the model grows in the future
res.json(user);
// Good: explicit projection
res.json({ id: user.id, displayName: user.displayName, avatarUrl: user.avatarUrl });
Where it breaks. Logging. Request bodies, stack traces and error monitoring payloads regularly contain emails, tokens and full addresses, then get shipped to a third-party SaaS and retained for a year.
How to verify. Add a redaction layer to your logger and a test that posts a payload containing a known email, then asserts the string never appears in the log sink or in the error tracker sample events.
8. Encryption in transit, done properly
What to build. TLS 1.2 minimum with TLS 1.3 preferred, HTTP to HTTPS redirect, HSTS with a long max-age, and cookies with Secure, HttpOnly and an appropriate SameSite. Internal service-to-service traffic counts too, including database connections and queue workers.
Where it breaks. The public site is perfect while the admin panel runs over plain HTTP on an internal VLAN, or a background worker connects to Postgres with sslmode=disable.
How to verify. Run an external TLS scan for the public endpoints, then SELECT ssl FROM pg_stat_ssl or the equivalent for every connected client. Look for the one worker that is not encrypted. The piece An easy-to-follow GDPR compliance checklist makes a good next read.
9. Encryption at rest that means something
What to build. Three layers, in increasing order of value:
- Volume or disk encryption on database and object storage. Cheap, mandatory, but protects only against stolen hardware.
- Backup and snapshot encryption with separate key material. Backups are the most frequently forgotten copy of your production data.
- Column-level or application-level encryption for special category data (health, biometrics, political opinions) and high-risk fields (national ID, bank details), with keys held in a KMS or vault, never in the same database.
-- Application-level envelope encryption pattern
ALTER TABLE patients
ADD COLUMN diagnosis_ciphertext BYTEA,
ADD COLUMN diagnosis_dek_id TEXT, -- reference to data key in KMS
ADD COLUMN diagnosis_nonce BYTEA;
Plan key rotation from day one: store a key identifier alongside each ciphertext so you can re-encrypt progressively instead of in one giant migration.
Where it breaks. Read replicas, staging snapshots, nightly dumps written to an unencrypted bucket, and search indexes that store the plaintext of an encrypted column so it stays queryable. If you need to search encrypted data, use deterministic encryption or a blind index, and understand the trade-off.
How to verify. Ask for a copy of last night’s backup and try to read a sensitive column without touching the KMS. If you can, the encryption is theatre.
10. Pseudonymize and truncate aggressively
What to build. Data you never store cannot leak. Practical wins:
- Truncate IPv4 to /24 and IPv6 to /48 in analytics and access logs.
- Replace user IDs with rotating pseudonyms in event pipelines and warehouses.
- Hash identifiers with a salted, slow hash when you only need equality checks. A plain SHA-256 of an email is still personal data, it is trivially reversible by dictionary attack.
- Keep the re-identification mapping table in a separate schema with its own access controls.
Where it breaks. Teams call something “anonymous” when it is only pseudonymous. Truly anonymous data is out of GDPR scope, pseudonymous data is not. If any key anywhere can reverse it, the whole dataset stays in scope.
How to verify. Take a sample export from your analytics store and try to link a row back to a named user using only data available to that system’s users. If you succeed, it is pseudonymous, and it needs the same protections as the rest.
11. Automate retention instead of documenting it
What to build. A retention policy is only real when a scheduled job enforces it. Define a period per data category, then implement a purge worker with a dry-run mode and metrics.
| Data | Typical period | Action at expiry |
|---|---|---|
| Web server access logs | 30 to 90 days | Hard delete |
| Inactive account | 24 to 36 months | Notify then anonymize |
| Support tickets | 2 to 3 years | Anonymize requester |
| Invoices and accounting records | 6 to 10 years (national law) | Retain, restricted access |
| Marketing consent records | Consent lifetime + limitation period | Retain as proof |
| CV and applicant data | 6 to 24 months | Delete unless re-consented |
Where it breaks. The job exists for the main table but not for the audit trail, the soft-deleted rows, the S3 attachments, the search index or the data warehouse. Also: purge jobs that silently fail for six months because nobody alerts on “zero rows deleted”.
How to verify. Emit a metric per purge run (retention.deleted_rows by table) and alert when a job has not run or deleted nothing for two consecutive cycles. Then query for the oldest row in each table and compare it against the policy.
Part 4: Data subject rights as API endpoints
12. Build a real data export endpoint
What to build. One authenticated endpoint that assembles everything you hold about a person in a structured, commonly used, machine-readable format. Run it asynchronously and deliver via a short-lived signed URL.
POST /api/privacy/export -> 202 { "jobId": "exp_01J..." }
GET /api/privacy/export/:jobId -> 200 { "status": "ready", "url": "...", "expiresAt": "..." }
The bundle should include profile, orders, messages, uploaded files, consent history, and derived data such as preference scores. Include a manifest describing each file.
Where it breaks. The export covers the primary database only. Data living in your CRM, help desk, email platform and analytics tool is missing. Also: exports delivered by email as an unprotected attachment, or a signed URL with no expiry.
How to verify. Create a test account, use the product fully for a week (orders, tickets, uploads, newsletter), then run the export and diff it against your data inventory from item 1. Every inventoried system must appear.
13. Ship an erasure endpoint, then handle the exceptions
What to build. Deletion is never a single DELETE FROM users. Design an explicit strategy per table.
// Erasure plan per entity
const ERASURE_PLAN = {
users: 'anonymize', // keep row, scrub identifiers
sessions: 'delete',
uploads: 'delete', // including object storage keys
orders: 'retain', // legal obligation, restrict access
order_contacts: 'anonymize',
audit_logs: 'retain', // integrity requirement
newsletter: 'suppress' // hashed email on suppression list
};
Anonymization in practice: replace email with deleted-{uuid}@invalid, null out name, address and phone, drop avatars, break the link to pseudonymous analytics IDs, and set erased_at.
Where it breaks.
- Backups. You cannot rewrite every snapshot. Document a backup retention window, keep a suppression list, and re-apply erasure automatically after any restore.
- Third parties. Your CRM, email tool and analytics platform each need their own deletion call. Build a fan-out job with retries.
- Foreign keys. Hard deletes explode against constraints, so developers soft-delete and call it done. A soft-deleted row that still contains the email address is not erased.
- Caches, queues and search indexes. A deleted user who still appears in autocomplete is a very visible failure.
How to verify. Automated test: create a fixture user, populate every entity, call the erasure endpoint, then scan all tables, buckets, indexes and stub third-party calls for the fixture’s email, phone and full name. Zero hits outside the explicit retain list.
14. Handle rectification and restriction of processing
What to build. Self-service edit for the fields users can change, a support path for the rest, and a processing_restricted flag that your pipelines actually respect. Restriction means “store it, stop using it”, which is a different code path from deletion.
if (user.processingRestricted) {
// no profiling, no marketing, no enrichment, no export to partners
return skipWithReason('processing_restricted');
}
Where it breaks. The flag is set in the main app but the batch jobs, the recommendation engine and the nightly warehouse sync read the raw table and ignore it.
How to verify. Set the flag on a test account and run every scheduled job. Confirm the account is excluded from each one, and that corrections propagate downstream within your stated timeframe.

Part 5: Access, logging and vendors
15. Audit log every access to personal data
What to build. Application logs tell you what the system did. Audit logs tell you who looked at whose data, when, and why. That is what you need during a breach investigation or a regulator inquiry.
{
"ts": "2026-09-29T14:22:08.412Z",
"actor": { "type": "staff", "id": "u_4821", "role": "support_agent" },
"action": "personal_data.read",
"subject": { "type": "customer", "id": "c_99120" },
"fields": ["email", "shipping_address"],
"context": { "ticket": "ZD-77412", "ip": "198.51.100.0", "request_id": "req_7f3a" },
"result": "allowed"
}
Requirements that matter: append-only storage, a separate sink from application logs, its own retention period (commonly 6 to 24 months), and no personal data values inside the log itself, only field names and identifiers.
Where it breaks. Admin panels that load a full customer list in one query and log a single “page viewed” event, so you cannot tell which records were exposed. Direct database access by engineers through a bastion host with no audit trail at all. Use a database proxy that logs queries by identity.
How to verify. Ask a support agent to look up a random customer. Within one minute you should be able to produce the audit entry showing exactly who did it and which fields were returned. Then confirm nobody, including admins, can edit or delete audit rows.
16. Least privilege and clean non-production environments
What to build. Role-based access with a default of no access to personal data. Time-boxed elevation for support and engineering. Multi-factor authentication for every admin surface. And critically: no production personal data in staging, development or local machines. Use a masking pipeline that generates realistic but fake data.
Where it breaks. “Just restore last night’s prod dump to staging so we can reproduce the bug.” That single command copies your entire customer base into an environment with weak passwords, shared accounts and public URLs.
How to verify. Run your personal data scanner against staging. It should return nothing but synthetic values. Review access lists quarterly and remove anyone who left the team or changed role.
17. Keep a processor register with DPAs and transfer safeguards
What to build. A list, in the repository, of every third party that receives personal data: hosting, CDN, email, analytics, error tracking, chat, payments, AI APIs. For each one record the purpose, data categories, hosting region, signed data processing agreement and transfer mechanism.
# processors.yml
- name: Stripe
purpose: payment processing
data: [email, name, billing_address, card_token]
region: EU + US
dpa: signed 2026-01-14
transfer: SCCs + DPF
- name: Sentry
purpose: error monitoring
data: [ip_truncated, user_id]
region: EU (self-hosted region)
dpa: signed 2025-11-02
transfer: n/a
Pay special attention to LLM and AI features. Sending user content to a model provider is a transfer of personal data, and “we use it to improve our models” in their terms is a processing purpose you probably cannot justify.
Where it breaks. A developer adds a new SaaS SDK in a pull request. There is no DPA, no register entry, and no cookie category. Six months later it appears in an audit.
How to verify. Add a pull request checklist item: “does this change send data to a new third party?” Combine it with a dependency review and a periodic network capture of outbound domains from production.
18. Be ready to detect and report a breach in 72 hours
What to build. The clock starts when you become aware, so detection speed is the whole game. Concretely:
- Alerts on anomalous data access volume (one account exporting 10,000 records at 3am).
- Alerts on failed authorization spikes and on new admin role grants.
- Integrity monitoring on the audit log sink.
- A written runbook with named roles, a severity matrix, and a pre-filled notification template for your supervisory authority.
- The ability to answer, from your audit logs, which data subjects and which categories were affected. Without item 15, you cannot.
Where it breaks. The technical team fixes the incident in two hours and nobody tells the DPO for a week. Or logs have a 7 day retention, so the forensic window is already gone.
How to verify. Run a tabletop exercise once a year. Simulate a leaked API key and time how long it takes to produce a list of affected users. If it takes more than a day, invest in item 15.

Pre-launch verification: the 10 minute smoke test
Before any release that touches personal data, run this sequence. It catches the majority of real-world failures.
- Fresh incognito window, load the homepage, check Application tab: only necessary cookies present.
- Check Network tab: no requests to analytics or ad domains before interaction.
- Click Reject all, reload three pages, confirm no tracking fires and the banner stays closed.
- Accept analytics, then withdraw consent, and confirm the third-party cookies are deleted.
- Query
consent_eventsfor your test session and confirm the record has a policy version and a method. - Trigger an export for a test account and diff the output against the data inventory.
- Trigger erasure for a test account and grep every store for the fixture’s email.
- Confirm the retention job ran in the last cycle and deleted a non-zero number of rows.
- Read a customer record from the admin panel and find the matching audit log entry.
- Confirm staging contains no production personal data.
Turn steps 1 to 4 and 6 to 7 into automated end-to-end tests. Compliance that depends on human memory decays within one quarter.
The five failures we see most often
| Failure | Root cause | Fix |
|---|---|---|
| Trackers fire before consent | Banner added on top of existing tags | Default-denied tag gating + CI test |
| Erasure leaves data in third parties | No processor fan-out | Deletion job with per-vendor adapters |
| Retention policy never executed | Policy written in a document only | Cron job with metrics and alerts |
| Personal data in application logs | Full request body logging | Redaction middleware + log tests |
| Prod data copied to staging | Debugging convenience | Automated masking pipeline |
FAQ
Does GDPR apply if my company is not based in the EU?
Yes, if you offer goods or services to people in the EU or monitor their behaviour, including through analytics and advertising cookies. Location of your servers or your company is not the deciding factor.
Do I need a cookie banner if I only use privacy-friendly analytics?
Cookie rules come from the ePrivacy framework and apply to storing or reading information on a device, regardless of whether it is personal data. If your analytics tool sets no cookies, reads no device identifiers and produces genuinely aggregated data, consent is often not required. Verify the actual network and storage behaviour yourself rather than trusting the marketing page, and confirm the position with your counsel for your specific country.
How fast must I respond to a deletion or access request?
Without undue delay and within one month, extendable by two further months for complex requests. In engineering terms, aim for an automated response under 24 hours so the manual fallback is rarely needed.
What about data in backups when someone requests erasure?
You are not expected to rewrite immutable backups. The accepted approach is to document your backup retention window, inform the requester that data will persist in backups until the window expires, prevent restored data from returning to active use, and maintain a suppression list that is re-applied automatically after any restore.
Is hashing an email address enough to make it anonymous?
No. Hashes are deterministic and the input space of email addresses is small enough to brute force. A hashed email is pseudonymous personal data and stays in scope. Use a salted, slow hash with the salt stored separately if you need equality matching, and treat the result as personal data anyway.
Do I need a DPIA for my project?
A data protection impact assessment is required for processing likely to result in high risk: large-scale profiling, systematic monitoring of public areas, special category data at scale, automated decisions with legal effects. If you are building any of those, run the DPIA before you write the code, not after the launch date is fixed.
Is there a downloadable version of this GDPR compliance checklist for developers?
The 18 items above map one to one onto backlog tickets. Copy the overview table into your issue tracker, attach the verification step as the acceptance criterion for each ticket, and you have a working implementation plan rather than a document that nobody reads.
Where to start if you only have one sprint
If your backlog is full and you need the highest risk reduction per hour of work, do these four first:
- Pre-consent script blocking (item 4). It is the most visible, most frequently sanctioned issue and it is testable in minutes.
- Retention jobs (item 11). Deleting data you no longer need shrinks the blast radius of every future incident.
- Erasure endpoint (item 13). It is the request you will receive most often and the most painful to do by hand.
- Audit logging (item 15). Without it you cannot investigate anything, and you cannot report a breach accurately.
Everything else builds on those four. If you want a second opinion on your implementation, or help turning this checklist into a prioritized technical roadmap for your stack, the team at coding4.net can review your architecture and ship the missing pieces with you.

