Bir CRM'de "veriyi ne kadar saklıyoruz?" sorusu genelde bir politika belgesiyle cevaplanır ve orada kalır. Belgede "pasif lead kayıtları iki yıl sonra anonimleştirilir" yazar, ama veritabanında bu cümleyi uygulayan tek satır kod yoktur. Üç yıl sonra biri yedekten eski bir tabloyu geri yüklediğinde, silindiği sanılan telefon numaraları da geri gelir.

Ben Koray Yalçın; yıllardır lead yönetimi, CRM kurulumu ve gayrimenkul satış operasyonları üzerine çalışıyorum. Bu yazıda data retention'ı, yani veri saklama süresini, bir geliştiricinin gözünden ele alıyorum: politikayı koda nasıl çevirirsiniz, saklama işini nasıl güvenli çalıştırırsınız, yasal tutma (legal hold) istisnasını nasıl modellersiniz, yedeklerdeki kopyalarla ne yaparsınız ve tüm bunların gerçekten çalıştığını nasıl kanıtlarsınız. Politikanın içeriği, yani hangi verinin kaç yıl tutulacağı, hukuk ekibinizin işidir; bu yazı hukuki görüş değildir. Benim derdim, karar verildikten sonra o kararın sistemde gerçekten uygulanması.

Politika belgesi neden yetmez?

Saklama politikalarının sahada çökmesinin üç tipik nedeni var.

Birincisi, kural insan hafızasına bırakılır. "Ayda bir eski kayıtları temizleriz" diye başlanır, iki ay sonra unutulur. İkincisi, kural tek bir tabloya uygulanır ama veri on yere kopyalanmıştır: ana leads tablosu temizlenir, fakat arama kayıtları, WhatsApp mesaj geçmişi, dışa aktarılmış CSV'ler, analitik ambarı ve hata loglarında aynı kişinin bilgisi durmaya devam eder. Üçüncüsü, silme işi bir kez yanlış çalışır ve fazla veri siler; ekip korkup işi tamamen kapatır.

Bu üç sorunun çözümü aynı yere çıkıyor: saklama kuralını veri olarak tutmak, tek bir iş tarafından uygulamak ve her çalıştırmayı kayda geçirmek.

Kuralı kod değil veri olarak tutun

Saklama süresini if bloklarına gömmek yerine bir tabloda tutmak, hem değişikliği kolaylaştırır hem de denetimde "kural neydi?" sorusuna net cevap verir.

CREATE TABLE retention_rule ( id SERIAL PRIMARY KEY, entity TEXT NOT NULL, -- 'lead', 'call_recording', 'message', 'export_file' condition_key TEXT NOT NULL, -- 'stage=lost', 'stage=won', 'no_activity' anchor_field TEXT NOT NULL, -- süre hangi tarihten sayılır: 'last_activity_at', 'closed_at' keep_days INTEGER NOT NULL, action TEXT NOT NULL CHECK (action IN ('anonymize','delete','archive')), version INTEGER NOT NULL, approved_by TEXT NOT NULL, effective_from DATE NOT NULL, UNIQUE (entity, condition_key, version) ); 

Burada iki ayrıntı önemli. İlki anchor_field: sürenin neye göre sayıldığı. Kaybedilmiş bir lead için çoğu zaman oluşturulma tarihi değil son temas tarihi anlamlıdır; satılmış bir konutun müşteri kaydı için ise satışın kapandığı tarih. Bu çapayı açıkça yazmazsanız, her geliştirici farklı bir tarih kullanır. İkincisi version: kural değiştiğinde eski satırı güncellemezsiniz, yeni sürüm eklersiniz. Böylece bir kaydın neden o tarihte anonimleştirildiğini geriye dönük açıklayabilirsiniz.

action alanındaki üç seçenek de bilinçli bir ayrım. Gayrimenkul CRM'inde satılmış dairenin sözleşme bağlantılı müşteri kaydını silemezsiniz; ama ilgilenip almamış bir adayın telefonunu iki yıl sonra tutmanın çoğu zaman bir gerekçesi kalmaz. Anonimleştirme, kişiyi tanımlayan alanları kaldırıp istatistik için gerekli olanları (kanal, proje, aşama, tarih) bırakır. Böylece "geçen yıl hangi kampanyadan kaç lead geldi?" raporu bozulmaz.

Hangi alan kişisel, hangisi değil: alan sınıflandırması

Anonimleştirmenin doğru çalışması, hangi kolonun kişisel veri olduğunu bilmeye bağlı. Bunu da yine bir kayıt defterinde tutmanızı öneririm:

CREATE TABLE field_classification ( entity TEXT NOT NULL, field TEXT NOT NULL, category TEXT NOT NULL CHECK (category IN ('identifier','contact','free_text','business','technical')), on_anonymize TEXT NOT NULL CHECK (on_anonymize IN ('null','hash','redact','keep')), PRIMARY KEY (entity, field) ); 

free_text kategorisini özellikle vurgulamak istiyorum. Satış danışmanlarının not alanına "Ahmet Bey'in eşi Ayşe Hanım 0533... numarasından aradı" gibi cümleler yazdığını hepimiz biliyoruz. Ad ve telefon kolonlarını boşaltıp not alanını bırakırsanız, anonimleştirme kâğıt üzerinde kalır. Serbest metin alanlarında ya tamamını kaldırmak ya da desen tabanlı bir temizleme (telefon, e-posta, TC kimlik deseni) uygulamak gerekir; ben genelde tamamen kaldırmayı ve notun yalnızca uzunluğunu ve tarihini bırakmayı tercih ediyorum.

Yeni bir kolon eklendiğinde bu tabloda karşılığı yoksa CI'ın hata vermesi de iyi bir alışkanlık. Basit bir test, şemadaki her kolonun sınıflandırıldığını kontrol edebilir. Böylece altı ay sonra eklenen secondary_phone alanı sessizce saklama kuralının dışında kalmaz.

Saklama işini güvenli çalıştırmak

Kural ve sınıflandırma hazır olduğunda geriye bunları uygulayan iş kalır. Burada hız değil güvenlik önceliklidir. Benim kullandığım iskelet şöyle:

def run_retention(entity, rule, dry_run=True, batch_size=500, max_total=20000): run_id = start_run(entity, rule.id, rule.version, dry_run) cutoff = today() - timedelta(days=rule.keep_days) processed = 0 while processed < max_total: ids = select_candidates(entity, rule, cutoff, limit=batch_size) ids = [i for i in ids if not under_legal_hold(entity, i)] if not ids: break if dry_run: log_candidates(run_id, ids) else: with transaction(): apply_action(entity, rule.action, ids) # anonymize / delete / archive write_tombstones(run_id, entity, ids) enqueue_downstream(entity, ids) # outbox: diğer sistemlere yay processed += len(ids) finish_run(run_id, processed, hit_limit=(processed >= max_total)) 

Bu iskeletteki her parça bir hatadan ders alınarak eklenmiş durumda.

Kuru çalıştırma (dry run) varsayılan olmalı. Yeni bir kural ya da yeni bir sürüm devreye girdiğinde iş önce sadece "şu kayıtlar etkilenecekti" listesini üretir. Bu listeyi bir insan gözden geçirdikten sonra gerçek mod açılır.

Toplu işleme ve üst sınır (max_total) şart. Bir kuralda keep_days yanlışlıkla 730 yerine 73 girildiğinde, üst sınır olmayan bir iş bütün tabloyu bir gecede anonimleştirebilir. Üst sınıra takılan bir çalıştırma alarm üretmeli ve otomatik devam etmemeli.

Mezar taşı (tombstone) kaydı, silinen veya anonimleştirilen kaydın kimliğini, hangi kural sürümüyle ve hangi çalıştırmada işlendiğini tutar; kişisel veri içermez. Bu kayıt iki işe yarar: denetimde kanıt olur ve yedekten geri yükleme sonrası temizliğin tekrar uygulanmasını sağlar.

Aşağı akış kuyruğu (outbox) ise asıl unutulan kısım. Ana veritabanında anonimleştirdiğiniz kişi, reklam kitlelerinde, e-posta aracında, analitik ambarında ve WhatsApp sağlayıcısında yaşamaya devam ediyorsa iş yarım kalmıştır. Aynı işlem içinde outbox'a yazılan olaylar, her hedef sistem için ayrı bir tüketici tarafından işlenir ve başarısız olanlar yeniden denenir.

Yasal tutma (legal hold) istisnası

Bazen saklama süresi dolmuş bir kaydı silmemeniz gerekir: devam eden bir uyuşmazlık, bir şikâyet incelemesi, bir resmi talep. Bu durumu her geliştiricinin kendi koduna if customer_id == 4812 diye yazması felakettir. Ayrı bir tablo kullanın:

CREATE TABLE legal_hold ( id SERIAL PRIMARY KEY, entity TEXT NOT NULL, entity_id BIGINT NOT NULL, reason TEXT NOT NULL, opened_by TEXT NOT NULL, opened_at TIMESTAMPTZ NOT NULL DEFAULT now(), review_at DATE NOT NULL, closed_at TIMESTAMPTZ ); CREATE INDEX ON legal_hold (entity, entity_id) WHERE closed_at IS NULL; 

review_at alanı zorunlu olmalı. Açılıp unutulan tutmalar, saklama politikasının sessizce delinmesinin en yaygın yoludur. Gözden geçirme tarihi geçmiş açık tutmalar için haftalık bir rapor üretin. Tutma kapandığında kayıt bir sonraki saklama çalıştırmasında normal kurala döner; ayrıca bir işlem yapmanız gerekmez.

Yedekler ve kopyalar: kripto-parçalama (crypto-shredding)

En zor soru yedeklerdir. Günlük yedekleri otuz gün, aylık yedekleri bir yıl tutuyorsanız, bugün anonimleştirdiğiniz kişi yedeklerin içinde bir yıl daha durur. Yedekleri tek tek açıp düzenlemek pratik değildir.

İki yaklaşım var ve çoğu zaman ikisini birlikte kullanıyorum.

Birincisi, geri yükleme sonrası yeniden uygulama. Yedekten geri dönüldüğünde, mezar taşı tablosu (ki ayrı ve daha uzun süre saklanır) okunur ve oradaki tüm kayıtlara anonimleştirme tekrar uygulanır. Bunu geri yükleme prosedürünün zorunlu bir adımı yapın; elle hatırlanmasını beklemeyin.

İkincisi, kişi başına anahtarla şifreleme. Telefon, e-posta ve ad gibi alanlar kişiye özel bir veri anahtarıyla şifrelenir; anahtarlar ayrı bir anahtar deposunda durur. Saklama süresi dolduğunda anahtarı silersiniz. Yedeklerde şifreli veri kalsa bile artık okunamaz.

def shred_subject(subject_id): key_ref = key_store.get_ref(subject_id) key_store.destroy(key_ref) # geri dönüşsüz db.execute("UPDATE subject SET key_ref = NULL, shredded_at = now() WHERE id = %s", [subject_id]) audit("subject_shredded", subject_id=subject_id, key_ref=key_ref) 

Kripto-parçalamanın bir bedeli var: şifreli alanlarda arama ve tekilleştirme zorlaşır. Bu yüzden arama için normalleştirilmiş değerin karma (hash) hâlini ayrı bir kolonda tutmak ve onu da saklama kuralına dahil etmek gerekir. Küçük bir CRM için bu mimari ağır gelebilir; o durumda kısa yedek süresi ve geri yükleme sonrası yeniden uygulama çoğu ihtiyacı karşılar.

Unutulan kopyalar listesi

Saklama işinin kapsamını belirlerken her ekipte şu listeyi tek tek kontrol ediyorum:

  • Dışa aktarılmış CSV ve Excel dosyaları (ortak sürücüler, e-posta ekleri).
  • Uygulama ve hata logları; özellikle istek gövdesini loglayan ara katmanlar.
  • Arama kayıtları ve çağrı merkezi transkriptleri.
  • WhatsApp ve SMS sağlayıcılarındaki mesaj geçmişi.
  • Analitik ambarı ve oradan türetilen tablolar.
  • Reklam platformlarına yüklenmiş özel kitleler.
  • Test ve geliştirme ortamlarına kopyalanmış üretim verisi.
  • Kuyruklardaki ölü mektup (DLQ) mesajları.

Bu listedeki her kalem için "sahibi kim, saklama süresi ne, silme nasıl tetikleniyor" sorularının cevabı yazılı olmalı. Cevabı olmayan kalem, politikanın deliğidir.

Gerçekten çalıştığını kanıtlamak

Saklama işi sessiz çalışan bir iştir; çalışmadığını genelde bir denetimde ya da bir şikâyette öğrenirsiniz. Bunu önlemek için üç kontrol kuruyorum.

İlki, çalıştırma kaydı ve sapma alarmı. Her çalıştırma retention_run tablosuna başlangıç, bitiş, işlenen kayıt sayısı ve üst sınıra takılıp takılmadığı bilgisiyle yazılır. Bir kural için beklenen hacim birden sıfıra düşerse ya da birden on katına çıkarsa alarm üretilir.

İkincisi, bağımsız doğrulama sorgusu. Saklama işinden ayrı yazılmış basit bir sorgu, "süresi dolmuş, tutmada olmayan ama hâlâ kişisel verisi duran kayıt var mı?" diye bakar:

SELECT count(*) FROM lead l JOIN retention_rule r ON r.entity = 'lead' AND r.condition_key = 'stage=lost' WHERE l.stage = 'lost' AND l.last_activity_at < now() - (r.keep_days || ' days')::interval AND l.phone IS NOT NULL AND NOT EXISTS (SELECT 1 FROM legal_hold h WHERE h.entity = 'lead' AND h.entity_id = l.id AND h.closed_at IS NULL); 

Bu sayı sıfır değilse iş ya çalışmıyor ya da bir koşulu kaçırıyor demektir.

Üçüncüsü, aylık örneklem. Rastgele birkaç anonimleştirilmiş kaydı açıp serbest metin alanlarında, ilişkili tablolarda ve aşağı akış sistemlerinde iz kalıp kalmadığına bakmak. Otomasyonun yakalayamadığı şeyleri genelde bu elle kontrol gösterir.

Gayrimenkul CRM'inden bir senaryo

Kıbrıs'ta konut projesi satan bir firmayı düşünün. Lansman döneminde Meta ve Google reklamlarından binlerce lead gelir; bunların küçük bir kısmı satışa döner. Kaybedilen adaylar için kural "son temastan 730 gün sonra anonimleştir" olsun, satılmış müşteriler için ise sözleşme kaydı ayrı tutulur ve anonimleştirilmez.

Bir aday, satıştan vazgeçtikten sonra şikâyet başvurusu yapmış olsun. Hukuk ekibi bu kayıt için yasal tutma açar, gözden geçirme tarihini altı ay sonrasına koyar. Saklama işi her gece çalışır, bu adayı atlar; diğer süresi dolmuş adayları anonimleştirir, mezar taşı yazar ve outbox üzerinden reklam kitlelerinden ve WhatsApp sağlayıcısından çıkarma olaylarını gönderir. Kampanya raporları bozulmaz, çünkü kanal, proje ve aşama bilgisi duruyor. Altı ay sonra tutma kapanınca kayıt bir sonraki çalıştırmada normal kurala döner.

Bu senaryodaki hiçbir adım bir insanın bir şeyi hatırlamasına bağlı değil. Saklama politikasının sahada yaşamasının tek yolu bu.

Saklama süresinin kavramsal tanımı ve KVKK bağlamı için sitemdeki data retention nedir sözlük maddesine bakabilirsiniz; bu yazı onun uygulama tarafını anlatıyor.

Sık Sorulan Sorular

Silmek mi, anonimleştirmek mi daha doğru?
Kişisel veriye artık ihtiyaç yoksa ama kaydın istatistiksel değeri varsa anonimleştirme çoğu zaman daha iyi bir denge sunar. Hangisinin hukuken yeterli olduğu ise sizin politikanıza ve hukuk ekibinizin değerlendirmesine bağlıdır. Teknik olarak önemli olan, anonimleştirmenin gerçekten geri döndürülemez olması: karma değerler tuzsuz ve tahmin edilebilir girdilerden üretiliyorsa, telefon numarası gibi alanlar kolayca geri bulunabilir.

Saklama işi ne sıklıkla çalışmalı?
Günlük çalıştırma çoğu CRM için yeterli. Önemli olan sıklıktan çok her çalıştırmanın kayda geçmesi ve bir gece çalışmadığında bunun fark edilmesi.

Kural değiştiğinde geçmiş kayıtlar ne olacak?
Yeni sürüm eklendiğinde ilk çalıştırmayı kuru modda yapın. Süre kısalıyorsa birden çok kaydı etkileyebilir; üst sınır ve gözden geçirme bu noktada sizi korur. Süre uzuyorsa, zaten anonimleştirilmiş kayıtlar geri gelmez; bunu ekibe açıkça söyleyin.

Yasal tutmadaki bir kayıt diğer sistemlerde de korunmalı mı?
Evet. Tutma bilgisi yalnızca ana veritabanında kalırsa, aşağı akış sistemlerindeki kendi saklama kuralları veriyi silebilir. Tutma açıldığında outbox üzerinden ilgili sistemlere de bildirim gitmesi gerekir.

Küçük bir ekip için en az ne yapılmalı?
Kural tablosu, alan sınıflandırması, kuru modla başlayan günlük bir iş, mezar taşı kaydı ve bağımsız doğrulama sorgusu. Kripto-parçalama ve gelişmiş aşağı akış yayılımı sonraki adım olabilir; ama dışa aktarılmış dosyalar ve loglar listesi ilk günden kapsama girmeli.

Koray Yalçın; lead yönetimi, CRM mimarisi, dijital pazarlama ve gayrimenkul teknolojileri (PropTech) üzerine çalışıyor ve yazıyor.