Serviciile noastre

Dacă poți să-ți imaginezi, noi putem să-l construim.

What changes in a codebase when GDPR compliance is required

Written By: BrainSoft In Security

GDPR rarely arrives as a greenfield project. It shows up mid-sprint, usually because a client signs an EU customer or a legal review flags something in the signup flow. The awkward part is that most of the work is not legal paperwork. It lands in the repository: new endpoints, new columns, new jobs, and a lot of decisions about data you were happily storing forever.

The short version: you add a data inventory, per-user export and deletion paths, consent and purpose tracking, retention jobs, and tighter logging and third-party handling. Below is what that actually looks like in code, in roughly the order I tend to do it.

Start with a data inventory, not with code

Before touching anything, I write down every place personal data lives. Not a compliance document, just a working list. Tables with names, emails, phone numbers, IPs, device IDs. JSON blobs in a metadata column. S3 buckets holding uploaded ID scans. Logs. Analytics events. The CRM. The support tool. The queue that keeps a copy of the payload for retries.

This list drives everything else. You cannot answer an export request if you do not know where the data sits, and you cannot delete a user if three services hold fragments of them. I keep it in the repo as a markdown file so it gets reviewed in pull requests.

The endpoints that have to exist

Two capabilities stop being optional: a user can get their data out, and a user can have it erased. Both need to be real, tested code paths, not a support ticket someone handles manually at 11pm.

Export is usually the easier one. It has to pull from every system in the inventory and produce something a human can read. A background job that assembles JSON and emails a signed link is fine.

@app.post("/me/export")
def request_export(user):
    job = enqueue("build_user_export", user_id=user.id)
    return {"status": "queued", "job_id": job.id}

Deletion is where it gets interesting. A hard DELETE FROM users breaks foreign keys, invoices you are legally required to keep, and audit trails. What I do instead is a two-phase erase: anonymise the record in place, then purge the parts that have no retention obligation.

UPDATE users
SET email = NULL,
    name = 'deleted-user-' || id,
    phone = NULL,
    deleted_at = now()
WHERE id = $1;

Then a separate job walks the inventory: delete uploads, clear cached profiles, drop analytics identifiers, remove the address from the shipping provider. Each of those is a small piece of code, and each one is a place where something can silently fail. I log every step and alert on failures, because a half-deleted user is worse than an undeleted one.

Consent, purpose and retention become data

Consent used to be a checkbox that wrote a boolean. That is not enough. You need to know what the user agreed to, when, and under which version of the text. So it becomes a table.

CREATE TABLE consents (
  user_id      bigint NOT NULL,
  purpose      text   NOT NULL,
  granted      boolean NOT NULL,
  policy_version text NOT NULL,
  recorded_at  timestamptz NOT NULL DEFAULT now()
);

Purpose matters because marketing email and product notifications are different things. If someone withdraws marketing consent, the code that sends campaigns has to check that, not just the signup form. That check belongs in one place, a small function every sender calls, so it cannot drift.

Retention turns into scheduled work. Instead of keeping rows forever, you define a lifetime per data type and run a job that deletes or anonymises past it. Support tickets after 24 months, raw event logs after 90 days, inactive accounts after a defined period. These jobs need dry-run modes, because the first time you run a delete job in production you want to see what it would have removed.

Logging, third parties and the quiet leaks

This is the part people underestimate. Your application logs are a data store, and they are usually the least controlled one. Request bodies, query strings, exception messages with an email in them, that one console.log someone added during a debugging session in 2022.

I add a scrubbing layer at the logging boundary, not at each call site, so it cannot be forgotten. Emails, tokens, card fragments and national ID patterns get redacted before the line is written. It is a small function and it saves a lot of pain.

Third-party calls get the same treatment. Every outbound integration that receives personal data needs a reason to exist, a documented purpose, and ideally a data processing agreement behind it. In code, I like a single module that wraps outbound HTTP so I can see the full list of destinations in one file rather than grepping the repo. That list is also where I check whether anything is sending more than it should, like shipping a full user object to an analytics endpoint when it only needed an ID.

If you are doing this across several services, or the inventory keeps growing faster than you can keep up, it is often worth bringing in help rather than bolting it on sprint by sprint. We do this kind of work regularly through our services.

Frequently asked questions

Does GDPR require me to delete user data immediately?

No. The regulation says without undue delay, and a month is the common working deadline for responding to a request. In practice you queue a deletion job, run it within days, and keep a record of what was erased and what was retained for a legal reason like invoicing.

Can I just anonymise everything instead of deleting it?

Only if the anonymisation is genuinely irreversible. Replacing a name with a UUID while keeping the email in another table is pseudonymisation, not anonymisation, and it still counts as personal data. If the record can be re-linked to a person, treat it as data you still have to protect and delete.

How much of this is code versus process?

Roughly half and half. The endpoints, retention jobs and log scrubbing are code. The inventory, the retention periods, the purposes and the third-party list are decisions people make. The mistake is doing only the process side and assuming the code will somehow follow.


#Security