Pragmatic, engineer first GDPR compliance: data mapping, lawful basis, tested data export and deletion, cookie consent, sub processors and DPAs — no lawyer required.
A client once forwarded me an email from a user in Germany. Subject line: "Delete all my data." The founder panicked, replied within the hour promising it was done, and then messaged me asking how you actually delete someone from a system that mirrors data across Firestore, Stripe, an email tool, an analytics pipeline, and a pile of nightly database backups. The honest answer was: nobody had ever designed for that. We'd designed for signups, not erasures.
That is GDPR in one story. Not a fine, not a cookie banner, not a legal opera. A single email that quietly asks your system to do something it was never built to do. I'm not a lawyer, I've never hired one for this, and I still run products that I'd defend to a regulator without breaking a sweat. This post is the pragmatic, engineer-first version of GDPR compliance I wish someone had handed me years ago: the small set of technical decisions that actually satisfy the law, stripped of the theater. Most of it maps cleanly onto the UK GDPR, CCPA/CPRA, and other modern data-protection regimes too, because they all rest on the same handful of ideas.
Every growth deck treats user data as gold. Collect everything, figure out the value later. GDPR flips that instinct on its head, and honestly, so should your engineering sense. Personal data is inventory you have to guard, account for, hand back on demand, and destroy on request. Every field you store is a small ongoing obligation with a real cost attached.
Once you internalize "data is a liability," most of the law becomes obvious. You stop asking "can I collect this?" and start asking "do I actually need this, and what do I owe the person if I keep it?"
A few consequences fall out immediately:
I now run a blunt filter on every new field: if this leaked tomorrow, would I be embarrassed or exposed? If yes, it needs a reason to exist. On a recent build we killed a "phone number" field that three people had added "just in case." Nobody could name a feature that used it. That's one less thing to encrypt, export, and erase forever.
GDPR calls this data minimisation (Article 5). You can call it not hoarding. It pairs with purpose limitation: data collected for one reason shouldn't quietly get reused for another. If you grabbed an email to send receipts, you don't get to fold it into a marketing blast without a new basis. Designing for a purpose up front is what keeps that line clean.
Before you can protect data, you have to know what counts as personal data. Most engineers picture name, email, address. The actual GDPR definition is anything that can identify a natural person, directly or when combined with something else. That drags in a lot of things you don't think of as "personal."
metadata blob you shoved a user object into at 2am.A special, sharper tier exists for special category data (Article 9): health, biometrics, sexual orientation, religion, political views, ethnicity, genetic data. If you touch any of that, the bar goes way up — you generally need explicit consent or a narrow legal carve-out, and this post is not enough. For most SaaS and consumer apps you're dealing with ordinary personal data, which is very manageable. But know which game you're in before you read on.
One useful distinction to keep in your head: anonymisation vs pseudonymisation. Truly anonymised data — where no one, including you, can re-link it to a person — falls outside GDPR entirely. Pseudonymised data (you swapped the name for a user_id but still hold the mapping) is still personal data. Most "anonymous analytics" that keeps a stable device identifier is actually pseudonymous, and the rules still apply. Real anonymisation is harder than hashing an email and calling it a day.
You cannot protect, export, or delete what you can't find. The single most useful thing I've ever done for compliance is a boring one-page data map (this is also your Article 30 "record of processing activities," minus the jargon). Not a diagram tool, not a Notion database with forty properties. A markdown table you can read in thirty seconds.
For each piece of personal data, four columns: what it is, where it enters, where it rests, and how long it lives.
| Data | Enters via | Rests in | Retention |
| --- | --- | --- | --- |
| Email, name | Signup | Firebase Auth, Firestore users | Life of account |
| Payment info | Checkout | Stripe (we store customer ID only) | Per Stripe |
| Support messages | Contact form | Zendesk | 24 months |
| IP + events | App usage | Analytics, Cloud Logging | 14 months |
| Uploaded files | In-app upload | Cloud Storage | Life of account |
The magic isn't the format, it's the act of doing it. Every time I've built one of these, I found data I forgot existed. A logging integration copying full request bodies. An old webhook writing raw payloads to a bucket nobody read. A "temporary" CSV export sitting in Storage for eight months. That kind of shadow data is exactly what turns a routine deletion request into a scramble.
Two rules that make the map honest:
If you do nothing else from this post, do this. It converts a vague legal dread into a finite, engineerable list. When you build export and deletion later, this table is your checklist — every row has to be reachable by both.
Here's where most founders either over-panic or over-ignore. GDPR says you need a lawful basis to process personal data (Article 6). There are six, but you'll almost only ever use three:
The practical rule I use: if it's necessary to run the service, it's contract or legitimate interest. If it's for someone else's benefit or a surprise to the user, it needs consent.
This is the part people get wrong in both directions. You do not need a consent banner to count how many users viewed a page using your own first-party, non-shared analytics. You do need consent before you load the Meta pixel that ships behavior off to an ad network.
So the analytics you can keep without a circus:
Where consent becomes non-negotiable:
I've shipped products that dropped GA entirely for a privacy-first tool and never looked back. Fewer scripts, faster pages, and the consent problem mostly evaporates because there's nothing to consent to. Privacy-by-design isn't a slogan here; it's the cheapest architecture.
GDPR gives people a set of data subject rights, and in practice a handful of them show up as real support tickets: access, portability, erasure, rectification, and objection. Rectification (fix wrong data) and objection (stop a given use) are usually handled by features you already have — an account settings page and an unsubscribe link. The two that break systems, and the two worth engineering deliberately, are export (access + portability) and deletion (erasure). Everything below is about those.
The law also puts a clock on you: you generally have one month to respond to a request. That's the deadline, not the target. If your export and deletion are real buttons, you answer in minutes, not weeks — which is exactly the posture that keeps a curious regulator calm.
This is the heart of it, and the part almost everyone fakes. If you can't run erasure and export on demand, you're not compliant, you're hoping. Treat both as endpoints you actually build and test, not policy paragraphs.
Export is the easier one. Gather everything you hold about a user into a machine-readable file and hand it over. JSON is fine, and it satisfies the "structured, commonly used, machine-readable" wording of the portability right.
async function exportUserData(userId: string) { const [profile, orders, messages] = await Promise.all([ getUserProfile(userId), getUserOrders(userId), getSupportMessages(userId), ]); return { exportedAt: new Date().toISOString(), profile, orders, messages, // include every table your data map lists for this user };}The trap is completeness. Your data map is the checklist. If a data type is on the map, it belongs in the export. Miss one and the export lies. A second trap is identity verification: before you hand a data dump to whoever emailed you, confirm they are the account holder — an attacker requesting "your data" is a classic way to exfiltrate a target. Gate export behind an authenticated session or a verified email challenge, never a raw request with an email address in it.
Deletion is where systems break, because it has to fan out across every place the data rests, including places you don't own.
async function deleteUser(userId: string) { // 1. First-party stores await db.users.delete(userId); await db.orders.softDelete({ userId }); // may need to keep for tax/legal await storage.deleteFolder(`users/${userId}`); // 2. Auth await auth.deleteUser(userId); // 3. Third-party processors await stripe.customers.del(stripeIdFor(userId)); // or anonymise await emailTool.contacts.delete(emailFor(userId)); // 4. Log the erasure itself (without the personal data) await db.erasures.insert({ userId: hash(userId), at: Date.now() });}Four things I learned the hard way here:
If export and deletion actually run and actually pass a test, you've handled the rights that generate the angry emails. Everything else is smaller.
Now the theater. Almost every cookie consent banner you've clicked is compliance cosplay: a giant "Accept All" button, a buried "Reject," and a wall of vendor logos. That pattern is not just annoying, it's often illegal (this is where the ePrivacy Directive, not just GDPR, bites), and it solves a problem most small products don't even have.
Here's what regulators actually care about, stripped down:
The liberating consequence: if you don't load non-essential cookies, you don't need a cookie banner. No third-party trackers, no ad pixels, first-party analytics that don't set identifying cookies, and the banner requirement mostly disappears. You still have a privacy policy, but you skip the pop-up entirely.
So my order of preference:
Most of the cookie-banner industry exists to make option 2 look mandatory. For a lean product, option 1 is usually right.
Your app is not one server anymore. A typical small stack sends personal data to Firebase, Stripe, an email provider, an analytics tool, error monitoring, and a support desk. Under GDPR you are the data controller, and each of those is a processor (or sub-processor) handling data on your behalf. You're responsible for the whole chain, including their sub-processors.
You don't need a legal team for this. You need three habits.
One: keep a sub-processor list. The same one-page discipline as the data map. Who touches personal data, what they get, and where they host it.
| Vendor | Purpose | Data shared | Region |
| --- | --- | --- | --- |
| Firebase / GCP | Auth, DB, storage | Profile, content | EU (chosen) |
| Stripe | Payments | Email, card via Stripe | US + EU |
| Postmark | Transactional email | Email, name | US |
| Sentry | Error monitoring | User ID, IP | EU (chosen) |
Two: pick regions on purpose. Most serious vendors let you choose EU data residency. Firebase lets you pick a region at project creation, and you can't change it later, so choose before you build, not after. Same for Sentry and many others. This one setting quietly removes most of the cross-border transfer headache — and when transfers to the US are unavoidable, reputable vendors now cover them under the EU–US Data Privacy Framework or Standard Contractual Clauses, which they document so you don't have to draft anything.
Three: rely on their DPA. Every reputable processor publishes a Data Processing Agreement you accept by using them, plus documentation for international transfers. You don't draft it; you accept it and file the link. When you evaluate a new vendor, "do they have a DPA and an EU region?" is a real selection criterion, right next to price.
The failure mode here is the random tool someone wires in over a weekend. A no-name analytics script, a scrappy AI API you're piping user messages into, a Zapier chain copying records somewhere unlisted. Every new integration that touches user data goes on the sub-processor list, or it doesn't get merged. That review is worth more than any banner. This is doubly true for LLM APIs now: if you send user content to a model provider, check whether that data can be used for training, and prefer the enterprise or zero-retention tier.
You will, at some point, get an email that starts with "Regarding your data practices..." It might be a curious user, a nosy competitor, or an actual regulator. The goal isn't a bulletproof legal fortress. It's to answer calmly, specifically, and in a way that shows you thought about this on purpose. That's what "defensible" means for a small team.
Here's the posture I keep ready, and it fits in a handful of artifacts:
Notice what's not on the list: a lawyer, a consultant, a six-figure tool, or a cookie banner. Those can help at scale, but none of them is the substance. The substance is knowing your data and being able to act on it. (Two caveats where you genuinely do want help: if you process special category data at scale, or if you're large enough to trigger a Data Protection Officer or a DPIA obligation, get advice. Most early products aren't there yet.)
One more thing that matters more than any document: breach honesty. If personal data leaks, GDPR generally expects notification of the supervisory authority within 72 hours for serious breaches, and notification of affected users when the risk to them is high. The instinct to hide a breach is the single most expensive mistake you can make. The fines that make headlines are usually for the cover-up and the sloppiness, not the leak itself. Decide now that you'll tell the truth fast. It's cheaper.
Compliance isn't a document you buy. It's a system you can explain in one email without flinching.