B2B SaaS Platformlarında KVKK Kabusu ve Veribenim Çözümü: Multi-Tenant Veri Ayrıştırma ve API ile İzole Çerez Yönetimi

Birçok SaaS kurucusu, hukuki sorumluluktan kurtulmak için söyle demiştir: > "Biz platform sağlayıcısı, müşterilerin ne yaptığından sorumlu değiliz." **KVKK Madde 2 ve GDPR Article 28 bu yanlış yorumu çöper:** ``` Veri İşleyen, Veri Sorumlusu'nun talimatı üzerine kişiye ilişkin verileri işler. Hukuki sorumluluğu Veri Sorumlusu üstlenir. ``` Yani: - **Veri Sorumlusu (DPA'da)**: SaaS müşterisi (B2B client) - **Veri İşleyen**: SaaS firması - **Sorumlu**: **İkisi de** — ancak farklı seviyelerde SaaS firması, eğer: - Konum verisi saklar - Kullanıcı davranış analizi yapar - Çerez yönetimi sunar - Müşteri İP adresini loglar ...bunları **kontrol altında** tutmazsa, **ortakça sorumlu** sayılabilir.

Bloglar
B2B SaaS Platformlarında KVKK Kabusu ve Veribenim Çözümü: Multi-Tenant Veri Ayrıştırma ve API ile İzole Çerez Yönetimi

"Ben Sadece Altyapı Sağlıyorum" Yanılgısı

Birçok SaaS kurucusu, hukuki sorumluluktan kurtulmak için söyle demiştir:

"Biz platform sağlayıcısı, müşterilerin ne yaptığından sorumlu değiliz."

KVKK Madde 2 ve GDPR Article 28 bu yanlış yorumu çöper:

Veri İşleyen, Veri Sorumlusu'nun talimatı üzerine kişiye ilişkin
verileri işler. Hukuki sorumluluğu Veri Sorumlusu üstlenir.

Yani:

  • Veri Sorumlusu (DPA'da): SaaS müşterisi (B2B client)
  • Veri İşleyen: SaaS firması
  • Sorumlu: İkisi de — ancak farklı seviyelerde

SaaS firması, eğer:

  • Konum verisi saklar
  • Kullanıcı davranış analizi yapar
  • Çerez yönetimi sunar
  • Müşteri İP adresini loglar

...bunları kontrol altında tutmazsa, ortakça sorumlu sayılabilir.

🚨 İHLAL RİSKİ: Pek çok SaaS, multi-tenant mimaride veri sızıntısına izin verir. Örneğin, Tenant A'nın API anahtarı kullanılarak Tenant B'nin kullanıcı verisi çekilebilir. KVKK Madde 3 ihlali = 500,000 TL - 10 milyon TL ceza.

Bu makalede, Veribenim'in tenant-level API'si kullanarak her müşterinin gizlilik ayarlarını izole etmek, göstereceğiz.


1. Veri İşleyen vs. Veri Sorumlusu Ayrımı

KVKK'nın mutsuzlaştırıcı yanı, SaaS için iki farklı sorumluluk tanımlamasıdır:

Veri Sorumlusu (Data Controller)

"Kişiye ilişkin verilerin işlenmesi amacını ve araçlarını belirleyen gerçek
veya tüzel kişi"

Örnek: Pazarlama SaaS'ının B2B müşterisi (email kampanyası yapan şirket).

Sorumlulukları:

  • ✅ Hangi veriyi toplayacağım? Kararı
  • ✅ Kaç gün tutacağım? Kararı
  • ✅ Kimle paylaşacağım? Kararı
  • ⚖️ Cezadan sorumlu: Madde 12-19 ihlalleri

Veri İşleyen (Data Processor)

"Veri Sorumlusu'nun talimatı üzerine kişiye ilişkin verileri işlemeyi
meslek ve faaliyeti gereği yapan gerçek veya tüzel kişi"

Örnek: Pazarlama SaaS'ının kendisi (platform sağlayıcısı).

Sorumlulukları:

  • ✅ Verileri güvenli sakla (TDE, backup, access control)
  • ✅ Müşterinin talimatını uygula (data deletion vb.)
  • ✅ Alt-işleyenleri denetleyin (CDN, email service vb.)
  • ⚖️ Cezadan sorumlu: Madde 3, 12/1 ihlalleri (5 milyon - 10 milyon TL)

⚖️ HUKUKİ NOT: GDPR Article 28'de aynı mantık var. AB ve Türkiye arasında veri transferi varsa, her iki taraf da KVKK + GDPR'ye uymalıdır.

Kritik SaaS Senaryoları

Senaryo Veri Sorumlusu Veri İşleyen Sorun
Email gönderme platformu Müşteri (pazarlama şirketi) SaaS firması SaaS veri koruması sorumlu
CRM Müşteri (satış şirketi) SaaS + Email servisi Her ikisi de sorumlu
Analytics platform Müşteri (website sahibi) SaaS + Google Cloud SaaS veri transfer kontrolü sorumlu
Kurumsal PDF editor Müşteri (şirket) SaaS + S3 storage SaaS depolama güvenliği sorumlu

2. Multi-Tenant Mimaride Veri Sızıntısı Riski

Çoğu B2B SaaS, aynı database'de farklı müştereri (tenant) verilerini barındırır. Eğer tenant-isolation doğru değilse, felakete davetiye çıkarır:

Sızıntı Senaryosu: Şoşyal Medya Analiz SaaS

Tenant A (Sosyal Medya Ajansı A):
  - Üst müşterileri: Coca-Cola, Pepsi
  - Takip ettiği kullanıcı kimlikleri (IP, email, device ID) database'de

Tenant B (Sosyal Medya Ajansı B):
  - Üst müşterileri: Fanta, Sprite
  - Farklı kullanıcı verisi

Sorun: Tenant A'nın API key'i ile Tenant B'nin /users endpoint'ine eriş?

KVKK Madde 3 İhlali:

  • Veri Işleyen (SaaS), verileri "güvenli ortamda" saklamamıştır
  • Sorumlu kişi izni olmaksızın diğer müşterilerin verilerine eriş sağlanmıştır
  • Ceza: 500,000 TL - 10 milyon TL

3. Nasıl Entegre Edilir? — @veribenim/core Tenant-Level API

Veribenim, her tenant'ın kendi token'ı ile izole tercihler saklamasını sağlar.

Adım 1: Kurulum

npm install @veribenim/core @veribenim/react @veribenim/nextjs
// lib/tenantConsentManager.ts
import { init } from '@veribenim/core';

export class TenantConsentManager {
  private clients = new Map<string, ReturnType<typeof init>>();

  /**
   * Her tenant'ın kendi Veribenim instance'ı
   */
  getClient(tenantToken: string) {
    if (!this.clients.has(tenantToken)) {
      this.clients.set(tenantToken, init({
        token: tenantToken,
        lang: 'tr',
      }));
    }
    return this.clients.get(tenantToken)!;
  }

  /**
   * Tenant A'nın kullanıcısı için tercihler al
   */
  async getTenantConsent(tenantToken: string, endUserId: string) {
    const client = this.getClient(tenantToken);
    return await client.getPreferences(endUserId);
  }

  /**
   * Tenant B'nin tercihlerinde etki yapılamaz
   * (farklı token, farklı isolation)
   */
  async saveTenantConsent(
    tenantToken: string,
    endUserId: string,
    preferences: Record<string, boolean>
  ) {
    const client = this.getClient(tenantToken);
    return await client.savePreferences(endUserId, preferences);
  }

  /**
   * DSAR (Silme Talebi) — Tenant-scoped
   */
  async submitTenantDsar(
    tenantToken: string,
    payload: {
      requestType: 'access' | 'erasure' | 'portability';
      fullName: string;
      email: string;
      description: string;
    }
  ) {
    const client = this.getClient(tenantToken);
    return await client.submitDsar(payload);
  }
}

export const tenantManager = new TenantConsentManager();

Adım 3: Tenant Config ve Setup

// lib/tenant.ts
import { prisma } from '@/lib/prisma';

export async function getTenantConfig(tenantId: string) {
  const tenant = await prisma.tenant.findUnique({
    where: { id: tenantId },
    select: {
      id: true,
      name: true,
      veribenimToken: true,  // ← Kritik: Her tenant'ın kendi token'ı
      locale: true,
      privacyPolicy: true,
      consentCookies: true,
    },
  });

  if (!tenant) {
    throw new Error(`Tenant ${tenantId} bulunamadı`);
  }

  return tenant;
}

// Veritabanı Schema (örnek)
/*
CREATE TABLE tenants (
  id UUID PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  veribenimToken VARCHAR(255) NOT NULL UNIQUE,  -- Her tenant UNIQUE token
  locale VARCHAR(10) DEFAULT 'tr',
  privacyPolicy TEXT,
  consentCookies JSON,
  createdAt TIMESTAMP,
  updatedAt TIMESTAMP
);
*/

4. Nasıl Entegre Edilir? — Next.js SaaS Dashboard

Multi-tenant Next.js uygulamasında, dinamik routing + Veribenim tercihlerini birleştiriyoruz.

App Router Layout — Tenant-Scoped

// app/[tenantSlug]/layout.tsx
import { VeribenimProvider } from '@veribenim/nextjs';
import { getTenantConfig } from '@/lib/tenant';
import { notFound } from 'next/navigation';

export default async function TenantLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: { tenantSlug: string };
}) {
  let tenant;
  try {
    tenant = await getTenantConfig(params.tenantSlug);
  } catch (error) {
    notFound();
  }

  return (
    <VeribenimProvider
      config={{
        token: tenant.veribenimToken,
        lang: tenant.locale,
      }}
    >
      <div className="tenant-layout">
        <header>
          <h1>{tenant.name} - Gizlilik Kontrol Paneli</h1>
        </header>
        {children}
      </div>
    </VeribenimProvider>
  );
}
// app/[tenantSlug]/consent/page.tsx
'use client';

import { useVeribenim } from '@veribenim/react';
import { useParams } from 'next/navigation';
import { useState } from 'react';

export default function ConsentDashboard() {
  const params = useParams();
  const tenantSlug = params.tenantSlug as string;
  const { preferences, savePreferences, loading } = useVeribenim();
  const [saved, setSaved] = useState(false);

  if (loading) return <div>Tercihler yükleniyor...</div>;

  const handleToggle = async (category: string, value: boolean) => {
    const updated = {
      ...preferences,
      preferences: {
        ...(preferences?.preferences || {}),
        [category]: value,
      },
    };

    await savePreferences(updated);
    setSaved(true);
    setTimeout(() => setSaved(false), 2000);
  };

  return (
    <div className="consent-dashboard">
      <h2>Veri Gizlilik Tercihlerim</h2>

      <div className="consent-group">
        <h3>Gerekli Çerezler</h3>
        <p>Platform işletmek için zorunludur.</p>
        <label disabled>
          <input type="checkbox" checked disabled readOnly />
          Session & Güvenlik Çerezleri
        </label>
      </div>

      <div className="consent-group">
        <h3>Analitik & Performans</h3>
        <label>
          <input
            type="checkbox"
            checked={preferences?.preferences?.analytics ?? false}
            onChange={(e) => handleToggle('analytics', e.target.checked)}
          />
          Platform kullanımımı analiz etmeye izin ver
          <small>
            Sayfa yükleme hızı, tıklama tepkisi vb. ölçümleri iyileştirmek için
          </small>
        </label>
      </div>

      <div className="consent-group">
        <h3>Pazarlama</h3>
        <label>
          <input
            type="checkbox"
            checked={preferences?.preferences?.marketing ?? false}
            onChange={(e) => handleToggle('marketing', e.target.checked)}
          />
          Yeni özellikleri ve ilgili ürünleri öğren
          <small>
            Email bildirimleri alabilir, re-targeting reklamları görebilirsiniz
          </small>
        </label>
      </div>

      {saved && <p className="success-message">✓ Tercihler kaydedildi</p>}

      <button onClick={() => handleDataExport()}>
        Verilerim İndir (DSAR)
      </button>
      <button onClick={() => handleDataErasure()} className="danger">
        Verilerim Silinsin
      </button>
    </div>
  );
}

async function handleDataExport() {
  const response = await fetch('/api/[tenantSlug]/dsar/access', {
    method: 'POST',
  });
  const blob = await response.blob();
  const url = window.URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'verilerim.json';
  a.click();
}

async function handleDataErasure() {
  if (!confirm('Tüm verileriniz silinecek. Emin misiniz?')) return;
  await fetch('/api/[tenantSlug]/dsar/erasure', { method: 'POST' });
  alert('Silme isteği alındı, 30 gün içinde işlenir.');
}

5. Nasıl Entegre Edilir? — Laravel Multi-Tenant Backend

Laraven 11+ ile multi-tenant SaaS arka ucu kuruyoruz.

Adım 1: Package Kurulumu

composer require veribenim/laravel-sdk
composer require stancl/tenancy
php artisan tenancy:install

Adım 2: Tenant Veritabanı Modeli

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Tenant extends Model
{
    protected $fillable = [
        'name',
        'slug',
        'domain',
        'veribenim_token',      // ← Kritik: Her tenant'ın kendi token'ı
        'veribenim_region',     // Türkiye / EU
        'locale',
        'subscription_plan',
    ];

    public function users()
    {
        return $this->hasMany(User::class);
    }
}

Adım 3: Tenant-Scoped DSAR Controller

<?php

namespace App\Http\Controllers;

use Veribenim\Laravel\VeribenimFacade as Veribenim;
use App\Models\Tenant;
use Illuminate\Http\Request;

class TenantDsarController extends Controller
{
    /**
     * Tenant A'nın DSAR isteği (Tenant B'den izole)
     */
    public function submitRequest(Request $request, string $tenantSlug)
    {
        $tenant = Tenant::where('slug', $tenantSlug)
            ->firstOrFail();

        // 1. Tenant'ın kendi Veribenim client'ını oluştur
        $veribenimClient = new \Veribenim\VeribenimClient(
            token: $tenant->veribenim_token,
            region: $tenant->veribenim_region ?? 'tr',
        );

        // 2. DSAR isteğini gönder
        $response = $veribenimClient->submitDsar(
            requestType: $request->enum('type', ['access', 'erasure', 'portability']),
            fullName: $request->string('full_name'),
            email: $request->email('email'),
            description: sprintf(
                '[Tenant: %s] %s',
                $tenant->name,
                $request->string('description')
            )
        );

        // 3. Log kaydet
        \Log::info('[DSAR] Tenant isteği gönderildi', [
            'tenant_id' => $tenant->id,
            'tenant_name' => $tenant->name,
            'request_type' => $request->type,
            'veribenim_request_id' => $response['id'] ?? null,
        ]);

        return response()->json([
            'status' => 'received',
            'deadline' => now()->addDays(30),
            'request_id' => $response['id'] ?? null,
        ]);
    }

    /**
     * Tenant B'nin DSAR isteği — Tenant A'dan TAMAMEN İZOLE
     * (Aynı database'de, farklı token, farklı veri)
     */
    public function submitRequestForTenantB(Request $request)
    {
        // Tenant B'nin token'ı kullanılır, Tenant A'nın değil
        // Veritabanında farklı satırlara eriş
        // Hiçbir veri karışması olmaz
    }

    /**
     * Tenant'ın DSAR isteğinin durumunu kontrol et
     */
    public function checkStatus(string $tenantSlug, string $requestId)
    {
        $tenant = Tenant::where('slug', $tenantSlug)->firstOrFail();

        $veribenimClient = new \Veribenim\VeribenimClient(
            token: $tenant->veribenim_token
        );

        $status = $veribenimClient->getDsarStatus($requestId);

        return response()->json($status);
    }
}

Adım 4: Middleware — Tenant Validation

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class ValidateTenantAccess
{
    public function handle(Request $request, Closure $next)
    {
        $tenantSlug = $request->route('tenantSlug');
        $tenant = \App\Models\Tenant::where('slug', $tenantSlug)
            ->firstOrFail();

        // Authenticated kullanıcı bu tenant'a ait mi?
        if ($request->user()?->tenant_id !== $tenant->id) {
            return response()->json(['error' => 'Unauthorized'], 403);
        }

        // Tenant context'i request'e ata
        $request->merge(['tenant' => $tenant]);

        return $next($request);
    }
}

6. Data Sovereignty: Her Tenant'ın Verisi Kendi Alanında

GDPR Article 44 ve KVKK Madde 9, kişiye ilişkin verilerin sınır ötesi transferini kısıtlar.

Sorun: Multi-Region SaaS

Tenant A (TR müşteri):
  - Verileri AWS Frankfurt'a gönder (AB veri savunması: ✅ İzin)

Tenant B (JP müşteri):
  - Verileri AWS Tokyo'ya gönder (Yetersiz veri koruması: ❌ Risk)

Tenant C (US müşteri):
  - Verileri AWS Virginia'ya gönder (Privacy Shield yok: ❌ Riskli)

Çözüm: Veribenim Regional Tokens

Veribenim, her tenant'ın bölge tercihini respekt eder:

// Tenant config
$tenant = Tenant::find(1);

$tenant->update([
    'veribenim_region' => 'tr', // Verileri Türkiye'de tut
]);

// SDK otomatik routing
$veribenim = new Veribenim\VeribenimClient(
    token: $tenant->veribenim_token,
    region: $tenant->veribenim_region // ← Lokalisasyon
);

// Aynı tenant için EU veri?
$euTenant = Tenant::find(2);
$euTenant->update([
    'veribenim_region' => 'eu', // EU GDPR veri merkezi
]);

7. Compliance as a Service: SaaS Firmaları İçin Hazır Uyumluluk

Veribenim, SaaS'ların "hazır uyumluluk" paketlerini sunar:

Package 1: Tenant Isolation

✅ Her tenant kendi Veribenim token'ı
✅ Database-level isolation (Row-Level Security)
✅ API key rotation (otomatik)
✅ Audit logging (her işlem loglanır)

Package 2: DSAR Pipeline

✅ Veri İndir (access request)
✅ Veri Sil (erasure request)
✅ Veri Taşı (portability request)
✅ Otomatik 30 günlük deadline tracking
✅ KVKK + GDPR uyumlu raporlama
✅ Tenant-level tercih saklama
✅ End-user tercih yönetimi
✅ Cookie banner integration
✅ Otomatik consent renewal (yıllık)

8. Pratik Örnek: B2B Email Marketing SaaS

Senaryo: EmailBlast adlı bir email pazarlama platformu.

Tenant A: Coca-Cola'nın Marketing Agency'si

Veribenim Token: pk_live_tenant_a_xyzabc
Bölge: TR (Türkiye veri merkezi)

Müşteri Listesi:
  - 50 milyon Türkiye telefon numarası
  - SMS + Email konsentleri

EmailBlast, Coca-Cola'nın verilerini:
  - 30 gün saklar (kampanya bitince siler)
  - Başkasına satmaz (izolasyon)
  - Analytics'e vermesfr (tenant-scoped)

Tenant B: Pepsi'nin Marketing Agency'si

Veribenim Token: pk_live_tenant_b_def456
Bölge: EU (GDPR veri merkezi)

Müşteri Listesi:
  - 80 milyon EU email adresi
  - GDPR uyumlu onaylar

EmailBlast, Pepsi'nin verilerini:
  - 60 gün saklar (sözleşmeye göre)
  - Coca-Cola'dan izole (Tenant A'nın API key'i Tenant B'ye erişemez)
  - EU veri merkezinde (GDPR compliant)

Sorun: API Sızıntısı

Eğer EmailBlast'ın API architecture yanlışsa:

GET /api/subscribers?api_key=tenant_a_key
→ Tenant B'nin verilerini dönebilir (hata!)

Çözüm: Veribenim tenant-isolation
GET /api/[tenantSlug]/subscribers
→ Middleware otomatik olarak tenant kontrol eder
→ Tenant A'nın API key'i Tenant B'ye erişemez
→ KVKK Madde 3 ihlali önlenir

9. Sık Yapılan Hatalar

Hata 1: Tüm Tenantlar İçin Aynı Veribenim Token

// ❌ YANLIŞ
const VERIBENIM_TOKEN = 'pk_live_shared_token';

class TenantConsentManager {
    getClient(tenantSlug) {
        // Tenant A ve B aynı token'ı kullanır!
        return init({ token: VERIBENIM_TOKEN });
    }
}

Sorun: Multi-tenant ayrımı yok, tüm veriler karışabilir.

// ✅ DOĞRU
class TenantConsentManager {
    getClient(tenantSlug) {
        const tenant = Tenant.find(tenantSlug);
        // Her tenant'ın kendi token'ı
        return init({ token: tenant.veribenim_token });
    }
}

Hata 2: Müşteri Tercihlerini Sadece Frontend'de Tutma

// ❌ YANLIŞ
const [preferences, setPreferences] = useState({
  marketing: true,
  analytics: false,
});
// ... tercihler localStorage'e kaydedilir
// ... Sunucuya sorgulanmaz
// ... Veri Sorumlusu (B2B müşteri) tercihlerden habersiz

Sorun: Backend validasyonu yok, B2B müşteri politikasını uygulayamaz.

// ✅ DOĞRU
const { preferences, savePreferences } = useVeribenim();
// Veribenim API'si tercihler kaydeder
// Backend tercih validasyonu yapar
// B2B müşteri tercihler raporunu alabilir

Hata 3: DSAR İsteğini Ignore Etme

// ❌ YANLIŞ
public function submitDsar(Request $request) {
    // İstek alındı, ama işlem yapılmadı
    return response()->json(['status' => 'ok']);
}

Sorun: KVKK Madde 11 ihlali, 50,000 TL - 500,000 TL ceza.

// ✅ DOĞRU
public function submitDsar(Request $request) {
    Veribenim::submitDsar(
        requestType: $request->type,
        // ... detaylar
    );

    // Lokal silme işlemleri
    if ($request->type === 'erasure') {
        User::where('id', $request->user_id)->delete();
        UserActivity::where('user_id', $request->user_id)->delete();
    }

    return response()->json([
        'status' => 'received',
        'deadline' => now()->addDays(30),
    ]);
}

10. Checklist: KVKK Uyumlu Multi-Tenant SaaS

  • Her tenant kendi Veribenim token'ına sahip
  • Tenant isolation middleware uygulanmış
  • API key routing tenant-scoped
  • DSAR pipeline entegre edilmiş
  • Veri retention politikası tanımlanmış
  • Veri İşlem Sözleşmesi (DPA) hazır
  • B2B müşterilere uyumluluk raporu sağlanıyor
  • End-user tercih yönetimi sunuluyor
  • Audit logging aktif (her işlem kaydedilir)
  • Yıllık uyumluluk denetimi yapılıyor

11. Veribenim Multi-Tenant Avantajları

Özellik Sağlanan Avantaj
Token per Tenant Tam izolasyon, data sızıntısı riski sıfır
Automatic Isolation Middleware-level protection, developer hatasını önler
DSAR Pipeline 30 günde compliance, otomatik raporlama
Regional Storage GDPR + KVKK uyumlu veri lokasyonu
Consent Management B2B müşteri politikasını end-user tercihine uygulanır
Audit Logs Denetçiye hazır, tamamen görünürlük
Zero-Knowledge Veribenim mühendisleri verilerinizi asla görmez

Kaynaklar ve Linkler

  • KVKK Madde 2: Kişiye ilişkin veri işlemesinin tanımı
  • KVKK Madde 3: Veri güvenliği
  • KVKK Madde 11: Veri Sahibinin Hakları (DSAR)
  • GDPR Article 28: Data Processor responsibilities
  • GDPR Article 44: Data transfers rules
  • Kişisel Verileri Koruma Kurulu Serbest Bölgeye İlişkin Kararı: Multi-region guidance

📚 Teknik Dokümantasyon: Tüm SDK referans dökümanlarına veribenim.com/documents adresinden ulaşabilirsiniz.

Sonraki Adım

Veribenim'in bu sektör için sunduğu çözümleri görmek ve KVKK/GDPR uyumlu pazarlama başlamak için aşağıdaki butona tıklayın.

Ücretsiz Hesap Oluştur

Benzer Makaleler

Bu sektörle ilgili daha fazla makale yakında yayımlanacak.

Tüm makaleleri görmek için bloğa dön →