Hey lovely readers,
The GDPR has been around since 2018, and most of us have heard of it. But when I ask developers what it actually means for the code they write, they often don't know. Usually someone mentions an annoying cookie banner and that's it.
So I'm starting a small series. I'm taking three European laws that affect developers and making them as simple as I can. This first post is about GDPR. After that, I'll cover the European Accessibility Act (EAA) and the AI Act.
Quick note before we start: I'm a developer, not a lawyer. Even though I'm really interested in this topic, I even did a thesis on it, I do not have a law degree. This post will help you understand what GDPR means for your code but if your company handles a lot of personal data, please also talk to someone who does this professionally.
What is GDPR?
GDPR (General Data Protection Regulation) is a European law that protects the personal data of people in the EU. It decides what you're allowed to collect, how you have to store it, and what people can ask you to do with their data.
Personal data is anything that can be linked to a real person. Names and email addresses are the obvious ones, but the list is longer than most people think:
- A phone number or home address
- A date of birth
- An IP address
- A user ID, if you can connect it back to someone
- Location data
But not all personal data is the same. A first name or an email address is personal data, but some data is much more sensitive and risky when it leaks. GDPR calls these special categories:
- Health data
- Sexual orientation and sex life
- Ethnic origin
- Religious or philosophical beliefs
- Political opinions
- Union membership
- Genetic data
- Biometric data used to identify someone, like a fingerprint
For these categories, the starting point is different. You're not allowed to process this data at all unless one of a few exceptions applies, like the user giving explicit consent. And if you process a lot of it, you need a risk assessment called a DPIA (more on that later). Criminal records have their own, similar set of rules.
And why should you care as a developer? Because the fines are big. A company can be fined up to 20 million euros or 4% of its worldwide yearly revenue, whichever is higher. But honestly the biggest reason here is trust. Your users give you their data, and it's our job to treat it well.
Does it apply to me?
Probably yes. GDPR applies when your app handles personal data of people in the EU. It doesn't matter where your company is based. If you build a webshop in the United States and people in Germany can order from it then the GDPR applies to you. So if your app has user accounts, a contact form, a newsletter sign up, or even just logs with IP addresses in them, this post is for you.
But what if your project is really small? Maybe you have a SaaS with four users, or just a newsletter sign up on your blog. Then GDPR still applies. There is no minimum number of users, and no minimum revenue. One person from the EU in your database is enough.
But here's the good news. What GDPR asks from you grows with your size and your risk. A small project with simple data needs a small, simple setup. Let's look at two examples.
A small SaaS
Let's say you have a SaaS with four users who pay you a few euros a month. Here's what you do need:
- A privacy policy. What you collect, why, how long you keep it, and who you share it with.
- Agreements with the services you use. Your hosting and payment provider handle personal data for you. Most of them have a standard data processing agreement you can simply accept.
- Decent security. HTTPS, hashed passwords, and backups.
- A simple overview of your data. One page with what you store and why is enough. There's an exception for companies with fewer than 250 employees.
- A way to handle requests. If someone asks to see or delete their data, you need to be able to do that.
And here's what you probably don't need:
- A risk assessment, since your processing isn't high risk
- A data protection officer
- An EU representative, if your business is based in the EU
- Special features in your code. If someone emails you to delete their account, doing it by hand is completely fine at this size.
A newsletter on your blog
Maybe you just have a blog with a newsletter sign up, and you store the email addresses in your own database. GDPR still applies. There is an exception for "purely personal" use, but a public newsletter isn't that.
Here's what you need:
- Consent. Someone has to actively sign up. A double opt in where people confirm their email address is stored in your system is the best way to do this.
- Proof of that consent. Store when and how someone signed up.
- An unsubscribe link in every email.
- A short privacy notice at your sign up form.
- Deleting the data when someone unsubscribes.
- An agreement with your email service if you use one to send your newsletter.
In code, that's a timestamp for the consent and an unsubscribe action. That's really it.
The rule of thumb
GDPR asks for results, not for features. When your project is small, a privacy policy, a simple overview of your data, and handling requests by hand is often enough. Build features for it once doing things by hand starts to hurt.
But what if you have a whole backend running with lots of users, well then there's some more work to do.
GDPR is more than code
Recently I helped a client who wanted to become GDPR compliant. When I wrote down all the work in tickets, something stood out: most of them weren't about code at all. They were about research and documentation.
So before we open our IDE, let's look at the work that comes first. You don't have to do all of this alone as a developer but you will be part of it. And honestly, you're often the person who knows best where the data actually lives.
Step 1: Find out what you have
- Map all personal data you collect. Every table, every form field, every log should be on there. This becomes your data inventory.
- Map how data flows. Where does it go after it enters your app? Think of your database, your email service, your analytics tool, and your payment provider.
- Check what you receive from others. Partners or third parties might send you personal data too. That counts as well.
Step 2: Know why you have it
- Write down the purpose for each type of data. Why do you store a phone number? "Maybe we'll need it someday" is not a purpose.
- Pick a lawful basis for each purpose. GDPR has six of them, like consent, a contract with the user, or a legal obligation. Every purpose needs one.
- Document legitimate interest when you use it. It's the most flexible basis, so you have to explain why your reason is fair, and why it doesn't hurt the rights of your users too much.
Step 3: Check the risk
- Find your high-risk processing. Think of large amounts of sensitive data, tracking people, or making automatic decisions about them.
- Decide if you need a DPIA. A Data Protection Impact Assessment is a document where you describe the risks and how you reduce them. For high-risk processing, it's required.
Step 4: Write it all down
This is the biggest part, and the least exciting one. But it matters, because GDPR asks you to be able to prove what you do.
- Records of processing activities. An overview of what you process, why, and for how long.
- A data retention schedule and a deletion policy. How long you keep each type of data and what happens after that.
- A data breach response plan. If something leaks, you usually have to report it to the data protection authority within 72 hours. That's not the moment to start figuring out who does what. You want that decided and written down somewhere.
- Procedures for user requests. What do you do when someone asks to see, change, or delete their data?
- Consent documentation. If you use consent, you need to be able to show when and how someone gave it.
- A vendor register and data processing agreements. Every service that handles personal data for you needs an agreement with you.
- Privacy notices. One for your users, and don't forget your employees. Their data counts too.
Step 5: Check if you need a representative
If your company is not based in the EU but you do you do collect data from people that live in there, you might need to appoint an EU representative. The UK has its own version of GDPR after Brexit, so the same goes for the UK.
Where this meets your code
Here's the nice part. A lot of these documents turn straight into work for developers:
- The data inventory tells you which properties to mark as personal data.
- The retention schedule becomes a background job that deletes or anonymizes old data.
- The user request procedures become a "download my data" and a "forget me" feature.
- The breach response plan only works if you have good logging and monitoring.
So let's get to that code.
Keep personal data out of your logs
Let's start with the mistake I see the most. Something goes wrong in production, so someone adds this:
logger.LogInformation("User signed up: {@User}", user);
It works great for debugging. But now the user's name, email address, and maybe even their phone number are sitting in your logs. And logs get copied everywhere: to a log service, to a colleague's screen, to a support ticket. You lose control over that data very quickly.
.NET has a built-in way to fix this, with the Microsoft.Extensions.Compliance.Redaction package. You mark which data is personal, and .NET removes it from your logs for you.
First, install these packages:
dotnet add package Microsoft.Extensions.Compliance.Redaction
dotnet add package Microsoft.Extensions.Telemetry
Next, you create a small "taxonomy". That's just a fancy word for a list of the types of data you want to treat differently:
using Microsoft.Extensions.Compliance.Classification;
public static class DataTaxonomy
{
public static string TaxonomyName => typeof(DataTaxonomy).FullName!;
public static DataClassification PersonalInfo => new(TaxonomyName, nameof(PersonalInfo));
}
public class PersonalInfoAttribute : DataClassificationAttribute
{
public PersonalInfoAttribute() : base(DataTaxonomy.PersonalInfo) { }
}
Now you can use that attribute on your model:
public class User
{
public int Id { get; set; }
[PersonalInfo]
public string Name { get; set; } = string.Empty;
[PersonalInfo]
public string Email { get; set; } = string.Empty;
}
Then you turn on redaction in your Program.cs:
builder.Logging.EnableRedaction();
builder.Services.AddRedaction(redaction =>
{
redaction.SetRedactor<ErasingRedactor>(
new DataClassificationSet(DataTaxonomy.PersonalInfo));
});
The last step is logging with the logging source generator, so .NET knows which properties to look at:
public static partial class Log
{
[LoggerMessage(Level = LogLevel.Information, Message = "User signed up")]
public static partial void UserSignedUp(ILogger logger, [LogProperties] User user);
}
And you call it like this:
Log.UserSignedUp(logger, user);
Now the user's ID still shows up in your logs, so you can still debug. But the name and email are erased before they ever reach a log file. You get the information you need, without the personal data you don't.
Quick side note: ErasingRedactor removes the value completely. If you'd rather still be able to see that two log lines belong to the same person, you can use the HmacRedactor instead, which turns the value into a hash.
Protect sensitive data with the Data Protection API
Some data you do need to store, like a phone number. But that doesn't mean it should sit in your database as plain text. If someone ever gets access to your database, plain text is the worst possible situation.
How strong should the protection be?
Remember the special categories from the start of this post, like health data? You might expect GDPR to say exactly how you should encrypt that kind of data. It doesn't. GDPR asks for "appropriate" security, based on the risk. Encryption is mentioned as an example, but there's no required algorithm or key size.
In practice, it comes down to this: the more sensitive the data, the stronger the protection should be. For something like health data or sexual orientation, encryption is really the minimum. If that data leaks and it wasn't encrypted, then that's very hard to defend.
And there's a nice bonus. If data leaks but it was properly encrypted, you often don't have to inform every affected user because the data is useless to whoever has it.
Good to know: some sectors have extra rules on top of GDPR. In the Netherlands, for example, healthcare organizations have to follow a security standard called NEN 7510. So if you build apps for a specific sector, check if something like that applies to you too.
Encrypting data in .NET
ASP.NET Core comes with the Data Protection API, which can encrypt and decrypt values for you. Here's a small class that protects a phone number:
using Microsoft.AspNetCore.DataProtection;
public class PhoneNumberProtector(IDataProtectionProvider provider)
{
private readonly IDataProtector _protector =
provider.CreateProtector("Users.PhoneNumber");
public string Protect(string phoneNumber) => _protector.Protect(phoneNumber);
public string Unprotect(string protectedPhoneNumber) =>
_protector.Unprotect(protectedPhoneNumber);
}
The string you pass to CreateProtector is called a purpose. Values protected with one purpose can't be unprotected with another, so your phone numbers and, say, your password reset tokens stay separated.
One important thing to know: the Data Protection API uses keys, and those keys need to be stored somewhere safe and permanent. If you lose them, you can't decrypt your data anymore. So make sure you configure where the keys live, for example:
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("/keys"))
.SetApplicationName("MyApp");
Keep in mind that Microsoft built this API mainly for data that doesn't live forever, like cookies and tokens. For sensitive data that you store for years, it's worth looking at options like Always Encrypted in SQL Server or keys in Azure Key Vault. But for getting started and understanding the idea, the Data Protection API is a great first step.
The right to be forgotten with EF Core
Under GDPR users can ask you to delete their personal data. This is called the right to be forgotten.
Sounds easy, right? Just delete the user. But here's where it gets interesting. Say this user has placed orders in your webshop. You often have to keep those orders for your bookkeeping. In the Netherlands, for example, that's seven years.
GDPR allows that. When the law says you have to keep something, then you can keep it. But you should remove everything that points to the person. That's where anonymizing comes in:
public async Task ForgetUserAsync(int userId)
{
var user = await db.Users.SingleAsync(u => u.Id == userId);
var orders = await db.Orders
.Where(o => o.UserId == userId)
.ToListAsync();
foreach (var order in orders)
{
order.UserId = null;
order.CustomerName = "Removed";
order.ShippingAddress = "Removed";
}
db.Users.Remove(user);
await db.SaveChangesAsync();
}
The user is gone, and the orders stay, but nobody can tell who placed them anymore.
Here's something I see very often in different web apps. Many apps use soft delete: an IsDeleted column that you set to true. That's great for "undo" buttons but it's not deleting. The data is still in your database. So if a user asks to be forgotten, a soft delete is not enough. And don't forget the places outside your main database: backups, logs, analytics tools, and third-party services you send data to. Those count too.
Anonymization vs pseudonymization
In the example above, I anonymized the orders. That word comes up a lot when you work with GDPR, and so does a word that sounds very similar: pseudonymization. They sound alike, but GDPR treats them very differently.
Pseudonymization: you can still go back
With pseudonymization, you replace something that identifies a person, like a name or an email address with a code. The information to link that code back to the person is stored somewhere else.
For example, your analytics tool only sees user_8f3a2c. A separate table or a key tells you who that is. This is still personal data, so GDPR still fully applies. But it lowers the risk a lot. If your analytics data leaks, nobody can see who user_8f3a2c is. That's why GDPR actively encourages it. The HmacRedactor I mentioned in the logging section is a good example. You can still see that two log lines belong to the same user, without seeing who that user is.
Anonymization: there's no way back
With anonymization, you change the data so nobody can find out who it belongs to anymore. Not your colleague, not a hacker, and not even you.
For example: "32 orders were placed in Amsterdam in March." There's no link to any customer at all. Data that is truly anonymous is no longer personal data. That means GDPR doesn't apply to it anymore.
The trap developers fall into
"Anonymous" is a high bar, and it's easy to think you've reached it when you haven't. Two common examples:
- Hashing an email address. That's pseudonymization, not anonymization. Anyone with a list of email addresses can hash them too and look for matches.
- Only removing the name. A postal code, a birth date, and a gender together can often point to one specific person. Each piece looks harmless, but together they're not.
This also goes for the "forget me" example above. Anonymizing the orders only works if nothing else on the order, like a postal code or a phone number, still points to the person.
Here's my advice after spending a lot of time thinking about this: if you're not 100% sure your data is anonymous, treat it as pseudonymized, and keep protecting it like personal data.
Only collect what you need
The easiest personal data to protect is the data you never collected. GDPR calls this data minimization.
A very common example is an endpoint that returns the full user entity:
app.MapGet("/users/{id}", async (int id, AppDbContext db) =>
await db.Users.FindAsync(id));
That sends everything to the frontend, including the date of birth, the address, and the phone number, even if the page only shows a name. Let's fix that with a small DTO:
public record UserSummary(int Id, string DisplayName);
app.MapGet("/users/{id}", async (int id, AppDbContext db) =>
{
var user = await db.Users
.Where(u => u.Id == id)
.Select(u => new UserSummary(u.Id, u.DisplayName))
.SingleOrDefaultAsync();
return user is null ? Results.NotFound() : Results.Ok(user);
});
Now only the data the page actually needs leaves your API.
The same idea works for your forms. Do you really need someone's full date of birth? Or do you only need to know that they're 18 or older? A simple checkbox might be all you need.
What about outside of Europe?
Before we wrap up, one more thing. GDPR is the most famous privacy law, but it's definitely not the only one. Many countries looked at GDPR and built something similar. Here are a few you might run into:
| Where | Law | Good to know |
|---|---|---|
| United Kingdom | UK GDPR | After Brexit, the UK kept its own copy of GDPR. It's almost the same, with a few small differences. |
| Switzerland | FADP | Switzerland isn't in the EU, so it has its own law. It's very close to GDPR. |
| Brazil | LGPD | Strongly inspired by GDPR, with similar user rights and its own data protection authority. |
| California, USA | CCPA | Focuses a lot on letting users opt out of their data being sold or shared. |
| Canada | PIPEDA | Canada's federal privacy law for companies. |
| South Africa | POPIA | Also close to GDPR in how it treats personal data. |
| India | DPDP Act | One of the newer ones, focused on consent. |
If you build your app with GDPR in mind then you're already in a good place for most of these laws. The ideas are often the same. Know what data you have, collect only what you need, protect it, and let users see and delete it.
But "similar" doesn't mean "the same". The details can be different like how users can say no, or how fast you need to report a data breach. So if your app has a lot of users in one of these countries, check that specific law too.
Your GDPR checklist
A short list you can bookmark:
- Map your personal data, where it goes, and why you have it.
- Make sure the documentation exists, and help turn it into code.
- Mark personal data in your code, and turn on redaction so it never ends up in your logs.
- Encrypt sensitive data you store, especially special categories like health data, and keep your keys safe.
- Build a real "forget me" flow: delete what you can, anonymize what you have to keep.
- If you're not sure data is truly anonymous, treat it as pseudonymized and keep protecting it.
- Remember that soft delete is not deleting.
- Use DTOs, and only collect and send the data you actually need.
That's a wrap!
GDPR can feel like a big scary law, but a lot of it comes down to good habits you can build right into your code. Start small, maybe with the logging part, and go from there.
In the next post of this series, I'll talk about the European Accessibility Act. It has been in force since June 2025, and it might apply to your app more than you think.
If you have questions, or tips on how you handle GDPR in your own projects, feel free to leave a comment or reach out to me on my socials.
See ya!
Top comments (0)