GDPR for Software Developers | What You Actually Need to Implement
A developer-focused guide to GDPR compliance. Covers the technical requirements, data handling patterns, and code-level decisions you need to make.
GDPR has been in effect since 2018, but most developer guides still focus on the legal theory. This guide is different. It covers the technical requirements, the code you need to write, and the architectural decisions that make compliance practical instead of painful.
If your software stores, processes, or touches personal data of people in the EU, this applies to you. It doesn’t matter where your company is based.
GDPR Overview for Developers
Skip the 99 articles. Here’s what GDPR means for your codebase:
- Collect only what you need. Don’t store data “just in case.”
- Tell users what you’re doing with their data. And get their permission when required.
- Let users access, export, and delete their data. You need API endpoints for this.
- Keep data secure. Encryption, access controls, audit logs.
- Report breaches fast. You have 72 hours to notify the authorities after discovering a breach.
- Document everything. Your processing activities, your security measures, your data flows.
That’s the practical summary. The rest of this guide shows you how to implement each requirement.
The 7 Key Technical Requirements
1. Consent Management
Consent must be freely given, specific, informed, and unambiguous. Pre-checked boxes don’t count. Bundled consent (“agree to everything”) doesn’t count. Withdrawal must be as easy as granting consent.
What to Build
A consent system needs three components:
- A consent record store. For every user, track what they consented to, when, and how.
- A consent check mechanism. Before processing data for a specific purpose, verify the user has active consent.
- A withdrawal mechanism. Let users revoke consent through your UI, and stop processing immediately.
Database Schema
CREATE TABLE user_consents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
consent_type VARCHAR(100) NOT NULL,
granted BOOLEAN NOT NULL,
granted_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_user_consents_lookup
ON user_consents (user_id, consent_type, granted);
Store the full history. Never delete or overwrite consent records. When a user revokes consent, insert a new row with granted = false and set revoked_at. This gives you an audit trail.
Consent Checking Middleware
Here’s an Express middleware that checks consent before processing a request:
import { Request, Response, NextFunction } from "express";
import { db } from "./database";
interface ConsentRequirement {
type: string;
required: boolean;
}
function requireConsent(consentType: string) {
return async (req: Request, res: Response, next: NextFunction) => {
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ error: "Authentication required" });
}
const consent = await db.query(
`SELECT granted FROM user_consents
WHERE user_id = $1 AND consent_type = $2
ORDER BY created_at DESC
LIMIT 1`,
[userId, consentType]
);
if (!consent.rows[0]?.granted) {
return res.status(403).json({
error: "Consent required",
consentType,
message: `You must grant "${consentType}" consent to use this feature.`,
consentUrl: `/settings/privacy`,
});
}
next();
};
}
// Usage
app.post(
"/api/newsletter/subscribe",
requireConsent("marketing_emails"),
subscribeHandler
);
app.post(
"/api/analytics/track",
requireConsent("usage_analytics"),
trackHandler
);
2. Data Access and Export (Right of Access)
Users have the right to request a copy of all personal data you hold about them. You must provide it in a commonly used, machine-readable format. JSON or CSV works fine.
What to Build
An endpoint that collects all personal data for a user across every table and service, then packages it into a downloadable file.
interface DataExport {
exportedAt: string;
user: {
profile: Record<string, unknown>;
activity: Record<string, unknown>[];
consents: Record<string, unknown>[];
communications: Record<string, unknown>[];
};
}
app.get("/api/me/data-export", authenticate, async (req, res) => {
const userId = req.user.id;
const [profile, activity, consents, communications] = await Promise.all([
db.query("SELECT id, email, name, created_at FROM users WHERE id = $1", [
userId,
]),
db.query(
"SELECT action, metadata, created_at FROM user_activity WHERE user_id = $1 ORDER BY created_at DESC",
[userId]
),
db.query(
"SELECT consent_type, granted, granted_at, revoked_at FROM user_consents WHERE user_id = $1 ORDER BY created_at DESC",
[userId]
),
db.query(
"SELECT type, sent_at, subject FROM communications WHERE user_id = $1 ORDER BY sent_at DESC",
[userId]
),
]);
const exportData: DataExport = {
exportedAt: new Date().toISOString(),
user: {
profile: profile.rows[0],
activity: activity.rows,
consents: consents.rows,
communications: communications.rows,
},
};
res.setHeader("Content-Type", "application/json");
res.setHeader(
"Content-Disposition",
`attachment; filename="data-export-${userId}.json"`
);
res.json(exportData);
});
This endpoint must cover every table that contains user data. Audit your schema thoroughly. A missing table means an incomplete export, which is a compliance failure.
3. Right to Deletion (Right to Be Forgotten)
Users can request that you delete all their personal data. You must comply unless you have a legal obligation to retain it (like tax records or fraud prevention).
What to Build
A deletion endpoint that removes or anonymizes user data across all tables. This is harder than it sounds because of foreign key constraints and data that other systems depend on.
app.delete("/api/me/account", authenticate, async (req, res) => {
const userId = req.user.id;
const client = await db.getClient();
try {
await client.query("BEGIN");
// Anonymize data that must be retained for business records
await client.query(
`UPDATE orders
SET customer_name = 'deleted', customer_email = 'deleted'
WHERE user_id = $1`,
[userId]
);
// Delete data that can be fully removed
await client.query("DELETE FROM user_activity WHERE user_id = $1", [
userId,
]);
await client.query("DELETE FROM user_consents WHERE user_id = $1", [
userId,
]);
await client.query("DELETE FROM communications WHERE user_id = $1", [
userId,
]);
await client.query("DELETE FROM sessions WHERE user_id = $1", [userId]);
// Anonymize the user record instead of deleting
// This preserves referential integrity
await client.query(
`UPDATE users SET
email = 'deleted-' || id || '@removed.invalid',
name = 'Deleted User',
phone = NULL,
address = NULL,
deleted_at = NOW()
WHERE id = $1`,
[userId]
);
await client.query("COMMIT");
// Trigger deletion in external systems
await Promise.allSettled([
emailService.deleteSubscriber(userId),
analyticsService.deleteUser(userId),
searchIndex.removeUser(userId),
]);
res.json({ message: "Account and personal data deleted" });
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
});
Key decisions:
- Delete vs. anonymize. Records needed for accounting (orders, invoices) should be anonymized. Delete everything else.
- External systems. Data sent to third-party services must be deleted there too.
- Timing. GDPR says “without undue delay.” Complete deletion within 30 days. For most systems, make it immediate.
4. Data Minimization
Only collect and store the data you actually need for a stated purpose. If you ask for a phone number but never call users, you shouldn’t be collecting it.
Practical Rules
- Audit every form field. For each field, ask: “What specific feature breaks if we remove this?” If the answer is nothing, remove it.
- Set retention periods. Don’t keep data forever. Define how long each type of data is needed, then auto-delete it.
- Minimize logging. Strip personal data from log entries. Log user IDs, not names or emails.
-- Automatic data retention with PostgreSQL
-- Run this as a scheduled job (e.g., pg_cron)
DELETE FROM user_activity
WHERE created_at < NOW() - INTERVAL '2 years';
DELETE FROM session_logs
WHERE created_at < NOW() - INTERVAL '90 days';
DELETE FROM password_reset_tokens
WHERE created_at < NOW() - INTERVAL '24 hours';
5. Encryption
GDPR requires “appropriate technical measures” to protect personal data. Encryption is the most important.
At Rest
- Encrypt your database disk. All major cloud providers support this. Enable it and verify.
- For highly sensitive fields (SSNs, health data), add application-level encryption on top.
- Encrypt backups. An unencrypted backup is a breach waiting to happen.
In Transit
- TLS everywhere. Every connection between services, databases, and users. No exceptions.
- Enforce HTTPS. Redirect HTTP. Set HSTS headers.
- Use TLS for database connections. PostgreSQL supports this natively.
// PostgreSQL connection with TLS
import { Pool } from "pg";
const pool = new Pool({
host: process.env.DB_HOST,
port: 5432,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
ssl: {
rejectUnauthorized: true,
ca: fs.readFileSync("/path/to/server-ca.pem").toString(),
},
});
6. Breach Notification
If personal data is compromised, you must notify the relevant Data Protection Authority within 72 hours. If the breach poses a high risk to individuals, you must also notify the affected users.
What to Build
- Audit logging. Track every access to personal data. Who accessed it, when, and from where.
- Anomaly detection. Alert on unusual access patterns (bulk data exports, access from new IPs, access outside business hours).
- An incident response plan. Document who does what when a breach is detected. This is not code. It’s a checklist your team practices.
CREATE TABLE data_access_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
accessed_by UUID NOT NULL,
access_type VARCHAR(50) NOT NULL,
resource_type VARCHAR(100) NOT NULL,
resource_id UUID,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_data_access_log_user
ON data_access_log (user_id, created_at);
CREATE INDEX idx_data_access_log_accessor
ON data_access_log (accessed_by, created_at);
7. Privacy by Design
GDPR says privacy should be built into systems from the start, not bolted on afterward. In practice, this means making privacy the default.
- Default to private. New features should collect minimal data and require opt-in for anything beyond the core function.
- Settings default to the most private option. Users who never touch their settings should have the highest privacy protection.
- Separate concerns. Don’t mix analytics data with functional data. Don’t reuse auth tokens for tracking.
Anonymization vs. Pseudonymization
These are not the same thing, and the distinction matters.
- Pseudonymization replaces identifying information with a reversible token. Example: hashing email addresses. If you have the hash function and original email, you can re-identify the person. GDPR still applies because re-identification is possible.
- Anonymization removes identifying information permanently. Example: aggregate analytics (“1,247 users visited the pricing page”) with no way to identify which users. GDPR does not apply to truly anonymized data.
True anonymization is hard. If your “anonymous” dataset includes a timestamp, a city, and a device type, that combination might uniquely identify someone. Be conservative.
Cookie Consent
If your website uses cookies beyond what’s strictly necessary, you need consent before setting them.
Requires consent: analytics cookies, advertising pixels, social media widgets, any third-party tracking script.
Does not require consent: session cookies, shopping cart cookies, CSRF tokens, the cookie consent preference cookie itself.
Your consent banner should block non-essential cookies until consent is given, offer granular choices, and make “reject all” as easy as “accept all.” Don’t build this from scratch. Tools like Cookiebot handle the complexity. The key rule: no tracking scripts fire before consent is granted.
Third-Party Data Processors
Every third-party service handling your users’ data is a “data processor” under GDPR. You are responsible for their compliance.
What to Check
Before integrating any third-party service that touches personal data:
- Do they have a DPA? A Data Processing Agreement is mandatory. Most SaaS providers publish theirs publicly.
- Where do they store data? If outside the EU, verify the legal basis for the transfer.
- What data do they access? Minimize what you send. If the service only needs an email, don’t send the full profile.
- Can you delete data from their systems? User deletion requests must propagate everywhere.
- How do they handle breaches? Their DPA should specify notification timelines.
Common Third-Party Processors to Review
- Email services (Resend, SendGrid, Mailchimp)
- Analytics (Google Analytics, Mixpanel, Amplitude)
- Error tracking (Sentry, Bugsnag)
- Payment processing (Stripe, Adyen)
- Cloud hosting (AWS, Google Cloud, Vercel)
- Customer support tools (Intercom, Zendesk)
- AI APIs (OpenAI, Anthropic, Google AI)
Maintain a list of all processors. Review it quarterly.
Data Retention Policies
Don’t keep personal data longer than necessary. Define retention periods for every data type.
| Data Type | Suggested Retention | Reason |
|---|---|---|
| User account data | Until deletion requested | Needed for the service |
| Session logs | 90 days | Security and debugging |
| User activity logs | 1-2 years | Product analytics |
| Support tickets | 3 years | Service quality |
| Financial records | 7 years | Tax/legal obligations |
| Password reset tokens | 24 hours | Security |
| Failed login attempts | 90 days | Security monitoring |
Implement automated cleanup jobs. Don’t rely on someone remembering to run a script.
GDPR Checklist for Developers
Use this as a starting point when building or auditing a system.
Data Collection
- Every form field has a stated purpose
- No unnecessary data is collected
- Privacy policy is linked from every data collection point
- Consent is collected before processing (where required)
- Consent records are stored with timestamps
Data Storage
- Database encryption at rest is enabled
- TLS is enforced for all connections
- Sensitive fields have application-level encryption
- Backups are encrypted
- Access to production data is restricted and logged
User Rights
- Data export endpoint exists and covers all tables
- Account deletion endpoint exists and handles all data
- Users can view and withdraw consent in their settings
- Deletion propagates to third-party services
- All user rights requests are responded to within 30 days
Cookies and Tracking
- Cookie consent banner is implemented
- Non-essential cookies are blocked before consent
- Consent choices are granular (not all-or-nothing)
- “Reject all” is as prominent as “Accept all”
Third Parties
- All data processors are documented
- DPAs are signed with every processor
- Data sent to third parties is minimized
- Data deletion from third parties is possible
Security
- Audit logs track access to personal data
- Anomaly alerting is configured
- Incident response plan is documented
- Breach notification process is defined (72-hour deadline)
Retention
- Retention periods are defined for all data types
- Automated cleanup jobs are scheduled
- Expired data is actually being deleted (verify this)
Final Thoughts
GDPR compliance is not a one-time project. It’s a set of practices woven into how you build software. The technical work is straightforward: consent storage, data export, deletion endpoints, encryption, audit logging. Standard engineering.
The hard part is being thorough. It’s easy to forget about that log file, that analytics event, or that third-party integration storing user emails. Audit regularly. Test your deletion endpoint. Verify your exports are complete. Build privacy into your process from the start.
Need help building GDPR-compliant software or auditing your existing systems? Get in touch. We build privacy-first applications for European businesses and companies serving EU users.