Mert Özen Further With Every Line
Security Data Privacy and KVKK for Developers — #01

KVKK and GDPR for Developers: Personal Data, Special Categories, Controllers/Processors and the Principles That Shape Code

Mert Özen Nov 15, 2026 14 min 7 views
KVKK and GDPR for Developers: Personal Data, Special Categories, Controllers/Processors and the Principles That Shape Code

Personal data is any information that another record can tie to a person, so hashed e-mail addresses, IP addresses and device IDs are personal data too. We cover the definitions of Turkey's Law 6698 and the GDPR, controllers versus processors, the endpoints behind data subject rights, what Law 7499 changed in 2024 and the KVKK–GDPR differences on Bookstore, with code that was run and primary sources; not legal advice.

Version note: The code in this post was built and run with .NET 10, ASP.NET Core 10, Entity Framework Core 10.0.15 (SQLite) and xUnit 2.9. Legal dates, deadlines and article numbers were checked against mevzuat.gov.tr, kvkk.gov.tr and the Turkish Official Gazette on 15 November 2026.

This series is a developer's view, not legal advice. How the law applies to you, which legal basis covers which data and how long you keep it are decisions for your legal counsel; what you read here is what those decisions turn into in code.

Guess First

Bookstore's marketing team wants to upload customer e-mail addresses to an advertising platform, but they say "let's not send personal data", so they send the SHA-256 digest of each address instead. Is that file anonymous data, or is it still personal data? A second question: when a customer asks to see their data, should your answer include the IP address they signed in from and their device ID? The answers, with code, are below.

What Personal Data Is and Is Not

In this post we read KVKK, Turkey's Law No. 6698 on the Protection of Personal Data, side by side with the GDPR, the EU's Regulation 2016/679. The Turkish law was published in Official Gazette No. 29677 on 7 April 2016; the GDPR has applied since 25 May 2018 (Art. 99). Article 3 of KVKK defines personal data as any information relating to an identified or identifiable natural person. The key word is "identifiable". A name and an e-mail address are obviously personal data, but so are a customer number, an order history, a delivery address or a device ID that carries no name on its own, as long as another record can connect it to the person. The GDPR gives the same definition in Art. 4(1) and explicitly lists an "online identifier"; Recital 30 names IP addresses and cookie identifiers as traces that can be used to identify people. KVKK's text does not list IP addresses separately; as a developer, the safe assumption is to treat IP addresses and device IDs as personal data.

Special categories of personal data are a separate class, which KVKK calls özel nitelikli kişisel veri. Article 6 lists race, ethnic origin, political opinion, philosophical belief, religion, sect or other beliefs, clothing, membership of associations, foundations or trade unions, health, sex life, criminal convictions and security measures, and biometric and genetic data. A bookstore needs none of these, which is good news: Bookstore's design rule is not to collect special category data at all. Note that the Turkish national ID number is not on this list; it is sensitive and must be protected, but it is not special category data under the law. That distinction will matter in post 03 when we decide how to encrypt it.

KVKK defines anonymisation as making the data impossible to associate with an identified or identifiable person in any way, even by matching it with other data (Art. 3(1)(b)). That is a high bar. The phrase "even by matching it with other data" explains why a hashed e-mail address is not anonymous. Anyone who holds their own customer list can try it with a line of code:

// An "anonymous" analytics export: SHA-256 of the e-mail address instead of the address itself.
string[] customerEmails = [.. FakePeople.All.Select((_, i) => FakePeople.ToCustomer(i, DateTime.UtcNow).Email)];
string[] exportedHashes = [Sha256Hex("grace.demoray@example.com"), Sha256Hex("linus.fakely@example.com")];

// Anyone holding the customer list re-identifies the rows with one dictionary.
Dictionary<string, string> lookup = customerEmails.ToDictionary(Sha256Hex, e => e);
foreach (string hash in exportedHashes)
{
    Console.WriteLine($"{hash[..16]}... -> {lookup.GetValueOrDefault(hash, "(unknown)")}");
}

// An 11-digit national ID has at most 900,000,000 valid values (9 free digits + 2 check digits).
const int sample = 2_000_000;
var sw = Stopwatch.StartNew();
for (int i = 0; i < sample; i++)
{
    SHA256.HashData(Encoding.ASCII.GetBytes((100_000_000 + i).ToString(CultureInfo.InvariantCulture)));
}
sw.Stop();
double perSecond = sample / sw.Elapsed.TotalSeconds;
double minutes = 900_000_000 / perSecond / 60;
Console.WriteLine(string.Create(CultureInfo.InvariantCulture,
    $"single core: {perSecond:N0} SHA-256/s, whole ID space: ~{minutes:N0} min"));
dotnet run --project tools/Bookstore.PrivacyLab -- hash-link
693cee2edc8c0bd0... -> grace.demoray@example.com
68203c30f2ed3faa... -> linus.fakely@example.com
single core: 5,408,153 SHA-256/s, whole ID space: ~3 min

The people in the sample are made up, and the addresses use the example.com domain. The first two lines show how someone with the customer list goes from a hash back to a person. The third line is more uncomfortable: even without a list, a small value space can be tried in full. A single CPU core hashes every possible value of an 11-digit ID in a few minutes. A hash does not hide the data; at best it gives you pseudonymisation. The GDPR draws that line clearly: Art. 4(5) defines pseudonymisation as processing that can no longer attribute the data to a person without additional information, and Recital 26 says pseudonymised data is still personal data. KVKK's Article 3 has no separate definition of pseudonymisation, so in practice the only question is whether the data meets the definition of anonymisation. A hashed e-mail address does not.

Controller and Processor

Two roles in the law affect code directly. The controller, veri sorumlusu in KVKK, is the person who determines the purposes and means of processing and is responsible for setting up and running the filing system. The processor, veri işleyen, processes personal data on the controller's behalf, based on the authority the controller gives (Art. 3(1)(ğ) and 3(1)(ı)). The data subject, ilgili kişi, is the natural person whose data is processed.

Let's read Bookstore, the online bookstore used as the shared example across this site's series, with that lens. For an individual customer's account, orders and delivery address, Bookstore is the controller: it decides what to keep, why and for how long. The courier that delivers the order and the cloud provider the application runs on are typical processor candidates to the extent they work on Bookstore's behalf. The payment institution is a more complicated case: it collects the payment for Bookstore, but it may also process the same data for its own purposes under its own regulatory obligations. The contract and the actual flow decide the role, and that decision belongs to the legal team, not to the code. The consequence for a developer is this: every external service integration is a data transfer that belongs in the inventory. Article 12(2) holds the controller jointly responsible with the processor for security measures on data processed on its behalf.

On the Institutional Orders side, when a school adds its own staff as users, who is the controller for that staff data, the school or Bookstore? That question also goes on the list below.

Article 4 of KVKK lists five principles: lawfulness and fairness; being accurate and, where necessary, up to date; processing for specified, explicit and legitimate purposes; being relevant, limited and proportionate to the purpose; and being kept only as long as the relevant legislation requires or the purpose needs. GDPR Art. 5(1) has six very similar principles, and Art. 5(2) adds accountability, the duty to be able to demonstrate compliance. Article 5 of KVKK gives the legal bases: the rule is explicit consent, açık rıza in the law, but it is not required when another condition applies, such as the conclusion or performance of a contract, a legal obligation or legitimate interest. The legal team decides which basis covers which field; your job is to make that decision visible and testable in code.

Principle (KVKK Art. 4 / GDPR Art. 5)In codeIn this series
Specified, legitimate purpose / purpose limitationA purpose label on every field; no queries outside the purpose02
Limited and proportionate / data minimisationNever adding unneeded fields to the model, an age confirmation instead of a birth date02
Accurate and up to date / accuracyA rectification endpoint, validation01
Kept only as long as needed / storage limitationRetention fields and a periodic destruction job04
Data security (KVKK Art. 12) / integrity and confidentialityEncryption, masking, restricted access03
Accountability (GDPR Art. 5(2))Inventory, destruction records, audit log02 and 04

Data Subject Rights: Endpoints and Jobs

Article 11 of KVKK gives the data subject a list of rights: to learn whether their data is processed, to request information if it is, to learn the purpose, to know the third parties the data was transferred to, to ask for incomplete or incorrect data to be corrected, to ask for erasure or destruction under the conditions of Article 7, to ask for those actions to be notified to the third parties, to object to a result against them that comes exclusively from automated analysis, and to claim compensation for damage. Article 13 requires an answer within thirty days at the latest; under the GDPR the period is one month, extendable by two more months where necessary (Art. 12(3)).

Every item on that list is an endpoint, a job or a line in the inventory. The first version of the Bookstore API starts with three endpoints:

RouteGroupBuilder me = app.MapGroup("/api/me").RequireAuthorization();

// KVKK md. 11/1-b, GDPR Art. 15: right of access
me.MapGet("/personal-data", async (ClaimsPrincipal user, DataSubjectRequests requests, CancellationToken ct) =>
    await requests.ExportAsync(CustomerId(user), ct) is { } export ? Results.Ok(export) : Results.NotFound());

// KVKK md. 11/1-d, GDPR Art. 16: right to rectification
me.MapPatch("/profile", async (ProfileCorrection change, ClaimsPrincipal user, DataSubjectRequests requests, CancellationToken ct) =>
    await requests.CorrectAsync(CustomerId(user), change, ct) ? Results.NoContent() : Results.NotFound());

// KVKK md. 7 and 11/1-e, GDPR Art. 17: erasure is a tracked request, not a synchronous DELETE
me.MapPost("/erasure-requests", async (ClaimsPrincipal user, DataSubjectRequests requests, CancellationToken ct) =>
{
    ErasureRequest request = await requests.RequestErasureAsync(CustomerId(user), ct);
    return Results.Accepted($"/api/me/erasure-requests/{request.Id}", new { request.Id, request.DueAtUtc });
});

Authentication is not the topic of this post. The sample uses a demo handler that only runs in Development and reads the customer number from a header; the real sign-in flow and the Identity side of account deletion are covered in the Authentication and Authorization series post "The Account Lifecycle with ASP.NET Core Identity: Registration, Email Confirmation, Password Reset, External Login and Account Deletion". The answer to an access request should not stop at the person's profile:

curl -s -H "X-Demo-Customer-Id: 3" http://127.0.0.1:52801/api/me/personal-data
{
    "profile": {
        "fullName": "Grace Demoray",
        "email": "grace.demoray@example.com",
        "phone": "+90 000 000 00 03",
        "birthDate": "1992-03-10",
        "city": "Istanbul"
    },
    "orders": [
        {
            "orderId": 3,
            "orderedAtUtc": "2026-03-13T09:00:00",
            "total": 412.5,
            "shippingAddress": "Example St. 3, Istanbul"
        }
    ],
    "sessions": [
        {
            "signedInAtUtc": "2026-03-13T08:55:00",
            "ipAddress": "127.0.0.1",
            "deviceId": "device-0003",
            "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
        }
    ],
    "exportedAtUtc": "2026-11-15T06:05:51.065062Z"
}

The sessions section answers the second half of the opening question. The IP address and device ID in the sessions table are data kept against the person; if they do not appear in the export, either you do not consider them personal data or there is a table you forgot exists. We pinned that down with a test:

[Fact]
public async Task Export_includes_ip_address_and_device_id()
{
    using var testDb = new TestDb();
    int id;
    using (BookstoreContext db = testDb.CreateContext())
    {
        Customer c = FakePeople.ToCustomer(0, Now);
        c.Sessions.Add(new CustomerSession { SignedInAtUtc = Now, IpAddress = "127.0.0.1", DeviceId = "device-0001", UserAgent = "test" });
        db.Customers.Add(c);
        await db.SaveChangesAsync();
        id = c.Id;
    }

    using BookstoreContext read = testDb.CreateContext();
    PersonalDataExport? export = await new DataSubjectRequests(read, new FixedClock(Now)).ExportAsync(id);

    SessionExport session = Assert.Single(export!.Sessions);
    Assert.Equal("127.0.0.1", session.IpAddress);
    Assert.Equal("device-0001", session.DeviceId);
}

The erasure request is deliberately not synchronous. Article 7 says erasure, destruction and anonymisation happen once the reasons that required the processing no longer exist; order and invoice records may still be under retention duties from tax and commercial law. So the request is opened as a record, and its due date is calculated from the 30-day response period:

// KVKK md. 13/2: requests are answered within 30 days at the latest.
public static readonly TimeSpan ResponseDeadline = TimeSpan.FromDays(30);

public async Task<ErasureRequest> RequestErasureAsync(int customerId, CancellationToken ct = default)
{
    DateTime now = clock.GetUtcNow().UtcDateTime;
    var request = new ErasureRequest
    {
        CustomerId = customerId,
        RequestedAtUtc = now,
        DueAtUtc = now + ResponseDeadline,
    };
    db.ErasureRequests.Add(request);
    await db.SaveChangesAsync(ct);
    return request;
}
curl -s -i -X POST -H "X-Demo-Customer-Id: 3" http://127.0.0.1:52801/api/me/erasure-requests
HTTP/1.1 202 Accepted
Content-Type: application/json; charset=utf-8
Location: /api/me/erasure-requests/1
Transfer-Encoding: chunked

{"id":1,"dueAtUtc":"2026-12-15T06:05:51.083069Z"}

The job that processes the request, deciding what to delete, what to anonymise and what to keep with restricted access until the legal period ends, is the topic of post 04. The lesson for today: "delete my account" is not a button, it is a workflow with a deadline. The right to know the recipients, and passing corrections on to them, also need a record of transfers; we build the sub-processor list in post 07.

KVKK and GDPR Side by Side

If Bookstore also sells to people in the EU, the GDPR may come into play as well; that assessment is also for the legal team. I collected the points that matter for code in one table. I deliberately do not quote administrative fine amounts: Article 18 of KVKK lists them, and the current values should be read from the Authority's website.

TopicKVKK (6698)GDPR (2016/679)
Special category dataArt. 6; clothing, association/foundation membership and criminal convictions are also listedArt. 9; conviction data in a separate article (Art. 10)
PseudonymisationNot defined in the lawDefined in Art. 4(5), an example measure in Art. 25
Answering a data subject requestWithin 30 days at the latest (Art. 13(2))One month, extendable by two months (Art. 12(3))
Breach notificationArt. 12(5) "as soon as possible"; 72 hours under Board decision 2019/10 of 24.01.2019Within 72 hours where feasible (Art. 33(1))
Registration dutyData Controllers' Registry (Art. 16), through VERBİS; exemptions by Board decisionRecords of processing activities (Art. 30)
Responsible personThe contact person in the registry regulationA DPO in certain cases (Art. 37)
Cross-border transfersArt. 9 (as amended by Law 7499): adequacy decision, appropriate safeguards, incidental casesChapter V (Art. 44–49)
Administrative finesArt. 18; current amounts on kvkk.gov.trEUR 10 million or 2%, or EUR 20 million or 4%, whichever is higher (Art. 83)

KVKK has no exact equivalent of the DPO; the Regulation on the Data Controllers' Registry, published in Official Gazette No. 30286 on 30 December 2017, requires a contact person to be named in the registration. Who must register in VERBİS and how is not the topic of this series; the Authority's VERBİS page is the only reliable source.

What Law 7499 changed in 2024

Law No. 7499 was published in Official Gazette No. 32487 on 12 March 2024 and amended Articles 6, 9 and 18 of Law 6698 with effect from 1 June 2024. The second paragraph of Article 6 was repealed and the third was rewritten: processing special category data is prohibited as a rule, and the exceptions are now a single list (explicit consent, being explicitly provided for by law, establishing or protecting a right, health services, employment and social security obligations and others; Art. 6(3)). The cross-border transfer article was rewritten from scratch: an adequacy decision, otherwise appropriate safeguards such as standard contracts, and failing that, incidental cases. Standard contracts must now be notified to the Authority within five business days of signing (Art. 9(5)), and a new administrative fine was added for not doing so (Art. 18(1)(d)). Actions against the Board's administrative fines now go to the administrative courts (Art. 18(3)); applications pending before the criminal judgeships of peace on 1 June 2024 continue there (provisional Art. 3(2)). Provisional Article 3 also allowed the old first paragraph of Article 9 to apply alongside the new version until 1 September 2024. We discuss the technical effect of the transfer regime in post 07.

Questions you need the legal team to answer before you write the code, from a developer's point of view:

  • For what purpose, and on which legal basis (Art. 5 or 6), is this field collected; is explicit consent needed?
  • What is the retention period for each data category, and from which event does it start (order date, account closure, last sign-in)?
  • What role do our courier, payment, e-mail and cloud providers have (processor or separate controller), and does any flow take data abroad?
  • Who is the controller for the data of institutional customers' users?
  • On an erasure request, which records stay because of a legal obligation, and who can access them?
  • In a data breach, who decides on the 72-hour notification, and what information does that person need from the technical team, by when?

Production Notes

The export for an access request does not end with one database query. Tickets in the support system, the send history in the e-mail service and the user ID in analytics belong to the same person. Even if you cannot collect all of them automatically, you need to know which systems must be checked by hand; that list is called the data inventory, and it is the topic of the next post. The second point is verifying who sent the request: if the export endpoint returns someone else's data to an unauthenticated request, you have produced a data breach while honouring a right. That is why the 401 response in the sample matters. The third is tracking deadlines: a DueAtUtc field that does not appear on a dashboard or in an alert is just a column.

Logs are part of this picture too. Writing the content of an export to the log while handling an access request creates one more copy of the personal data. The mechanics of classifying personal data in logs and masking it automatically are shown in the .NET Logging series post "Sensitive Data in Logs: Redaction, Data Classification and Privacy-Compliant Logging".

Common Pitfalls

The most common pitfall is believing a hashed e-mail address or phone number is anonymous data. The output above shows that anyone with the customer list can match the hash back, and that small value spaces can be tried in full within minutes. Even when the hash is strengthened with a salt or an HMAC, it is reversible for whoever holds the key; that is pseudonymisation, and the data stays personal data.

The second pitfall is treating IP addresses, device IDs or cookie values as "technical data" outside personal data. If these values sit next to a customer number in the sessions table, they are tied to a person; they must be returned on an access request, deleted on an erasure request and limited in logs and analytics. The third is believing the national ID number is special category data, or the opposite, treating it like an ordinary field. It is not on the list in Article 6, but it is one of the fields most exposed to identity theft; we encrypt it in post 03. The fourth is naming a table "anonymized" without asking the law's question: can the person be reached even by matching it with other data?

Sources for This Post

Every legal statement relies on the primary sources below; accessed on 15 November 2026.

  • Law No. 6698 on the Protection of Personal Data, current text (Art. 3, 4, 5, 6, 7, 11, 12, 13, 16, 18, provisional Art. 3): mevzuat.gov.tr/mevzuatmetin/1.5.6698.pdf
  • Law No. 7499, Official Gazette No. 32487 of 12 March 2024: resmigazete.gov.tr/eskiler/2024/03/20240312-1.htm
  • Board decision 2019/10 of 24.01.2019 (breach notification, 72 hours): kvkk.gov.tr/Icerik/5362/Veri-Ihlali-Bildirimi
  • Regulation on the Data Controllers' Registry, Official Gazette No. 30286 of 30 December 2017: kvkk.gov.tr/Icerik/5442/VERI-SORUMLULARI-SICILI-HAKKINDA-YONETMELIK
  • GDPR, Regulation (EU) 2016/679 (Art. 4, 5, 9, 12, 17, 25, 33, 37, 83, 99; Recitals 26 and 30): eur-lex.europa.eu/eli/reg/2016/679/oj

A Small Experiment

★☆☆ Run the hash-link command on your own machine and see how many hashes per second one core computes. Do the same calculation for the space of 10-digit phone numbers: how long does it take to reverse a list of hashed phone numbers?

★★☆ Add a GET /api/me/recipients endpoint to DataSubjectRequests that returns the third parties the person's data was shared with (Art. 11(1)(ç)). Think about where it reads from: a list embedded in code, or the inventory?

★★★ Write an integration test showing that a request that is unauthenticated, or that carries another customer's number, cannot reach the export. Expect 401 when the header is missing; also test that the demo handler is never registered in production.

In the next post we gather this scattered knowledge in one place: we derive Bookstore's personal data inventory from the code itself, classify fields with Microsoft.Extensions.Compliance attributes and catch the field added "because we might need it later" in a test after the build.