Jetimza Brand Logo
v1.3  ·  Veritabanı Tasarımı

Jetimza MVP Veritabanı Tasarımı

23 tablo, 221 kolon ve 25 ilişkiden oluşan MVP şeması: varlık-ilişki diyagramı, kolon kolon açıklamalar, kısıtlar, indeksler ve enum sözlüğü. Jetimza MVP — Teknik Strateji & API Sözleşmesi v1.0 dokümanının veri katmanı karşılığıdır.

DurumÇalışma Dokümanı
Versiyonv1.3
VeritabanıPostgreSQL
ErişimEF Core · Controller → DbContext
ZamanUTC · timestamptz
Şemanın Temel Sözü

Bakiye bir sayaç değil, hareketlerden hesaplanan bir değerdir; her işlem ve her token hareketi silinmeden, değiştirilmeden saklanır. Böylece "bu kullanıcının bakiyesi neden bu?" sorusunun cevabı her zaman veritabanından okunabilir.

İlkeler & Konvansiyonlar

İsimlendirme

  • Tablo adları çoğul ve snake_case: users, token_grants.
  • Kolon adları snake_case; yabancı anahtarlar <tekil_tablo>_id kalıbında: user_id, grant_id.
  • Zaman kolonları fiil-geçmiş + _at: created_at, consumed_at, revoked_at. Böylece "null ise henüz olmadı" okunuşu tutarlı olur.
  • Boolean kolonlardan mümkün olduğunca kaçınılır; bunun yerine olay zamanı tutulur (revoked_at > is_revoked) — hem durumu hem zamanı verir.

Anahtar tipi

  • Dışarıya görünen varlıklar uuid birincil anahtar kullanır (users, transactions, token_grants…). API'de sıralı sayı sızdırmak, kullanıcı ve işlem hacmini dışarıya ifşa eder; ayrıca kaynak kimliği tahmin edilebilir hale gelir.
  • Yalnız içeride büyüyen, sıralı okunan append-only tablolar bigint kimlik kullanır (token_ledger, transaction_events). Sıra numarası burada bir özelliktir: hareketleri doğal kronolojik sırada okumak ve imleçli sayfalama yapmak için kullanılır.

Zaman

Tüm zaman kolonları timestamptz tipindedir ve UTC tutar. Uygulama katmanı hiçbir yere yerel saat yazmaz. Yalnızca iş takvimi hesapları (aylık token paketinin son geçerlilik anı) yapılandırılabilir bir referans saat diliminde hesaplanır ve sonuç UTC olarak saklanır.

Neden timestamptz, timestamp değil

timestamp (saat dilimsiz) tipi, aynı değeri okuyan iki farklı sunucuda farklı ana karşılık gelir. Ürün ileride birden çok bölgeye dağıtıldığında bu, sessiz ve geriye dönük düzeltilmesi zor bir veri hatasına dönüşür. timestamptz mutlak anı saklar; sunucunun saat dilimi değişse bile anlam değişmez.

Append-only tablolar

token_ledger ve transaction_events tablolarına yalnızca ekleme yapılır. Güncelleme ve silme uygulama tarafında yasaktır; ileride veritabanı seviyesinde de kural ile engellenmesi önerilir (Bölüm 11). Bir hareketin yanlış olduğu anlaşılırsa satır düzeltilmez — ters kayıt eklenir.

Kişisel veri (KVKK)

  • Kimlik numarası şifreli saklanır. Aydınlatma metni kapsamındadır. Düz metin hiçbir yerde bulunmaz; arama için anahtarlı özet kullanılır, log'a asla yazılmaz, API yanıtlarında yalnızca maskeli döner.
  • OTP kodu düz metin saklanmaz; anahtarlı özet (HMAC) olarak tutulur.
  • Oturum yenileme jetonu da özet olarak tutulur; veritabanı sızsa bile jetonlar kullanılamaz.
  • Belge içerikleri veritabanında değil, nesne depolamada tutulur; şemada yalnızca erişim anahtarı bulunur.
  • Kullanılmayan kişisel veri kolonu açılmaz. "İleride gerekebilir" gerekçesiyle boş kolon tutmak veri minimizasyonu ilkesine aykırıdır.

Varlık-İlişki Diyagramı

① KİMLİK & ERİŞİM ② SÖZLEŞME & İZİN ③ JET TOKEN ④ DAVET & GERİ BİLDİRİM ⑤ İŞLEM & DENETİM users id PK uuid phone_e164 UQ text username UQ text status text phone_verified_at ts created_at ts updated_at ts last_login_at ts deleted_at ts suspended_reason text suspended_until ts national_id_enc bytea national_id_hash bytea national_id_confirmed_at ts otp_challenges id PK uuid phone_e164 text purpose text code_hash bytea attempt_count int2 max_attempts int2 expires_at ts consumed_at ts created_ip inet created_at ts refresh_tokens id PK uuid user_id FK uuid token_hash UQ bytea issued_at ts expires_at ts revoked_at ts replaced_by FK uuid user_agent text ip inet user_consents id PK uuid user_id FK uuid document_id FK uuid accepted_at ts ip inet user_agent text revoked_at ts UQ (user_id, document_id) consent_documents id PK uuid document_type text version text effective_from ts content_url text UQ (document_type, version) token_grants id PK uuid user_id FK uuid kind text amount int remaining int granted_at ts expires_at ts source_type text source_id uuid idempotency_key UQ text token_ledger id PK int8 user_id FK uuid grant_id FK uuid delta int reason text ref_type text ref_id uuid balance_after int idempotency_key UQ text correlation_id text created_at ts token_requests id PK uuid user_id FK uuid amount int status text granted_grant_id FK uuid note text decided_by text decided_at ts created_at ts UQ (user_id) WHERE status='pending' invites id PK uuid user_id FK UQ uuid code UQ text created_at ts disabled_at ts invite_redemptions id PK uuid invite_id FK uuid invited_user_id FK UQ uuid redeemed_at ts reward_grant_id FK uuid ip inet fraud_flag bool feedback id PK uuid user_id FK uuid category text subject text body text status text internal_note text created_at ts transactions id PK uuid user_id FK uuid type text status text document_name text document_size int8 document_sha256 bytea document_short_code text source_object_key text result_object_key text signgate_request_ref UQ text signgate_request_id uuid hold_ledger_id FK int8 commit_ledger_id FK int8 attempt_count int2 worker_instance text correlation_id text trace_id text client_ip inet user_agent text created_at ts started_at ts completed_at ts expires_at ts failure_code text failure_message text transaction_signatures transaction_id PK FK uuid channel text format text kind text thumbprint text visible_signature bool certificate_subject text certificate_serial text certificate_identity_no text signing_time ts transaction_verifications transaction_id PK FK uuid detected_format text is_valid bool signature_count int valid_signature_count int result jsonb transaction_timestamps transaction_id PK FK uuid tsa_provider text tsa_serial text visible_stamp bool stamped_at ts transaction_events id PK int8 transaction_id FK uuid from_status text to_status text source text payload jsonb created_at ts phone_e164 · FK YOK user_id user_id document_id user_id user_id grant_id user_id granted_grant_id user_id · 1—1 invite_id invited_user_id · 1—1 reward_grant_id user_id user_id hold_ledger_id · commit_ledger_id transaction_id 1—1 1—1 1—1 Gosterim bir — cok iliskisi (FK) bir — bir iliskisi (UQ FK) mantiksal bag — FK yok PK birincil FK yabanci UQ tekil
users şemanın merkezidir; kullanıcıya ait her kayıt ona bağlanır. Diyagram beş işlevsel öbekte okunur: ① Kimlik & Erişim (giriş ve oturum), ② Sözleşme & İzin (hangi metnin hangi sürümünü kim onayladı), ③ Jet Token (bakiye kaynağı, hareket kaydı, ek talep), ④ Davet & Geri Bildirim, ⑤ İşlem & Denetim (imza / doğrulama / zaman damgası ve durum geçişleri). otp_challenges tablosunun users'a yabancı anahtarı yoktur — kesikli çizgi bunu gösterir; gerekçe Bölüm 04'te.

Şema 2 — Operasyon, Denetim & Ölçüm

Yirmi üç tabloyu tek diyagrama sıkıştırmak okunabilirliği yok eder. Çekirdek iş şeması yukarıda; aşağıdaki ikinci şema servisi işletmek için gereken tabloları taşır. Kesikli kutular birinci şemadaki tablolara yapılan dış referanslardır; bu bağların tam listesi Bölüm 03'teki ilişki envanterindedir.

⑦ İLETİŞİM ⑧ SAKLAMA & TEKRAR-GÜVENLİĞİ ⑨ DENETİM & ÖLÇÜM otp_challenges ↗ ① Kimlik & Erişim users ↗ ① Kimlik & Erişim transactions ↗ ⑤ İşlem & Denetim users · transactions ↗ ① / ⑤ users ↗ ① Kimlik & Erişim sms_deliveries id PK uuid otp_challenge_id FK uuid phone_e164 text provider text provider_message_id text status text error_code text cost_micros int requested_at ts sent_at ts delivered_at ts correlation_id text notifications id PK uuid user_id FK uuid channel text template_key text payload jsonb status text failure_reason text correlation_id text created_at ts sent_at ts stored_objects id PK uuid object_key UQ text bucket text size_bytes int8 sha256 bytea content_type text owner_type text owner_id uuid created_at ts delete_after ts deleted_at ts api_idempotency_keys id PK uuid idempotency_key text user_id FK uuid endpoint text request_hash bytea response_status int2 response_body jsonb transaction_id FK uuid created_at ts expires_at ts UQ (user_id, endpoint, idempotency_key) daily_user_stats id PK uuid user_id FK uuid date date signatures_started int signatures_succeeded int timestamps_total int verifications_total int tokens_granted int tokens_spent int first_activity ts last_activity ts UQ (user_id, date) daily_funnel_stats date PK date otp_requested int otp_verified int registered int first_signature int invites_accepted int audit_events id PK int8 actor_type text actor_id text action text target_type text target_id text before jsonb after jsonb ip inet user_agent text correlation_id text created_at ts otp_challenge_id user_id owner_type + owner_id user_id · transaction_id user_id
Bu tablolar iş kuralı taşımaz; servisin kendi sağlığını, maliyetini ve geçmişini bilmesini sağlar. sms_deliveries OTP başarı oranını ölçülebilir kılar, stored_objects dosya çöpünü toplar, api_idempotency_keys tekrar edilen isteklerin çift işlem doğurmasını engeller, audit_events yönetsel eylemleri kayda alır, daily_*_stats ürün ölçütlerini ham tabloları taramadan verir.

İlişki Envanteri

Şemadaki tüm bağlar aşağıdadır. Silme davranışı kolonu, ana kayıt silindiğinde bağlı kayıtlara ne olacağını belirtir.

Kaynak → HedefKolonTipSilmeAnlam
otp_challenges ⇢ usersphone_e164mantıksalDoğrulama, kullanıcı oluşmadan önce başlar; bu yüzden yabancı anahtar yoktur
refresh_tokens → usersuser_idN—1CASCADEKullanıcının açık oturumları
refresh_tokens → refresh_tokensreplaced_byN—1SET NULLJeton rotasyon zinciri (kendine referans)
user_consents → usersuser_idN—1CASCADEKullanıcının onayları
user_consents → consent_documentsdocument_idN—1RESTRICTOnaylanan metnin tam sürümü
token_grants → usersuser_idN—1RESTRICTKullanıcıya tanımlanmış token paketleri
token_ledger → usersuser_idN—1RESTRICTKullanıcının token hareketleri
token_ledger → token_grantsgrant_idN—1RESTRICTHareketin hangi paketi etkilediği (tanımlama hareketlerinde dolu)
token_requests → usersuser_idN—1CASCADEEk token talepleri
token_requests → token_grantsgranted_grant_idN—1SET NULLTalep onaylandığında oluşan paket
invites → usersuser_id1—1CASCADEHer kullanıcının tek davet bağlantısı
invite_redemptions → invitesinvite_idN—1CASCADEBir davetin kaç kez kullanıldığı
invite_redemptions → usersinvited_user_id1—1CASCADEDavetle gelen kullanıcı — yalnızca bir kez
invite_redemptions → token_grantsreward_grant_idN—1SET NULLDavet edene verilen ödül paketi
feedback → usersuser_idN—1SET NULLGeri bildirim; kullanıcı silinse de kayıt korunur
transactions → usersuser_idN—1RESTRICTKullanıcının işlemleri
transactions → token_ledgerhold_ledger_id1—1RESTRICTİşlem başında alınan token karşılığı
transactions → token_ledgercommit_ledger_id1—1RESTRICTİşlem başarıyla bittiğinde yazılan kesinleşme kaydı
transaction_signatures → transactionstransaction_id1—1CASCADEİmza detayı. Birincil anahtarın aynı zamanda yabancı anahtar olması bire biri garanti eder
transaction_verifications → transactionstransaction_id1—1CASCADEDoğrulama detayı
transaction_timestamps → transactionstransaction_id1—1CASCADEZaman damgası detayı
transaction_events → transactionstransaction_idN—1CASCADEİşlemin durum geçişleri
sms_deliveries → otp_challengesotp_challenge_idN—1CASCADEBir doğrulama kodu için yapılan gönderim denemeleri
notifications → usersuser_idN—1CASCADEKullanıcıya gönderilen bildirimler
api_idempotency_keys → usersuser_idN—1CASCADETekrar-güvenliği anahtarının sahibi
api_idempotency_keys → transactionstransaction_idN—1SET NULLAnahtarın ilk çağrıda ürettiği işlem
daily_user_stats → usersuser_idN—1CASCADEGünlük kullanıcı özeti
stored_objects ⇢ (çoklu)owner_type + owner_idmantıksalNesnenin sahibi tip + kimlik ikilisiyle belirtilir; tek bir tabloya yabancı anahtar verilemez
RESTRICT neden bu kadar çok

Token ve işlem kayıtları hesap verebilirlik taşır. Bir kullanıcıyı silmek token hareketlerini ve imza kayıtlarını yok etmemelidir. Bu yüzden kullanıcı silme yumuşak yapılır (users.deleted_at işaretlenir), fiziksel silme yalnızca saklama süresi dolduğunda ve ayrı bir arşivleme adımıyla düşünülür (Bölüm 11).

① Kimlik & Erişim

users

14 kolon Hesap kaydı. Şemanın merkezi; kullanıcıya ait her şey buraya bağlanır.
KolonTipKısıtAçıklama
iduuidPKKullanıcı kimliği. API'de bu değer görünür; sıralı sayı kullanılmaz.
phone_e164textUQNNE.164 biçiminde telefon numarası (+905321234567). Tek doğrulanmış kimlik bilgisidir; giriş bununla yapılır. Tek biçim zorunluluğu, aynı numaranın iki farklı yazımla iki hesap açmasını engeller. SignGate'e gönderilirken ülke kodu ayrıştırılır.
usernametextUQNNKullanıcının seçtiği benzersiz ad (@gurkan). Tekillik büyük/küçük harf duyarsız olmalıdır; bu yüzden benzersizlik lower(username) üzerine kurulan ifade indeksiyle sağlanır.
statustextNNCHECKHesap durumu: active, suspended, deleted. Askıya alınmış hesap giriş yapamaz ama verisi korunur.
phone_verified_attimestamptzNumaranın OTP ile doğrulandığı an. Kayıt akışında dolar; boş kalması beklenmez ama numara değişimi ileride eklenirse tekrar boşalabilir.
created_attimestamptzNNKayıt anı. Üyelik dönüşüm ölçümlerinin temel zaman referansı.
updated_attimestamptzNNSon değişiklik anı.
last_login_attimestamptzSon başarılı giriş. Aktif kullanıcı sayımı ve hareketsiz hesap tespiti için.
deleted_attimestamptzYumuşak silme işareti. Dolu olduğunda kullanıcı uygulamada yok sayılır; token ve işlem geçmişi hesap verebilirlik için korunur.
suspended_reasontextAskıya alma gerekçesi. status = suspended olduğunda destek ekibinin ve kullanıcının aynı nedeni görmesini sağlar; gerekçesiz askı, açıklanamayan bir müşteri şikâyetidir.
suspended_untiltimestamptzAskının kendiliğinden kalkacağı an. Boş olması süresiz askı demektir. Süreli askıyı elle geri açmaya bırakmak, unutulan hesaplar üretir.
national_id_encbyteaKimlik numarası — uygulama katmanında şifreli. KVKK aydınlatma metni bu veriyi kapsadığı için saklanabilir. Şifreleme veritabanı dışında yapılır: yedek dosyaları veya bir okuma yetkisi sızsa bile değer açık değildir. İlk e-imzada alınır, sonraki imzalarda tekrar sorulmaz.
national_id_hashbyteaIDXAynı değerin anahtarlı özeti. Şifreli alan üzerinde arama yapılamaz; eşitlik sorgusu bununla çözülür. Asıl faydası aynı kimliğin birden çok hesapta kullanılmasını tespit etmektir — davet ödülü suistimalinin telefon numarasıyla aşılamayan tek kontrolü.
national_id_confirmed_attimestamptzBeyan ile kanıt farkı. Kullanıcının girdiği numara bir beyandır; imza tamamlandığında sertifikadan dönen kimlik numarası kanıttır. İkisi eşleştiğinde bu alan dolar. Boş kalması, numaranın hiç doğrulanmadığı anlamına gelir — destek ve uyuşmazlık incelemesinde bu ayrım belirleyicidir.

Burada olmayanlar bilinçli: ad-soyad, e-posta, adres, şifre. Üyelik telefon + kullanıcı adı ile kurulur; imza sonucunda gelen isim işlem kaydına yazılır, profile değil. Şifre yoktur çünkü giriş her seferinde OTP ile yapılır. Kimlik numarası saklanır — KVKK aydınlatma metni bu veriyi kapsayacak biçimde onaylatılacağı için. Şifreli tutulur, aramada özeti kullanılır ve sertifikadan teyit edildiği an ayrıca kaydedilir.

otp_challenges

10 kolon SMS doğrulama oturumları. Hem giriş hem kayıt akışını taşır.
KolonTipKısıtAçıklama
iduuidPKDoğrulama oturumu kimliği. İstemciye bu değer döner (challengeId); kod bu kimlikle birlikte doğrulanır.
phone_e164textNNIDXKodun gönderildiği numara. users'a yabancı anahtar değildir: kayıt akışında kullanıcı henüz yoktur. Bağ mantıksaldır.
purposetextNNCHECKlogin veya register. Amaç ayrımı önemlidir: kayıt için üretilmiş bir kod giriş için kullanılamaz.
code_hashbyteaNNKodun anahtarlı özeti (HMAC-SHA256 + uygulama sırrı). Düz kod hiçbir yerde saklanmaz; veritabanı sızsa bile kodlar okunamaz. Anahtarlı özet tercih edilir çünkü 6 haneli bir kodun düz özeti kaba kuvvetle anında çözülür.
attempt_countsmallintNNYapılan yanlış deneme sayısı. Her hatalı girişte artar.
max_attemptssmallintNNİzin verilen üst sınır (öntanımlı 3). Kolonda tutulur ki sınır değişse bile o anki kural kayıtta kalsın.
expires_attimestamptzNNKodun geçerlilik sonu (öntanımlı +3 dakika). Süre dolmuş kod, doğru bile olsa kabul edilmez.
consumed_attimestamptzKodun başarıyla kullanıldığı an. Dolu olan kayıt tekrar kullanılamaz — tek kullanımlık olmasını sağlayan alan budur.
created_ipinetTalebin geldiği adres. Kötüye kullanım incelemesi ve hız sınırı denetimi için.
created_attimestamptzNNOluşturma anı. Yeniden gönderim bekleme süresi bu değerden hesaplanır.
Neden Valkey'de değil, Postgres'te

Doğrulama oturumu kısa ömürlüdür, akla ilk gelen onu önbellekte tutmaktır. Ancak bu tablo aynı zamanda bir kötüye kullanım kaydıdır: hangi numaraya kaç kod gitti, kaç yanlış deneme yapıldı, hangi adresten geldi. Bu izi kalıcı tutmak brute-force ve SMS maliyet saldırılarını sonradan inceleyebilmek için gerekli. Valkey yalnızca hız sınırı sayacı için kullanılır — ucuz, sık okunan, kaybı sorun olmayan veri.

refresh_tokens

9 kolon Oturum yenileme jetonları. Kullanıcının açık oturumlarını temsil eder.
KolonTipKısıtAçıklama
iduuidPKJeton kaydı kimliği.
user_iduuidFKNNJetonun sahibi.
token_hashbyteaUQNNJetonun özeti. Jetonun kendisi yalnızca istemcide bulunur. Tekillik kısıtı, aynı jetonun iki kayda düşmesini engeller.
issued_attimestamptzNNVerildiği an.
expires_attimestamptzNNGeçerlilik sonu.
revoked_attimestamptzİptal anı. Çıkış yapıldığında veya güvenlik gerekçesiyle dolar.
replaced_byuuidFKBu jetonun yerine geçen yeni jeton. Rotasyon zincirini kurar: bir jeton kullanıldığında iptal edilir ve yerine yenisi verilir. Zincir sayesinde, iptal edilmiş bir jeton tekrar kullanılmaya çalışılırsa hırsızlık şüphesi tespit edilip tüm zincir iptal edilebilir.
user_agenttextOturumu açan istemci bilgisi. "Cihazlarım" ekranı ve şüpheli oturum tespiti için.
ipinetOturumun açıldığı adres.

③ Jet Token

Token modeli iki tabloya dayanır: paketler bakiyenin kaynağıdır, hareket kaydı her değişimin denetlenebilir izidir. Bakiye hiçbir yerde tek bir sayı olarak tutulmaz.

token_grants

10 kolon Token paketleri. Bakiyenin kaynağı; her paketin kendi son kullanma tarihi vardır.
KolonTipKısıtAçıklama
iduuidPKPaket kimliği.
user_iduuidFKNNPaketin sahibi.
kindtextNNCHECKPaketin kaynağı: monthly (aylık ücretsiz), invite (davet ödülü), manual (ek talep onayı), refund (iade). Ayrım, "bakiyem nereden geldi" sorusunu cevaplar ve büyüme ölçümünü mümkün kılar.
amountintegerNN>0Paketin başlangıç miktarı. Hiç değişmez — tarihsel gerçek.
remainingintegerNN0..amountPaketten kalan miktar. Harcamada azalır, iadede artar. Kısıt sayesinde eksiye düşmesi ve başlangıç miktarını aşması veritabanı tarafından engellenir.
granted_attimestamptzNNTanımlanma anı.
expires_attimestamptzSon geçerlilik anı. monthly paketlerde ayın son anı (devretmeme kuralı budur), invite paketlerde +30 gün. Boş olması "süresiz" demektir; MVP'de kullanılmaz.
source_typetextPaketi doğuran kaydın türü (token_request, invite_redemption, transaction).
source_iduuidPaketi doğuran kaydın kimliği. Tür + kimlik ikilisi esnek bir köken bağı kurar; her kaynak türü için ayrı kolon açmaya gerek kalmaz.
idempotency_keytextUQNNÇift tanımlamaya karşı tek savunma. Aylık paket için monthly:<user_id>:2026-08 biçiminde üretilir. Aynı ay için ikinci kez tanımlama denenirse tekillik kısıtı bunu reddeder. Aylık tanımlama zamanlanmış bir görevle değil, kullanıcı bakiyeye dokunduğunda yapıldığı için bu koruma zorunludur: iki eşzamanlı istek aynı anda tanımlamaya çalışabilir.

token_ledger

11 kolon Append-only hareket kaydı. Silinmez, güncellenmez.
KolonTipKısıtAçıklama
idbigintPKArtan hareket numarası. Sıralılık burada istenen bir özelliktir: hareketler doğal kronolojik sırada okunur, imleçli sayfalama bu değerle yapılır.
user_iduuidFKNNHareketin sahibi. Paket üzerinden dolaylı olarak da bulunabilirdi; doğrudan tutulması "kullanıcının hareket dökümü" sorgusunu tek tabloda ve tek indeksle çözer.
grant_iduuidFKHareketin etkilediği paket. Harcama ve iadede doludur. MVP'de her işlem 1 token olduğu için bir hareket tek paketten karşılanır; bölünme yoktur.
deltaintegerNNBakiye değişimi. Tanımlamada +n, harcamada -1, iadede +1. Sıfır olabilir: kesinleşme kaydı bakiyeyi değiştirmez, yalnızca askıdaki harcamanın nihai hale geldiğini belgeler.
reasontextNNCHECKHareketin gerekçesi (Bölüm 10). Kullanıcıya gösterilen hareket dökümünün etiketi buradan üretilir.
ref_typetextHareketi doğuran kaydın türü.
ref_iduuidHareketi doğuran kaydın kimliği — genellikle bir işlem.
balance_afterintegerNNHareket sonrası bakiye. Hesaplanabilir bir değerin saklanması bilinçli bir tekrardır: hareket dökümünü göstermek için tüm geçmişi toplamak gerekmez ve bir tutarsızlık oluşursa hangi hareketten sonra başladığı anında görülür.
idempotency_keytextUQNNAynı hareketin iki kez yazılmasını engeller. Ağ tekrarı veya arka plan işinin yeniden denenmesi durumunda tek savunma budur.
correlation_idtextIDXHareketi doğuran isteğin ilişkilendirme kimliği. Bir bakiye anlaşmazlığında bu değerle log ve iz kayıtlarına gidilir; hareketin hangi HTTP isteğinden doğduğu kesin olarak bulunur.
created_attimestamptzNNHareket anı.

Bakiye nasıl hesaplanır

-- Bakiye = süresi dolmamış paketlerdeki kalan miktarların toplamı SELECT COALESCE(SUM(remaining), 0) FROM token_grants WHERE user_id = @userId AND remaining > 0 AND (expires_at IS NULL OR expires_at > now()); -- Harcama: en erken dolan paketten (FIFO). Süresi geçen paket kendiliğinden düşer; -- ayrıca bir "süre doldurma" işine gerek yoktur. ORDER BY expires_at NULLS LAST

İmza işleminin token muhasebesi

AdımLedger kaydıdeltaPaket ve işlem üzerindeki etkisi
İşlem başlatıldıhold−1token_grants.remaining 1 azalır; oluşan hareket transactions.hold_ledger_id'ye yazılır. Bakiye o an düşer — kullanıcı aynı token'ı ikinci bir işlemde kullanamaz.
İmza başarılıcommit0Bakiye değişmez; hareket transactions.commit_ledger_id'ye yazılır. Askıdaki harcama kesinleşmiş sayılır.
Hata / iptal / süre aşımırelease+1remaining geri artar. Başarısız imza kullanıcıya bedel yazmaz.
Neden "askıya al, sonra kesinleştir"

Tek adımlı iki alternatifin de açığı var. Yalnızca başarıda düşmek, bakiyesi biten kullanıcının eşzamanlı çok sayıda işlem açıp tek token'la birkaç imza atmasına izin verir. Başlangıçta kesin düşmek ise Easy kurulu olmadığında veya PIN yanlış girildiğinde token'ı yakar; MVP'nin en kritik ölçütü olan "üyelik → ilk imza" dönüşümünü doğrudan zedeler. Askıya alma her iki riski birlikte kapatır ve commit kaydı sayesinde bir işlemin gerçekten sonuçlandığı denetlenebilir kalır.

token_requests

9 kolon Ek 10 token talepleri. MVP'de onay doğrudan veritabanından verilir.
KolonTipKısıtAçıklama
iduuidPKTalep kimliği.
user_iduuidFKNNTalep sahibi.
amountintegerNNTalep edilen miktar. MVP'de sabit 10; kolonda tutulur ki miktar politikası değişse eski talepler kendi miktarını korusun.
statustextNNCHECKpending, approved, rejected.
granted_grant_iduuidFKOnay sonucunda oluşan paket. Talebin gerçekten karşılandığını kanıtlar; onaylanmış ama paketi oluşmamış talep bu kolonun boşluğundan tespit edilir.
notetextKullanıcının gerekçesi veya ekibin notu.
decided_bytextKararı veren kişi. MVP'de yönetim paneli olmadığı için serbest metindir; panel geldiğinde kimliğe bağlanır.
decided_attimestamptzKarar anı. Talep-cevap süresi ölçümü buradan çıkar.
created_attimestamptzNNTalep anı.
"Aynı anda tek açık talep" kuralı — kodda değil veritabanında

CREATE UNIQUE INDEX ... ON token_requests (user_id) WHERE status = 'pending'
Kısmi tekillik indeksi, bir kullanıcının aynı anda yalnızca bir bekleyen talebi olmasını garanti eder. Bu kuralı uygulama katmanında "önce sorgula, sonra ekle" ile yazmak eşzamanlı iki istekte çalışmaz. Kural veritabanında olduğu için ikinci istek kısıt hatası alır ve API bunu 409 olarak döner.

④ Davet & Geri Bildirim

invites

5 kolon Kullanıcı başına tek davet bağlantısı.
KolonTipKısıtAçıklama
iduuidPKDavet kaydı kimliği.
user_iduuidFKUQNNDavet sahibi. Tekillik kısıtı, her kullanıcının tek davet bağlantısı olmasını sağlar.
codetextUQNNBağlantıda görünen kod. Kısa, karışmayan bir alfabeden üretilir ve tahmin edilemez olmalıdır; sıralı kod üretmek davetleri toplu deneme ile sömürülebilir hale getirir.
created_attimestamptzNNOluşturma anı.
disabled_attimestamptzDevre dışı bırakma anı. Kötüye kullanım tespit edilirse bağlantı kapatılır; geçmiş kullanımlar silinmez.

invite_redemptions

7 kolon Davetin kullanımları ve verilen ödüller.
KolonTipKısıtAçıklama
iduuidPKKullanım kaydı kimliği.
invite_iduuidFKNNKullanılan davet.
invited_user_iduuidFKUQNNDavetle gelen kullanıcı. Tekillik kısıtı ödül suistimalinin kilididir: bir kullanıcı yalnızca bir kez "davet edilmiş" olabilir, dolayısıyla yalnızca bir kez ödül doğurur.
redeemed_attimestamptzNNDavetin kullanıldığı an — yeni kullanıcının kayıt tamamlandığı an.
reward_grant_iduuidFKDavet edene verilen 30 gün geçerli ödül paketi. Boş kalması "ödül henüz verilmedi veya verilemedi" demektir; bu, ödül dağıtımını denetlenebilir kılar.
ipinetKaydın geldiği adres. Aynı adresten seri davet kabulü, sahte hesap üretiminin en görünür işaretidir.
fraud_flagbooleanNNŞüpheli işaret. İşaretlenen kayıt ödül üretmez; inceleme sonrası kaldırılabilir.
Davet tasarımında ne YOK

Davet edilen üçüncü kişinin adı, telefonu veya e-postası hiçbir yerde tutulmaz. Davet bağlantısı yalnızca bir kod taşır; kim geldiği ancak o kişi kendi isteğiyle üye olduğunda ve yalnızca invited_user_id olarak bilinir. Bu, ürün stratejisindeki "üçüncü kişi verisi toplamayan davet" ilkesinin şemadaki karşılığıdır.

feedback

8 kolon "Deneyimini Paylaş" kayıtları: sorun, öneri, memnuniyet, destek.
KolonTipKısıtAçıklama
iduuidPKKayıt kimliği.
user_iduuidFKGönderen. Kullanıcı silinse bile geri bildirim korunur, bu yüzden bağ boşa çıkabilir.
categorytextNNCHECKissue, feature, praise, support. Kategori ayrımı, tekrarlayan taleplerin gruplanmasını sağlar.
subjecttextKısa başlık.
bodytextNNKullanıcının anlatımı.
statustextNNCHECKnew, triaged, in_progress, closed.
internal_notetextEkibin iç notu. Kullanıcıya gösterilmez.
created_attimestamptzNNGönderim anı.

İşlem & Denetim

İşlem modeli bir çekirdek tablo + tipe özgü detay tabloları biçiminde kurulur. Çekirdek tablo her işlem tipinde anlamlı olan alanları taşır: sahiplik, durum, belge özeti, token muhasebesi, zamanlar ve gözlemlenebilirlik. Tipe özgü alanlar kendi tablolarında bire bir ilişkiyle durur.

Kalıtım eşlemesi değil, isteğe bağlı bire bir detay

Bu ayrım bir tip hiyerarşisi değildir. transactions kendi başına tam ve sorgulanabilir bir tablodur; detay tabloları ona bağlı isteğe bağlı uzantılardır. Pratik sonucu şudur: "İşlemlerim" listesi hiçbir birleştirme yapmadan tek tablodan okunur; detay yalnızca işlem detayı ekranında çekilir. Kalıtım eşlemesi seçilseydi her listeleme sorgusu birleştirme üretirdi.

Neden şimdi bölündü

MVP'de üç işlem tipi var. Ürün yol haritasında Mobil Dijital Onay, E-İmzalı Onay ve Kimlik Beyanı servisleri bulunuyor; her biri kendi alanlarıyla birer işlem tipi olarak gelecek. Tek tabloda kalınsaydı bu tablo altmışı aşan bir kolon sayısına ulaşır ve bölme kararı, içinde gerçek imza kayıtları bulunan bir tabloyu taşımak anlamına gelirdi. Şimdi bölmek bedelsiz; sonra bölmek riskli.

Bölmenin dürüst bedeli

Tek tabloda sorun, zorunlu alanların boş bırakılabilmesiydi. Bölünmüş modelde sorun yer değiştirir: bir imza işleminin detay satırı hiç oluşmamış olabilir. Bu, değer kısıtıyla ifade edilemez. Uygulamada iki satır aynı işlem bloğunda yazıldığı için risk düşüktür; yine de sıfır değildir. Karşılığında elde edilen şey, detay tablolarındaki alanların gerçekten zorunlu olabilmesidir: kanalsız imza satırı veritabanı tarafından reddedilir.

transactions

26 kolon Çekirdek işlem kaydı. Tüm tiplerde anlamlı olan alanlar. "İşlemlerim" ekranının kaynağı.
KolonTipKısıtAçıklama
iduuidPKİşlem kimliği. API'de bu değer görünür; durum sorgusu bununla yapılır. Detay tabloları da bu kimliği paylaşır.
user_iduuidFKNNİşlemin sahibi.
typetextNNCHECKsignature, timestamp, verification. Hangi detay tablosunun dolu olacağını belirler. Yeni bir tip eklemek, yeni bir detay tablosu ve bu kümeye bir değer eklemek demektir — mevcut tabloların hiçbiri değişmez.
statustextNNCHECKYaşam döngüsü durumu. İmzada queued → signing → …; doğrulama ve zaman damgası doğrudan sonuçlanır.
document_nametextNNKullanıcının yüklediği dosya adı.
document_sizebigintNNBayt cinsinden boyut.
document_sha256byteaNNIDXKaynak belgenin özeti. Aynı belgenin geçmiş işlemlerini bulmayı ve kullanıcının gördüğü belge ile işlenen içeriğin aynı olduğunu kanıtlamayı sağlar.
document_short_codetextÖzetten türetilen kullanıcı dostu doğrulama kodu (MAVI-47-KALE-82).
source_object_keytextKaynak belgenin nesne depolamadaki adresi.
result_object_keytextİmzalı / damgalı çıktının adresi. Altyapı sonucu kalıcı saklamadığı için çıktıyı biz saklarız.
signgate_request_reftextUQBizim ürettiğimiz korelasyon anahtarı; altyapıya istekle gönderilir. Üç tipin üçü de dış çağrı yaptığı için çekirdekte durur; dış çağrı yapmayan bir tip eklenirse boş kalır.
signgate_request_iduuidAltyapının kendi ürettiği kimlik. İki sistemin kaydını eşlemek için.
hold_ledger_idbigintFKCHECKİşlem başında alınan token karşılığı. Doğrulama ücretsiz olduğu için o tipte boştur — ve değer kısıtı bunu zorunlu kılar.
commit_ledger_idbigintFKBaşarılı sonuçta yazılan kesinleşme kaydı. hold dolu, commit boş ve durum sonuçlanmış görünüyorsa muhasebe tutarsızlığı vardır — bu ikili bir denetim kancasıdır.
attempt_countsmallintNNArka plan işinin kaç kez denediği.
worker_instancetextİşlemi yürüten arka plan işçisinin kimliği. Yeniden başlatma sonrası askıda kalan işleri ayırt eder.
correlation_idtextNNIDXLog ve iz dünyasına açılan kapı. Destek ekranındaki bir işlemden bu değerle log kayıtlarına gidilir.
trace_idtextIDXDağıtık iz kimliği. İşlemden uçtan uca iz görünümüne geçilir.
client_ipinetİşlemi başlatan adres. Hukuki sonuç doğuran işlemde adli açıdan gereklidir.
user_agenttextİşlemi başlatan istemci.
created_attimestamptzNNIDXKayıt anı. Liste bu değere göre sıralanır.
started_attimestamptzDış çağrının başladığı an.
completed_attimestamptzCHECKSonuçlandığı an. Sonuçlanmış durumlarda dolu olması değer kısıtıyla zorunludur.
expires_attimestamptzIDXEn son sonuçlanması gereken an. Gözcü süreç bunu geçmiş ve hâlâ askıda olanları kapatır.
failure_codetextMakine tarafından okunabilir hata kodu.
failure_messagetextAltyapıdan gelen ham hata metni. Kullanıcıya ham hâliyle gösterilmez.

transaction_signatures

10 kolon İmza detayı. type = signature olduğunda dolu.
KolonTipKısıtAçıklama
transaction_iduuidPKFKHem birincil hem yabancı anahtar. Bire bir ilişkiyi tek başına garanti eder: bir işlemin en fazla bir imza detayı olabilir.
channeltextNNCHECKeasy veya mobileid. Artık gerçekten zorunlu — kanalsız imza satırı veritabanınca reddedilir. Tek tabloda bu mümkün değildi.
formattextNNCHECKpades, cades, xades.
kindtextNNCHECKİmza seviyesi: bes, t, xl, a.
thumbprinttextKullanıcının seçtiği sertifikanın parmak izi. Karttaki birden çok sertifikadan hangisiyle imzalandığını belgeler.
visible_signaturebooleanNNGörünür imza istenip istenmediği. Çıktının neden farklı göründüğü sorusunun cevabı.
certificate_subjecttextİmzalayan sertifikanın sahibi. İmza sonrasında gelir.
certificate_serialtextSertifika seri numarası.
certificate_identity_notextSertifikadan gelen kimlik numarası — maskeli saklanır (123****8901). Tam numara hiçbir yerde tutulmaz.
signing_timetimestamptzİmzanın atıldığı an — imza yapısından okunur, bizim kaydettiğimiz an değil.

transaction_verifications

6 kolon Doğrulama detayı. type = verification olduğunda dolu.
KolonTipKısıtAçıklama
transaction_iduuidPKFKBire bir bağ.
detected_formattextBelgede tespit edilen imza biçimi. Kullanıcı biçim belirtmediği için sonuç bu alandan okunur.
is_validbooleanNNGenel sonuç. Listeleme ekranında rozeti bu belirler; ayrıntı için tam sonuca inmek gerekmez.
signature_countintegerNNBelgedeki toplam imza sayısı.
valid_signature_countintegerNNGeçerli imza sayısı. İkisi farklıysa belge kısmen geçerlidir — kullanıcıya anlatılması gereken en kritik ayrım budur.
resultjsonbDoğrulamanın tam sonucu: imzacılar, sertifika durumları, denetim kalemleri. Yapısı imza sayısına ve türüne göre değiştiği için ilişkisel tabloya açılmaz, belge olarak tutulur.

transaction_timestamps

5 kolon Zaman damgası detayı. type = timestamp olduğunda dolu.
KolonTipKısıtAçıklama
transaction_iduuidPKFKBire bir bağ.
tsa_providertextDamgayı veren zaman damgası otoritesi. Sağlayıcı değişirse eski kayıtlar kendi sağlayıcısını korur.
tsa_serialtextDamganın seri numarası. Bir damganın otorite nezdinde aranabilmesi için gereken tek değer.
visible_stampbooleanNNGörünür damga istenip istenmediği.
stamped_attimestamptzDamganın taşıdığı zaman — otoritenin beyanı, bizim kaydımız değil.

transaction_events

7 kolon Append-only durum geçiş kaydı. Tipten bağımsız; çekirdeğe bağlıdır.
KolonTipKısıtAçıklama
idbigintPKArtan olay numarası; geçişleri kronolojik okumayı sağlar.
transaction_iduuidFKNNİlgili işlem. Detay tablolarına değil çekirdeğe bağlanır; böylece yeni bir işlem tipi geldiğinde olay kaydı hiç değişmez.
from_statustextÖnceki durum. İlk kayıtta boştur.
to_statustextNNYeni durum.
sourcetextNNCHECKGeçişi kim yaptı: api, worker, watchdog, user. Bir işlemin neden başarısız olduğu sorusunun cevabı çoğu zaman buradadır.
payloadjsonbGeçişe ait ham bağlam — altyapı yanıtı, hata gövdesi, deneme numarası.
created_attimestamptzNNGeçiş anı.
Yeni bir işlem tipi eklemenin maliyeti

Yeni bir detay tablosu ve type kümesine bir değer. transactions, transaction_events, token_ledger ve token muhasebesi kodunun tamamı değişmez. Bölünmüş modelin asıl kazancı budur: yol haritasındaki üç yeni servis, mevcut hiçbir tabloya dokunmadan eklenebilir.

Operasyon, Denetim & Ölçüm

Bu bölümdeki tablolar iş kuralı taşımaz. Servisin işletilebilir olmasını sağlarlar: gönderilen SMS'in akıbeti, saklanan dosyaların sahipliği, tekrar edilen isteklerin zararsızlaştırılması, yönetsel eylemlerin kaydı ve ürün ölçütlerinin ucuz okunması.

Buradaki tablolar log deposu DEĞİLDİR

Uygulama log'ları, metrikler ve izler veritabanına yazılmaz; telemetri altyapısına akar. Bu tablolar yalnızca kesin olması gereken, az hacimli ve uzun ömürlü kayıtları tutar. Ayrım ve gerekçesi ayrı bir dokümanda ele alınmıştır.

sms_deliveries

12 kolon SMS gönderim denemeleri ve sonuçları. OTP başarı oranının kaynağı.
KolonTipKısıtAçıklama
iduuidPKGönderim kaydı.
otp_challenge_iduuidFKNNHangi doğrulama oturumu için gönderildiği. Bir oturum için birden çok gönderim olabilir (yeniden gönder).
phone_e164textNNHedef numara.
providertextNNGönderimi yapan sağlayıcı. Sağlayıcı değiştiğinde eski kayıtlar kendi sağlayıcısını korur; karşılaştırma yapılabilir.
provider_message_idtextIDXSağlayıcının verdiği kimlik. Teslim bildirimi geldiğinde kaydı bulmanın tek yolu budur.
statustextNNCHECKqueued, sent, delivered, failed. "Gönderildi" ile "ulaştı" aynı şey değildir — OTP başarı oranı ancak bu ayrımla ölçülür.
error_codetextSağlayıcı hata kodu. Geçersiz numara mı, kota mı, operatör reddi mi — hepsi farklı aksiyon gerektirir.
cost_microsintegerGönderim maliyeti (milyonda bir birim). SMS, ücretsiz üyelik modelinde tek gerçek değişken maliyettir; kullanıcı başına maliyet ancak burada ölçülür ve kötüye kullanımın faturası görünür olur.
requested_attimestamptzNNGönderim isteği anı.
sent_attimestamptzSağlayıcının kabul ettiği an.
delivered_attimestamptzCihaza ulaştığı an. requested_at ile farkı, kullanıcının kodu ne kadar beklediğidir — OTP terk oranının doğrudan açıklayıcısı.
correlation_idtextIDXGönderimi doğuran isteğin ilişkilendirme kimliği.

notifications

10 kolon Kullanıcıya giden bilgilendirmeler. Stratejideki "bildirim" iş paketinin karşılığı.
KolonTipKısıtAçıklama
iduuidPKBildirim kaydı.
user_iduuidFKNNAlıcı.
channeltextNNCHECKsms, inapp. MVP'de e-posta yok — üyelikte e-posta toplanmıyor.
template_keytextNNMetin şablonu anahtarı. Metnin kendisi kayda gömülmez; şablon değişse bile hangi bildirimin gönderildiği anlaşılır.
payloadjsonbŞablona geçen değerler (token miktarı, işlem adı). Şablon + değer ikilisi, gönderilen metni yeniden üretmeye yeter.
statustextNNCHECKpending, sent, failed, suppressed. Son değer, kullanıcının izni olmadığı için gönderilmeyeni işaretler — izin ihlalinin kanıtı, gönderilmemenin kaydıdır.
failure_reasontextGönderilemediyse nedeni.
correlation_idtextIDXBildirimi doğuran işlem/istek bağı.
created_attimestamptzNNOluşturma anı.
sent_attimestamptzGönderim anı.

stored_objects

11 kolon Nesne depolamadaki dosyaların kaydı. Sahiplik, saklama ve çöp toplama.
KolonTipKısıtAçıklama
iduuidPKNesne kaydı.
object_keytextUQNNDepodaki tam adres. Tekillik, aynı dosyanın iki kez kaydedilmesini engeller.
buckettextNNDepo bölmesi. Kaynak belge ile imzalı çıktının farklı saklama sürelerine sahip olmasını mümkün kılar.
size_bytesbigintNNBoyut. Depolama maliyetinin kullanıcı ve işlem başına dağılımı buradan çıkar.
sha256byteaNNİçerik özeti. Depodaki dosyanın bozulup bozulmadığı bununla denetlenir.
content_typetextMIME tipi.
owner_typetextNNSahibin türü (transaction). Tek bir tabloya yabancı anahtar verilemediği için sahiplik tip + kimlik ikilisiyle kurulur.
owner_iduuidNNIDXSahibin kimliği.
created_attimestamptzNNYükleme anı.
delete_aftertimestamptzIDXSaklama politikasının şemadaki karşılığı. Temizlik işi yalnızca bu değeri geçmiş kayıtları siler; süre politikası kodda değil veride durur, dolayısıyla dosya ve bölme bazında değiştirilebilir. Kısa saklama senaryosunda (işlemden sonra ~15 dakika) bu değer işlem tamamlandığında hesaplanır.
deleted_attimestamptzDepodan silindiği an. Kayıt kalır: bir belgenin var olduğu ve süresi dolduğu için silindiği kanıtlanabilir olur.
Kısa saklama seçilirse ne değişir

Belgelerin işlemden kısa süre sonra (örneğin 15 dakika) silinmesi şemada hiçbir değişiklik gerektirmezdelete_after zaten dosya bazında çalışıyor. Ancak ürün davranışı değişir ve bunun bilinçli kabul edilmesi gerekir: "İşlemlerim" ekranı işlemin kendisini (tarih, imzacı, sonuç, kısa doğrulama kodu) süresiz gösterebilir, ama belgenin kendisi süre dolduktan sonra indirilemez. İndirme ucu bu durumda bozuk bir bağlantı vermemeli; stored_objects.deleted_at dolu ise "belge saklama süresi doldu" anlamına gelen açık bir yanıt dönmelidir. Kullanıcıya bu sınırın imza anında söylenmesi gerekir — belgesini indirmemiş kullanıcı için sonradan telafisi yoktur.

api_idempotency_keys

10 kolon Tekrar edilen isteklerin ikinci kez iş yapmasını engeller.
KolonTipKısıtAçıklama
iduuidPKKayıt kimliği.
idempotency_keytextNNİstemcinin ürettiği anahtar (Idempotency-Key başlığı). Mobil ağda kopan bir istek tekrar gönderildiğinde aynı anahtarla gelir.
user_iduuidFKNNAnahtarın sahibi. Anahtar yalnızca kendi kullanıcısı içinde tekildir; bir kullanıcının anahtarı diğerini etkilemez.
endpointtextNNHangi uç için. Aynı anahtarın farklı uçlarda kullanılması ayrı kayıtlar üretir.
request_hashbyteaNNİstek gövdesinin özeti. Aynı anahtarla farklı gövde gelirse istek reddedilir — anahtarın yanlışlıkla yeniden kullanılması sessizce yanlış cevap dönmesine yol açmaz.
response_statussmallintİlk çağrının HTTP durumu.
response_bodyjsonbİlk çağrının yanıtı. Tekrar gelen istek yeniden iş yapmaz; aynı yanıt döner.
transaction_iduuidFKİlk çağrının ürettiği işlem. Tekrarın hangi işleme denk geldiğini gösterir.
created_attimestamptzNNİlk çağrı anı.
expires_attimestamptzIDXAnahtarın saklanma sonu (öneri 24 saat). Tablo sınırsız büyümez.
Bu tablo neden token muhasebesinin tamamlayıcısı

Token askıya alma, bakiyesi biten kullanıcıyı korur. Tekrar-güvenliği ise ağı kopan kullanıcıyı korur: "İmzala"ya bastı, cevap gelmedi, tekrar bastı. Bu tablo olmadan iki işlem ve iki token askısı oluşur; kullanıcı bir imza için iki token harcamış gibi görünür. İkisi farklı riskler ve ikisi de gereklidir.

audit_events

12 kolon Yönetsel ve güvenlik açısından anlamlı eylemler. Append-only.
KolonTipKısıtAçıklama
idbigintPKArtan olay numarası.
actor_typetextNNCHECKEylemi yapan: user, operator, system. Kullanıcının kendi yaptığı ile ekibin yaptığını ayırmak denetimin ilk şartıdır.
actor_idtextEyleyenin kimliği. Operatörler için serbest metin; MVP'de yönetim paneli olmadığı için kimliğe bağlanamaz.
actiontextNNIDXEylem adı (token_request.approved, user.suspended, invite.disabled). Her satır değişikliği değil, yalnızca anlamlı eylemler yazılır.
target_typetextNNEtkilenen varlığın türü.
target_idtextIDXEtkilenen varlığın kimliği.
beforejsonbEylem öncesi ilgili alanlar — tüm satır değil. Kişisel veri taşımaz.
afterjsonbEylem sonrası ilgili alanlar.
ipinetEylemin geldiği adres.
user_agenttextEylemi yapan istemci.
correlation_idtextIDXİlgili istek bağı.
created_attimestamptzNNEylem anı.
Bu tablo bir "her değişikliği yaz" kancası DEĞİLDİR

Kayıt katmanına takılan ve her satır güncellemesini yazan genel bir denetim mekanizması, yazma yolunu yavaşlatır ve okunamayacak kadar gürültülü bir tablo üretir. Burada yalnızca hesap sorulabilecek eylemler yer alır: ek token onayı, hesap askıya alma, davet kapatma, geri bildirim durumu değişimi. Token hareketleri ve işlem geçişleri zaten kendi tablolarında append-only tutulduğu için burada tekrarlanmaz.

daily_user_stats

11 kolon Kullanıcı başına günlük özet. Ham tabloları taramadan okunur.
KolonTipKısıtAçıklama
iduuidPKÖzet satırı.
user_iduuidFKNNKullanıcı.
datedateNNGün. Kullanıcı + gün ikilisi tekildir; özet işi tekrar çalışsa bile satır çoğalmaz.
signatures_startedintegerNNBaşlatılan imza sayısı.
signatures_succeededintegerNNTamamlanan imza sayısı. Başlatılan ile farkı, kullanıcının yaşadığı sürtünmedir.
timestamps_totalintegerNNZaman damgası işlemi.
verifications_totalintegerNNDoğrulama işlemi.
tokens_grantedintegerNNO gün tanımlanan token.
tokens_spentintegerNNO gün harcanan token.
first_activitytimestamptzGün içindeki ilk hareket.
last_activitytimestamptzGün içindeki son hareket.

daily_funnel_stats

6 kolon Stratejinin dört başarı ölçütünün doğrudan kaynağı.
KolonTipKısıtAçıklama
datedatePKGün.
otp_requestedintegerNNKod isteyen sayısı.
otp_verifiedintegerNNKodu doğrulayan sayısı → OTP başarı oranı.
registeredintegerNNÜyeliği tamamlayan → üyelik dönüşümü.
first_signatureintegerNNİlk imzasını atan → üyelik → ilk imza oranı.
invites_acceptedintegerNNKabul edilen davet → davet dönüşüm oranı.
Neden özet tablosu, neden panoda canlı sorgu değil

Huni oranlarını her pano yenilemesinde ham tablolardan hesaplamak, veri büyüdükçe hem yavaşlar hem de üretim veritabanına raporlama yükü bindirir. Günlük özet, gecelik tek bir işle üretilir; pano yalnızca özet okur. Ayrıca özet o günkü gerçeği dondurur: kullanıcı sonradan silinse bile o günün dönüşüm oranı değişmez.

Kısıt & İndeks Envanteri

Aşağıdaki kısıtlar iş kurallarını veritabanı seviyesinde uygular. Uygulama katmanında da kontrol edilirler, ancak son söz veritabanınındır: eşzamanlı isteklerde yalnızca veritabanı kısıtı doğru sonucu garanti eder.

Kısıt / İndeksTürUyguladığı kural
users (phone_e164)tekillikBir telefon numarası tek hesap açar. E.164 tek biçim zorunluluğu bunun ön koşuludur.
users (lower(username))ifade tekilliği@Gurkan ve @gurkan aynı addır; ikisi birlikte var olamaz.
token_grants (idempotency_key)tekillikAylık paket bir ay için yalnızca bir kez tanımlanır — eşzamanlı iki istek gelse bile.
token_ledger (idempotency_key)tekillikAynı token hareketi iki kez yazılamaz; yeniden denenen arka plan işi bakiyeyi bozamaz.
token_grants (remaining)değer kısıtı0 ≤ remaining ≤ amount. Bakiye eksiye düşemez, paketten fazlası harcanamaz.
token_requests (user_id) WHERE pendingkısmi tekillikAynı anda tek açık ek token talebi.
user_consents (user_id, document_id)tekillikAynı metin sürümü iki kez onaylanamaz.
invites (user_id)tekillikKullanıcı başına tek davet bağlantısı.
invites (code)tekillikDavet kodları çakışmaz.
invite_redemptions (invited_user_id)tekillikBir kullanıcı yalnızca bir kez davet ödülü doğurur. Ödül suistimalinin ana kilidi.
transactions (signgate_request_ref)tekillikKorelasyon anahtarı çakışmaz; iki işlem aynı SignGate isteğine bağlanamaz.
refresh_tokens (token_hash)tekillikAynı jeton iki kayda düşemez.
otp_challenges (phone_e164, created_at ↓)indeksNumaraya ait son doğrulama oturumunu ve hız sınırı penceresini hızlı bulur.
token_grants (user_id, expires_at) WHERE remaining > 0kısmi indeksBakiye hesabı ve FIFO harcama seçimi. Kısmi olması indeksi küçük tutar: tükenmiş paketler indekste yer almaz.
token_ledger (user_id, id ↓)indeksHareket dökümünün imleçli sayfalaması.
transactions (user_id, created_at ↓)indeks"İşlemlerim" listesi.
transactions (status, expires_at) WHERE askıdakısmi indeksGözcü sürecin taraması. Kısmi olması kritik: milyonlarca sonuçlanmış işlem arasından yalnızca askıda olanlara bakar.
transaction_events (transaction_id, id)indeksBir işlemin geçiş geçmişini sırayla okur.
transactions (document_sha256)indeksAynı belgenin geçmiş işlemlerini bulur.
transactions — doğrulama ücretsizkoşullu değer kısıtıtype = 'verification' ise token askısı olamaz; diğer tiplerde zorunludur. Ücretsizlik kuralı koda değil şemaya yazılmış olur.
transactions — sonuçlanma tutarlılığıkoşullu değer kısıtıDurum sonuçlanmış (succeeded·failed·cancelled·expired) ise completed_at boş olamaz.
transaction_signatures (transaction_id)PK = FKBir işlemin en fazla bir imza detayı olabilir. Ayrı bir tekillik kısıtına gerek kalmaz.
transaction_signatures (channel, format, kind)NOT NULL + değer kısıtıBölmenin asıl kazancı. Kanalsız, biçimsiz veya seviyesiz bir imza satırı veritabanınca reddedilir — tek tabloda bu alanlar nullable kalmak zorundaydı.
transaction_verifications (transaction_id)PK = FKBire bir.
transaction_timestamps (transaction_id)PK = FKBire bir.

Enum Sözlüğü

Sabit değer kümeleri text kolon + değer kısıtı olarak tutulur, veritabanı enum tipi olarak değil. Gerekçe Bölüm 12'de.

AlanDeğerler ve anlamı
users.statusactive normal · suspended giriş kapalı, veri korunur · deleted yumuşak silinmiş
otp_challenges.purposelogin mevcut hesaba giriş · register yeni kayıt
consent_documents.document_typeterms kullanıcı sözleşmesi · privacy KVKK aydınlatma ve gizlilik · cookie çerez politikası · commercial_message ticari elektronik ileti (geri alınabilir) · esign_rules e-imza ve token kullanım kuralları · referral_terms davet şartları
token_grants.kindmonthly aylık ücretsiz 10 · invite davet ödülü (30 gün) · manual ek talep onayı · refund iade
token_ledger.reasonmonthly_grant aylık tanımlama · invite_reward davet ödülü · manual_grant onaylı ek token · hold işlem başında askıya alma · commit kesinleşme (delta 0) · release iade · expiry süre dolumu kaydı
token_requests.statuspending bekliyor · approved onaylandı · rejected reddedildi
transactions.typesignature mobil imza veya e-imza · timestamp zaman damgası · verification doğrulama
transactions.statusqueued kayıt açıldı, token askıda · signing altyapı çağrısı sürüyor · succeeded başarılı · failed hata · cancelled kullanıcı iptali · expired süre aşımı
transactions.channeleasy masaüstü USB token · mobileid operatör mobil imzası
transactions.formatpades PDF · cades her tür dosya · xades XML
transactions.kindbes temel · t zaman damgalı · xl uzun ömürlü · a arşiv
transaction_events.sourceapi kullanıcı isteği · worker arka plan işi · watchdog gözcü süreç · user kullanıcı iptali
feedback.categoryissue sorun · feature öneri · praise memnuniyet · support destek
feedback.statusnew · triaged · in_progress · closed

Açık DB Kararları

Aşağıdaki başlıklar bir sonraki turda konuşulmak üzere açık bırakılmıştır. Her biri için mevcut öneri ve gerekçesi verilmiştir.

KonuDurumÖneri ve gerekçe
Tek işlem tablosu mu, tip başına ayrı tablo muKarar verildiÇekirdek + tipe özgü bire bir detay tabloları. Gerekçe: yol haritasındaki üç yeni servis (Mobil Dijital Onay, E-İmzalı Onay, Kimlik Beyanı) birer işlem tipi olarak gelecek; tek tabloda kalmak, sonradan gerçek imza kayıtları içeren bir tabloyu bölmek anlamına gelirdi. Kalıtım eşlemesi değil isteğe bağlı bire bir seçildi — listeleme sorguları birleştirme yapmaz. Bkz. Bölüm 08.
Değer kümeleri: metin + kısıt mı, veritabanı enum tipi miÖneriMetin + değer kısıtı. Postgres enum tipine değer eklemek göç gerektirir ve değer çıkarmak pratikte mümkün değildir; ürün henüz kümelerin son hâline gelmedi. Metin + kısıt, aynı güvenceyi esnek biçimde verir.
Append-only'nin veritabanında zorlanmasıÖneritoken_ledger ve transaction_events üzerine güncelleme ve silmeyi reddeden kural konulması. Şu an bu disiplin yalnızca uygulama kodunda; ilerideki bir hata veya elle müdahale sessizce geçmişi bozabilir.
Belge saklama süresiYönelimİşlemden sonra kısa saklama (~15 dakika) ihtimali var. Şema buna hazır; karar ürün tarafında. Netleştirilmesi gerekenler: kaynak belge ile imzalı çıktı aynı süreye mi tabi, kullanıcıya sınır nerede anlatılacak, ve süre dolduktan sonra indirme ucunun vereceği yanıt.
Nesne deposu seçimiKarar bekliyorHangi depolama (nesne deposu / disk). IObjectStore arayüzü arkasında; iskelette disk. Belgeleri veritabanına gömmek yedekleme boyutunu ve geri dönüş süresini hızla yönetilemez hâle getirir.
Saklama ve arşivleme süreleriKarar bekliyorDoğrulama oturumları kısa süre sonra temizlenebilir. İşlem ve token kayıtları hesap verebilirlik taşıdığı için uzun süre kalır. Kesin süreler hukuki gereklilikle birlikte belirlenmeli.
Çok kiracılılıkÖneriMVP tek kiracı; kiracı kolonu açılmaz. Jetimza tek bir üründür ve şu an ikinci bir kiracı ihtiyacı yok. Gerekirse eklenmesi göçle mümkün, ama şimdiden taşımak her sorguya ve her indekse bedel yazar.
Kimlik numarası şifreleme anahtarıKarar bekliyorŞifreleme uygulama katmanında yapılıyor; anahtarın nerede duracağı (ortam değişkeni / anahtar kasası) ve nasıl döndürüleceği belirlenmeli. Anahtar döndürme, saklanan değerlerin yeniden şifrelenmesini gerektirir — bu işi mümkün kılan bir anahtar sürümü alanı gerekebilir.
Genel denetim kaydıKarar bekliyorToken ve işlem geçmişi kendi denetim izini taşıyor. Bunun ötesinde "kim neyi ne zaman değiştirdi" tipi genel bir kayıt gerekip gerekmediği belirlenmeli — özellikle ek token onayları elle verildiği sürece.
Şema adı ve göç aracıÖneriUygulamaya ait tablolar öntanımlı şemada; göçler EF Core ile yürütülür ve sürüm kontrolünde tutulur. Kısmi tekillik indeksleri ve değer kısıtları göç dosyalarında elle yazılır.
Yumuşak silme kapsamıÖneriYalnızca users. Her tabloya yumuşak silme yaymak her sorguya bir filtre borcu yükler ve unutulan tek filtre sızıntıya dönüşür. Diğer tablolarda kayıt ya vardır ya yoktur.
Şema Prensibi

Bir iş kuralı veritabanında zorlanabiliyorsa, orada zorlanır. Uygulama kodu kuralı açıklar; veritabanı kuralı garanti eder.