If you build software that serves Turkish users, three acronyms will eventually land on your desk: KVKK, İYS, and BİK. They are not optional "nice to have" features — they are legal obligations with real fines attached. Yet most engineering write-ups about them are written by lawyers, for lawyers, and stop exactly where the interesting part begins: the code.
This is the article I wish I had when we first had to make 200+ production sites compliant. It's the engineer's view — what each rule actually requires from your application, and how to implement it cleanly in PHP. The examples are intentionally generic; adapt the storage and framework details to your own stack.
The three you'll meet
| Acronym | Full name | What it governs | Who enforces it |
|---|---|---|---|
| KVKK | Kişisel Verilerin Korunması Kanunu | Personal data protection (Turkey's GDPR analogue) | KVKK Authority (KVKK Kurumu) |
| İYS | İleti Yönetim Sistemi | Commercial electronic messages (SMS/email/calls) | Managed via a central national registry |
| BİK | Basın İlan Kurumu | Press/news site requirements & official announcements | Basın İlan Kurumu (for news publishers) |
KVKK touches almost every app. İYS hits you the moment you send a marketing message. BİK is specific to news publishers. Let's take them in order.
1. KVKK — personal data protection
KVKK shares its DNA with GDPR, so if you've done GDPR work the concepts will feel familiar: lawful basis, explicit consent, data minimization, the right to erasure, breach notification, and registration with a central inventory (VERBİS) once you cross certain thresholds.
The mistakes I see most often are not legal misreadings — they're engineering shortcuts. Three of them matter.
Separate consent from the action
A checkbox that says "I accept the terms and consent to marketing" bundles two different lawful bases. KVKK wants explicit, specific, unbundled consent. In practice that means storing each consent as its own record with enough metadata to prove it later.
// Each consent is its own row — never a single boolean on the user.
function recordConsent(int $userId, string $purpose, bool $granted): void
{
$stmt = $pdo->prepare(
'INSERT INTO consent_log
(user_id, purpose, granted, ip, user_agent, created_at)
VALUES (:uid, :purpose, :granted, :ip, :ua, :now)'
);
$stmt->execute([
'uid' => $userId,
'purpose' => $purpose, // e.g. 'marketing_email', 'analytics'
'granted' => $granted ? 1 : 0,
'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
'ua' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255),
'now' => date('Y-m-d H:i:s'),
]);
}
The point of the ip, user_agent, and timestamp is not surveillance — it's that when someone asks "did this user actually consent, and when?", you can answer with evidence instead of a shrug. Consent is also revocable, so you append a new granted = 0 row rather than mutating the old one. The history is the proof.
The cookie banner has to actually block scripts
A banner that loads Google Analytics and the Meta pixel before the user clicks "accept" is theatre. Under KVKK (as under GDPR) non-essential trackers must not fire until consent exists. The cleanest implementation is to gate the script tags server-side:
function trackingScripts(int $userId): string
{
if (!hasConsent($userId, 'analytics')) {
return ''; // nothing loads, no third-party calls
}
return '<script src="/js/analytics.js" defer></script>';
}
If your analytics is purely first-party and aggregated (no cross-site identifiers, no PII), you have a much easier compliance story — which is one practical reason a lot of Turkish products lean toward self-hosted, first-party analytics.
Erasure means erasure (but keep the legal minimum)
The right to be forgotten collides with other obligations: you may be legally required to keep invoice and transaction records for years. The resolution is anonymization, not blind deletion. Strip the identifying fields, keep the financially/legally required skeleton.
function eraseUser(int $userId): void
{
// Anonymize, don't orphan: invoices must survive for tax law.
$pdo->prepare(
"UPDATE users SET
name='[deleted]', email=CONCAT('deleted_', id, '@invalid'),
phone=NULL, address=NULL, anonymized_at=:now
WHERE id=:id"
)->execute(['now' => date('Y-m-d H:i:s'), 'id' => $userId]);
// Hard-delete things with no retention requirement.
$pdo->prepare('DELETE FROM consent_log WHERE user_id=:id')
->execute(['id' => $userId]);
}
Decide your retention periods deliberately, document them, and make them queryable. "We delete logs after N days" is a sentence you want to back with a cron job, not a hope.
2. İYS — the commercial message registry
İYS is the part that surprises foreign engineers. In Turkey there is a central national registry for commercial electronic message consent. Before you send a marketing SMS, email, or call to someone, their consent must exist in that registry — and your own database agreeing isn't enough on its own.
The engineering consequence: never send marketing straight from your app's consent table. You check İYS first. Transactional messages (order confirmations, password resets, shipping updates) are exempt — but the line between "transactional" and "marketing" is exactly where people get fined, so be conservative.
function canSendMarketing(string $recipient, string $channel): bool
{
// 'channel' is one of: MESAJ (SMS), EPOSTA (email), ARAMA (call)
$status = iysLookup($recipient, $channel); // calls the registry/integrator API
// Only an explicit, active opt-in counts.
return $status === 'ONAY'; // vs 'RET' (rejected) or 'ALICI_YOK' (unknown)
}
function sendCampaign(array $recipients, string $channel, string $message): void
{
foreach ($recipients as $r) {
if (!canSendMarketing($r, $channel)) {
logSkipped($r, 'no_iys_consent');
continue; // skipping is cheaper than a fine
}
dispatch($channel, $r, $message);
}
}
Two practical notes from running this at scale:
- Sync both directions. When a user opts in or out in your UI, push that change up to the registry. When the registry changes (a user opts out there), pull it down. A nightly reconciliation job catches drift.
- Cache lookups, but not forever. Hammering the lookup API per-recipient on a large campaign is slow and rude. A short-TTL cache (hours, not weeks) keeps you fast without letting stale "ONAY" values send messages to someone who opted out this morning.
3. BİK — for news publishers
If you don't run a news site, skip this section. If you do, BİK compliance is what stands between you and being eligible for official announcements (resmî ilan) and the credibility that comes with it.
BİK's requirements are more editorial than algorithmic, but several do translate into application features:
- Mandatory imprint (künye): publisher identity, responsible editor, contact, and address must be present and reachable. This is a structured page, not a footer afterthought.
- Source attribution (mahreç): agency-sourced content must be labelled with its origin. If you ingest from news agencies, carry the source field through your pipeline to the rendered article.
- Content cadence & originality: a minimum flow of original editorial content, which in practice means your CMS needs solid authorship, scheduling, and audit metadata.
- Archive integrity: published pieces should remain accessible and stable at their URLs.
The imprint and source-attribution pieces are the ones that bite teams late, because they're easy to bolt on at the end and easy to get subtly wrong. Model them as first-class fields from the start:
// Carry the source through to render — don't lose it in the import step.
function renderArticleMeta(array $article): string
{
$out = '<div class="article-meta">';
$out .= '<span class="author">' . e($article['author_name']) . '</span>';
if (!empty($article['source_agency'])) {
$out .= '<span class="source">Kaynak: '
. e($article['source_agency']) . '</span>'; // mahreç
}
$out .= '<time datetime="' . e($article['published_at']) . '">'
. e(formatTr($article['published_at'])) . '</time>';
return $out . '</div>';
}
This is the kind of requirement that pushed our news CMS work toward treating source, author, and publish metadata as non-negotiable columns rather than optional extras — it's far cheaper than retrofitting attribution across an archive later.
A checklist you can paste into a ticket
- [ ] KVKK: consent stored per-purpose, with timestamp + evidence, revocable as an append
- [ ] KVKK: non-essential trackers gated server-side, fire only after consent
- [ ] KVKK: erasure = anonymize-and-retain-legal-minimum, with documented retention periods
- [ ] KVKK: VERBİS registration checked against your data-controller thresholds
- [ ] İYS: registry lookup before every marketing send; transactional messages clearly separated
- [ ] İYS: two-way sync (UI ⇄ registry) + nightly reconciliation + short-TTL cache
- [ ] BİK: structured imprint (künye) page, reachable
- [ ] BİK: source attribution (mahreç) carried end-to-end from import to render
- [ ] BİK: stable article URLs and accessible archive
Closing thought
None of this is exotic engineering. It's mostly the discipline of modeling consent and provenance as data you can prove, not state you assume. Do it at the schema level on day one and compliance becomes a property of the system. Bolt it on at the end and it becomes a migration you'll dread.
We've shipped this pattern across 200+ Turkish production sites at Alesta WEB, and the single biggest lesson is the boring one: make consent and source first-class data, and the legal requirements mostly take care of themselves.
This is general engineering guidance, not legal advice — confirm specifics for your situation with a qualified professional.













