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.
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>_idkalı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
uuidbirincil 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
bigintkimlik 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.
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ı
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.
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 → Hedef | Kolon | Tip | Silme | Anlam |
|---|---|---|---|---|
| otp_challenges ⇢ users | phone_e164 | mantıksal | — | Doğrulama, kullanıcı oluşmadan önce başlar; bu yüzden yabancı anahtar yoktur |
| refresh_tokens → users | user_id | N—1 | CASCADE | Kullanıcının açık oturumları |
| refresh_tokens → refresh_tokens | replaced_by | N—1 | SET NULL | Jeton rotasyon zinciri (kendine referans) |
| user_consents → users | user_id | N—1 | CASCADE | Kullanıcının onayları |
| user_consents → consent_documents | document_id | N—1 | RESTRICT | Onaylanan metnin tam sürümü |
| token_grants → users | user_id | N—1 | RESTRICT | Kullanıcıya tanımlanmış token paketleri |
| token_ledger → users | user_id | N—1 | RESTRICT | Kullanıcının token hareketleri |
| token_ledger → token_grants | grant_id | N—1 | RESTRICT | Hareketin hangi paketi etkilediği (tanımlama hareketlerinde dolu) |
| token_requests → users | user_id | N—1 | CASCADE | Ek token talepleri |
| token_requests → token_grants | granted_grant_id | N—1 | SET NULL | Talep onaylandığında oluşan paket |
| invites → users | user_id | 1—1 | CASCADE | Her kullanıcının tek davet bağlantısı |
| invite_redemptions → invites | invite_id | N—1 | CASCADE | Bir davetin kaç kez kullanıldığı |
| invite_redemptions → users | invited_user_id | 1—1 | CASCADE | Davetle gelen kullanıcı — yalnızca bir kez |
| invite_redemptions → token_grants | reward_grant_id | N—1 | SET NULL | Davet edene verilen ödül paketi |
| feedback → users | user_id | N—1 | SET NULL | Geri bildirim; kullanıcı silinse de kayıt korunur |
| transactions → users | user_id | N—1 | RESTRICT | Kullanıcının işlemleri |
| transactions → token_ledger | hold_ledger_id | 1—1 | RESTRICT | İşlem başında alınan token karşılığı |
| transactions → token_ledger | commit_ledger_id | 1—1 | RESTRICT | İşlem başarıyla bittiğinde yazılan kesinleşme kaydı |
| transaction_signatures → transactions | transaction_id | 1—1 | CASCADE | İmza detayı. Birincil anahtarın aynı zamanda yabancı anahtar olması bire biri garanti eder |
| transaction_verifications → transactions | transaction_id | 1—1 | CASCADE | Doğrulama detayı |
| transaction_timestamps → transactions | transaction_id | 1—1 | CASCADE | Zaman damgası detayı |
| transaction_events → transactions | transaction_id | N—1 | CASCADE | İşlemin durum geçişleri |
| sms_deliveries → otp_challenges | otp_challenge_id | N—1 | CASCADE | Bir doğrulama kodu için yapılan gönderim denemeleri |
| notifications → users | user_id | N—1 | CASCADE | Kullanıcıya gönderilen bildirimler |
| api_idempotency_keys → users | user_id | N—1 | CASCADE | Tekrar-güvenliği anahtarının sahibi |
| api_idempotency_keys → transactions | transaction_id | N—1 | SET NULL | Anahtarın ilk çağrıda ürettiği işlem |
| daily_user_stats → users | user_id | N—1 | CASCADE | Günlük kullanıcı özeti |
| stored_objects ⇢ (çoklu) | owner_type + owner_id | mantıksal | — | Nesnenin sahibi tip + kimlik ikilisiyle belirtilir; tek bir tabloya yabancı anahtar verilemez |
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.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| id | uuid | PK | Kullanıcı kimliği. API'de bu değer görünür; sıralı sayı kullanılmaz. |
| phone_e164 | text | UQNN | E.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. |
| username | text | UQNN | Kullanı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. |
| status | text | NNCHECK | Hesap durumu: active, suspended, deleted. Askıya alınmış hesap giriş yapamaz ama verisi korunur. |
| phone_verified_at | timestamptz | Numaranı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_at | timestamptz | NN | Kayıt anı. Üyelik dönüşüm ölçümlerinin temel zaman referansı. |
| updated_at | timestamptz | NN | Son değişiklik anı. |
| last_login_at | timestamptz | Son başarılı giriş. Aktif kullanıcı sayımı ve hareketsiz hesap tespiti için. | |
| deleted_at | timestamptz | Yumuş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_reason | text | Askı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_until | timestamptz | Askı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_enc | bytea | Kimlik 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_hash | bytea | IDX | Aynı 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_at | timestamptz | Beyan 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.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| id | uuid | PK | Doğrulama oturumu kimliği. İstemciye bu değer döner (challengeId); kod bu kimlikle birlikte doğrulanır. |
| phone_e164 | text | NNIDX | Kodun gönderildiği numara. users'a yabancı anahtar değildir: kayıt akışında kullanıcı henüz yoktur. Bağ mantıksaldır. |
| purpose | text | NNCHECK | login veya register. Amaç ayrımı önemlidir: kayıt için üretilmiş bir kod giriş için kullanılamaz. |
| code_hash | bytea | NN | Kodun 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_count | smallint | NN | Yapılan yanlış deneme sayısı. Her hatalı girişte artar. |
| max_attempts | smallint | NN | İ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_at | timestamptz | NN | Kodun geçerlilik sonu (öntanımlı +3 dakika). Süre dolmuş kod, doğru bile olsa kabul edilmez. |
| consumed_at | timestamptz | Kodun başarıyla kullanıldığı an. Dolu olan kayıt tekrar kullanılamaz — tek kullanımlık olmasını sağlayan alan budur. | |
| created_ip | inet | Talebin geldiği adres. Kötüye kullanım incelemesi ve hız sınırı denetimi için. | |
| created_at | timestamptz | NN | Oluşturma anı. Yeniden gönderim bekleme süresi bu değerden hesaplanır. |
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.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| id | uuid | PK | Jeton kaydı kimliği. |
| user_id | uuid | FKNN | Jetonun sahibi. |
| token_hash | bytea | UQNN | Jetonun özeti. Jetonun kendisi yalnızca istemcide bulunur. Tekillik kısıtı, aynı jetonun iki kayda düşmesini engeller. |
| issued_at | timestamptz | NN | Verildiği an. |
| expires_at | timestamptz | NN | Geçerlilik sonu. |
| revoked_at | timestamptz | İptal anı. Çıkış yapıldığında veya güvenlik gerekçesiyle dolar. | |
| replaced_by | uuid | FK | Bu 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_agent | text | Oturumu açan istemci bilgisi. "Cihazlarım" ekranı ve şüpheli oturum tespiti için. | |
| ip | inet | Oturumun açıldığı adres. |
② Sözleşme & İzin
İzin yönetiminin tek zor tarafı şudur: kullanıcı bir metni değil, o metnin belirli bir sürümünü onaylar. Metin değiştiğinde eski onay yeni metni kapsamaz. Bu yüzden sürümlenmiş metin ve onay kaydı iki ayrı tablodadır.
consent_documents
5 kolon Sürümlenmiş hukuki metinler. Referans veri; uygulama tarafından tohumlanır.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| id | uuid | PK | Metin sürümü kimliği. |
| document_type | text | NNCHECK | Metnin türü: kullanıcı sözleşmesi, gizlilik politikası, çerez politikası, ticari elektronik ileti izni, e-imza kullanım kuralları, davet şartları. |
| version | text | NN | Sürüm etiketi (1.0, 1.1). Tür + sürüm birlikte tekildir. |
| effective_from | timestamptz | NN | Bu sürümün yürürlüğe girdiği an. Yeni kayıt olan kullanıcıya gösterilecek sürüm buradan seçilir. |
| content_url | text | Metnin yayınlandığı adres. Metnin kendisi veritabanında tutulmaz; yayınlanan sürüm kaynaktır. |
user_consents
7 kolon Kimin hangi metin sürümünü ne zaman onayladığı. Denetlenebilir izin kaydı.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| id | uuid | PK | Onay kaydı kimliği. |
| user_id | uuid | FKNN | Onaylayan kullanıcı. |
| document_id | uuid | FKNN | Onaylanan metnin tam sürümü. Metin türüne değil sürüme bağlanır. |
| accepted_at | timestamptz | NN | Onay anı. Zaman damgası, onayın hangi metin yürürlükteyken verildiğini kanıtlar. |
| ip | inet | Onayın verildiği adres. | |
| user_agent | text | Onayın verildiği istemci. | |
| revoked_at | timestamptz | Geri alma anı. Yalnızca geri alınabilir izinler için anlamlıdır — ticari elektronik ileti izni gibi. Kullanıcı sözleşmesi ve gizlilik politikası hizmetin ön koşuludur, geri alınmaz. |
UQ (user_id, document_id) — bir kullanıcı aynı metin sürümünü iki kez
onaylayamaz. Metnin yeni sürümü çıktığında yeni bir document_id
oluşur, dolayısıyla yeni bir onay satırı yazılır ve eski onay kaydı bozulmadan tarihte kalır.
③ 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.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| id | uuid | PK | Paket kimliği. |
| user_id | uuid | FKNN | Paketin sahibi. |
| kind | text | NNCHECK | Paketin 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. |
| amount | integer | NN>0 | Paketin başlangıç miktarı. Hiç değişmez — tarihsel gerçek. |
| remaining | integer | NN0..amount | Paketten 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_at | timestamptz | NN | Tanımlanma anı. |
| expires_at | timestamptz | Son 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_type | text | Paketi doğuran kaydın türü (token_request, invite_redemption, transaction). | |
| source_id | uuid | Paketi 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_key | text | UQNN | Ç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.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| id | bigint | PK | Artan 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_id | uuid | FKNN | Hareketin 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_id | uuid | FK | Hareketin 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. |
| delta | integer | NN | Bakiye 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. |
| reason | text | NNCHECK | Hareketin gerekçesi (Bölüm 10). Kullanıcıya gösterilen hareket dökümünün etiketi buradan üretilir. |
| ref_type | text | Hareketi doğuran kaydın türü. | |
| ref_id | uuid | Hareketi doğuran kaydın kimliği — genellikle bir işlem. | |
| balance_after | integer | NN | Hareket 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_key | text | UQNN | Aynı hareketin iki kez yazılmasını engeller. Ağ tekrarı veya arka plan işinin yeniden denenmesi durumunda tek savunma budur. |
| correlation_id | text | IDX | Hareketi 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_at | timestamptz | NN | Hareket anı. |
Bakiye nasıl hesaplanır
İmza işleminin token muhasebesi
| Adım | Ledger kaydı | delta | Paket ve işlem üzerindeki etkisi |
|---|---|---|---|
| İşlem başlatıldı | hold | −1 | token_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ı | commit | 0 | Bakiye 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 | +1 | remaining geri artar. Başarısız imza kullanıcıya bedel yazmaz. |
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.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| id | uuid | PK | Talep kimliği. |
| user_id | uuid | FKNN | Talep sahibi. |
| amount | integer | NN | Talep edilen miktar. MVP'de sabit 10; kolonda tutulur ki miktar politikası değişse eski talepler kendi miktarını korusun. |
| status | text | NNCHECK | pending, approved, rejected. |
| granted_grant_id | uuid | FK | Onay 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. |
| note | text | Kullanıcının gerekçesi veya ekibin notu. | |
| decided_by | text | Kararı veren kişi. MVP'de yönetim paneli olmadığı için serbest metindir; panel geldiğinde kimliğe bağlanır. | |
| decided_at | timestamptz | Karar anı. Talep-cevap süresi ölçümü buradan çıkar. | |
| created_at | timestamptz | NN | Talep anı. |
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ı.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| id | uuid | PK | Davet kaydı kimliği. |
| user_id | uuid | FKUQNN | Davet sahibi. Tekillik kısıtı, her kullanıcının tek davet bağlantısı olmasını sağlar. |
| code | text | UQNN | Bağ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_at | timestamptz | NN | Oluşturma anı. |
| disabled_at | timestamptz | Devre 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.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| id | uuid | PK | Kullanım kaydı kimliği. |
| invite_id | uuid | FKNN | Kullanılan davet. |
| invited_user_id | uuid | FKUQNN | Davetle 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_at | timestamptz | NN | Davetin kullanıldığı an — yeni kullanıcının kayıt tamamlandığı an. |
| reward_grant_id | uuid | FK | Davet 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. |
| ip | inet | Kaydın geldiği adres. Aynı adresten seri davet kabulü, sahte hesap üretiminin en görünür işaretidir. | |
| fraud_flag | boolean | NN | Şüpheli işaret. İşaretlenen kayıt ödül üretmez; inceleme sonrası kaldırılabilir. |
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.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| id | uuid | PK | Kayıt kimliği. |
| user_id | uuid | FK | Gönderen. Kullanıcı silinse bile geri bildirim korunur, bu yüzden bağ boşa çıkabilir. |
| category | text | NNCHECK | issue, feature, praise, support. Kategori ayrımı, tekrarlayan taleplerin gruplanmasını sağlar. |
| subject | text | Kısa başlık. | |
| body | text | NN | Kullanıcının anlatımı. |
| status | text | NNCHECK | new, triaged, in_progress, closed. |
| internal_note | text | Ekibin iç notu. Kullanıcıya gösterilmez. | |
| created_at | timestamptz | NN | Gö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.
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.
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ğı.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| id | uuid | PK | İş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_id | uuid | FKNN | İşlemin sahibi. |
| type | text | NNCHECK | signature, 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. |
| status | text | NNCHECK | Yaşam döngüsü durumu. İmzada queued → signing → …; doğrulama ve zaman damgası doğrudan sonuçlanır. |
| document_name | text | NN | Kullanıcının yüklediği dosya adı. |
| document_size | bigint | NN | Bayt cinsinden boyut. |
| document_sha256 | bytea | NNIDX | Kaynak 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_code | text | Özetten türetilen kullanıcı dostu doğrulama kodu (MAVI-47-KALE-82). | |
| source_object_key | text | Kaynak belgenin nesne depolamadaki adresi. | |
| result_object_key | text | İmzalı / damgalı çıktının adresi. Altyapı sonucu kalıcı saklamadığı için çıktıyı biz saklarız. | |
| signgate_request_ref | text | UQ | Bizim ü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_id | uuid | Altyapının kendi ürettiği kimlik. İki sistemin kaydını eşlemek için. | |
| hold_ledger_id | bigint | FKCHECK | İş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_id | bigint | FK | Baş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_count | smallint | NN | Arka plan işinin kaç kez denediği. |
| worker_instance | text | İşlemi yürüten arka plan işçisinin kimliği. Yeniden başlatma sonrası askıda kalan işleri ayırt eder. | |
| correlation_id | text | NNIDX | Log ve iz dünyasına açılan kapı. Destek ekranındaki bir işlemden bu değerle log kayıtlarına gidilir. |
| trace_id | text | IDX | Dağıtık iz kimliği. İşlemden uçtan uca iz görünümüne geçilir. |
| client_ip | inet | İşlemi başlatan adres. Hukuki sonuç doğuran işlemde adli açıdan gereklidir. | |
| user_agent | text | İşlemi başlatan istemci. | |
| created_at | timestamptz | NNIDX | Kayıt anı. Liste bu değere göre sıralanır. |
| started_at | timestamptz | Dış çağrının başladığı an. | |
| completed_at | timestamptz | CHECK | Sonuçlandığı an. Sonuçlanmış durumlarda dolu olması değer kısıtıyla zorunludur. |
| expires_at | timestamptz | IDX | En son sonuçlanması gereken an. Gözcü süreç bunu geçmiş ve hâlâ askıda olanları kapatır. |
| failure_code | text | Makine tarafından okunabilir hata kodu. | |
| failure_message | text | Altyapıdan gelen ham hata metni. Kullanıcıya ham hâliyle gösterilmez. |
transaction_signatures
10 kolon İmza detayı.type = signature olduğunda dolu.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| transaction_id | uuid | PKFK | Hem birincil hem yabancı anahtar. Bire bir ilişkiyi tek başına garanti eder: bir işlemin en fazla bir imza detayı olabilir. |
| channel | text | NNCHECK | easy veya mobileid. Artık gerçekten zorunlu — kanalsız imza satırı veritabanınca reddedilir. Tek tabloda bu mümkün değildi. |
| format | text | NNCHECK | pades, cades, xades. |
| kind | text | NNCHECK | İmza seviyesi: bes, t, xl, a. |
| thumbprint | text | Kullanıcının seçtiği sertifikanın parmak izi. Karttaki birden çok sertifikadan hangisiyle imzalandığını belgeler. | |
| visible_signature | boolean | NN | Görünür imza istenip istenmediği. Çıktının neden farklı göründüğü sorusunun cevabı. |
| certificate_subject | text | İmzalayan sertifikanın sahibi. İmza sonrasında gelir. | |
| certificate_serial | text | Sertifika seri numarası. | |
| certificate_identity_no | text | Sertifikadan gelen kimlik numarası — maskeli saklanır (123****8901). Tam numara hiçbir yerde tutulmaz. | |
| signing_time | timestamptz | İ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.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| transaction_id | uuid | PKFK | Bire bir bağ. |
| detected_format | text | Belgede tespit edilen imza biçimi. Kullanıcı biçim belirtmediği için sonuç bu alandan okunur. | |
| is_valid | boolean | NN | Genel sonuç. Listeleme ekranında rozeti bu belirler; ayrıntı için tam sonuca inmek gerekmez. |
| signature_count | integer | NN | Belgedeki toplam imza sayısı. |
| valid_signature_count | integer | NN | Geçerli imza sayısı. İkisi farklıysa belge kısmen geçerlidir — kullanıcıya anlatılması gereken en kritik ayrım budur. |
| result | jsonb | Doğ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.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| transaction_id | uuid | PKFK | Bire bir bağ. |
| tsa_provider | text | Damgayı veren zaman damgası otoritesi. Sağlayıcı değişirse eski kayıtlar kendi sağlayıcısını korur. | |
| tsa_serial | text | Damganın seri numarası. Bir damganın otorite nezdinde aranabilmesi için gereken tek değer. | |
| visible_stamp | boolean | NN | Görünür damga istenip istenmediği. |
| stamped_at | timestamptz | Damganı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.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| id | bigint | PK | Artan olay numarası; geçişleri kronolojik okumayı sağlar. |
| transaction_id | uuid | FKNN | İ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_status | text | Önceki durum. İlk kayıtta boştur. | |
| to_status | text | NN | Yeni durum. |
| source | text | NNCHECK | Geç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. |
| payload | jsonb | Geçişe ait ham bağlam — altyapı yanıtı, hata gövdesi, deneme numarası. | |
| created_at | timestamptz | NN | Geçiş anı. |
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ı.
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ğı.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| id | uuid | PK | Gönderim kaydı. |
| otp_challenge_id | uuid | FKNN | Hangi doğrulama oturumu için gönderildiği. Bir oturum için birden çok gönderim olabilir (yeniden gönder). |
| phone_e164 | text | NN | Hedef numara. |
| provider | text | NN | Gö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_id | text | IDX | Sağlayıcının verdiği kimlik. Teslim bildirimi geldiğinde kaydı bulmanın tek yolu budur. |
| status | text | NNCHECK | queued, sent, delivered, failed. "Gönderildi" ile "ulaştı" aynı şey değildir — OTP başarı oranı ancak bu ayrımla ölçülür. |
| error_code | text | Sağlayıcı hata kodu. Geçersiz numara mı, kota mı, operatör reddi mi — hepsi farklı aksiyon gerektirir. | |
| cost_micros | integer | Gö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_at | timestamptz | NN | Gönderim isteği anı. |
| sent_at | timestamptz | Sağlayıcının kabul ettiği an. | |
| delivered_at | timestamptz | Cihaza 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_id | text | IDX | Gönderimi doğuran isteğin ilişkilendirme kimliği. |
notifications
10 kolon Kullanıcıya giden bilgilendirmeler. Stratejideki "bildirim" iş paketinin karşılığı.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| id | uuid | PK | Bildirim kaydı. |
| user_id | uuid | FKNN | Alıcı. |
| channel | text | NNCHECK | sms, inapp. MVP'de e-posta yok — üyelikte e-posta toplanmıyor. |
| template_key | text | NN | Metin şablonu anahtarı. Metnin kendisi kayda gömülmez; şablon değişse bile hangi bildirimin gönderildiği anlaşılır. |
| payload | jsonb | Şablona geçen değerler (token miktarı, işlem adı). Şablon + değer ikilisi, gönderilen metni yeniden üretmeye yeter. | |
| status | text | NNCHECK | pending, 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_reason | text | Gönderilemediyse nedeni. | |
| correlation_id | text | IDX | Bildirimi doğuran işlem/istek bağı. |
| created_at | timestamptz | NN | Oluşturma anı. |
| sent_at | timestamptz | Gönderim anı. |
stored_objects
11 kolon Nesne depolamadaki dosyaların kaydı. Sahiplik, saklama ve çöp toplama.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| id | uuid | PK | Nesne kaydı. |
| object_key | text | UQNN | Depodaki tam adres. Tekillik, aynı dosyanın iki kez kaydedilmesini engeller. |
| bucket | text | NN | Depo bölmesi. Kaynak belge ile imzalı çıktının farklı saklama sürelerine sahip olmasını mümkün kılar. |
| size_bytes | bigint | NN | Boyut. Depolama maliyetinin kullanıcı ve işlem başına dağılımı buradan çıkar. |
| sha256 | bytea | NN | İçerik özeti. Depodaki dosyanın bozulup bozulmadığı bununla denetlenir. |
| content_type | text | MIME tipi. | |
| owner_type | text | NN | Sahibin türü (transaction). Tek bir tabloya yabancı anahtar verilemediği için sahiplik tip + kimlik ikilisiyle kurulur. |
| owner_id | uuid | NNIDX | Sahibin kimliği. |
| created_at | timestamptz | NN | Yükleme anı. |
| delete_after | timestamptz | IDX | Saklama 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_at | timestamptz | Depodan silindiği an. Kayıt kalır: bir belgenin var olduğu ve süresi dolduğu için silindiği kanıtlanabilir olur. |
Belgelerin işlemden kısa süre sonra (örneğin 15 dakika) silinmesi şemada hiçbir
değişiklik gerektirmez — delete_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.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| id | uuid | PK | Kayıt kimliği. |
| idempotency_key | text | NN | İstemcinin ürettiği anahtar (Idempotency-Key başlığı). Mobil ağda kopan bir istek tekrar gönderildiğinde aynı anahtarla gelir. |
| user_id | uuid | FKNN | Anahtarın sahibi. Anahtar yalnızca kendi kullanıcısı içinde tekildir; bir kullanıcının anahtarı diğerini etkilemez. |
| endpoint | text | NN | Hangi uç için. Aynı anahtarın farklı uçlarda kullanılması ayrı kayıtlar üretir. |
| request_hash | bytea | NN | İ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_status | smallint | İlk çağrının HTTP durumu. | |
| response_body | jsonb | İlk çağrının yanıtı. Tekrar gelen istek yeniden iş yapmaz; aynı yanıt döner. | |
| transaction_id | uuid | FK | İlk çağrının ürettiği işlem. Tekrarın hangi işleme denk geldiğini gösterir. |
| created_at | timestamptz | NN | İlk çağrı anı. |
| expires_at | timestamptz | IDX | Anahtarın saklanma sonu (öneri 24 saat). Tablo sınırsız büyümez. |
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.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| id | bigint | PK | Artan olay numarası. |
| actor_type | text | NNCHECK | Eylemi yapan: user, operator, system. Kullanıcının kendi yaptığı ile ekibin yaptığını ayırmak denetimin ilk şartıdır. |
| actor_id | text | Eyleyenin kimliği. Operatörler için serbest metin; MVP'de yönetim paneli olmadığı için kimliğe bağlanamaz. | |
| action | text | NNIDX | Eylem 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_type | text | NN | Etkilenen varlığın türü. |
| target_id | text | IDX | Etkilenen varlığın kimliği. |
| before | jsonb | Eylem öncesi ilgili alanlar — tüm satır değil. Kişisel veri taşımaz. | |
| after | jsonb | Eylem sonrası ilgili alanlar. | |
| ip | inet | Eylemin geldiği adres. | |
| user_agent | text | Eylemi yapan istemci. | |
| correlation_id | text | IDX | İlgili istek bağı. |
| created_at | timestamptz | NN | Eylem anı. |
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.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| id | uuid | PK | Özet satırı. |
| user_id | uuid | FKNN | Kullanıcı. |
| date | date | NN | Gün. Kullanıcı + gün ikilisi tekildir; özet işi tekrar çalışsa bile satır çoğalmaz. |
| signatures_started | integer | NN | Başlatılan imza sayısı. |
| signatures_succeeded | integer | NN | Tamamlanan imza sayısı. Başlatılan ile farkı, kullanıcının yaşadığı sürtünmedir. |
| timestamps_total | integer | NN | Zaman damgası işlemi. |
| verifications_total | integer | NN | Doğrulama işlemi. |
| tokens_granted | integer | NN | O gün tanımlanan token. |
| tokens_spent | integer | NN | O gün harcanan token. |
| first_activity | timestamptz | Gün içindeki ilk hareket. | |
| last_activity | timestamptz | Gün içindeki son hareket. |
daily_funnel_stats
6 kolon Stratejinin dört başarı ölçütünün doğrudan kaynağı.| Kolon | Tip | Kısıt | Açıklama |
|---|---|---|---|
| date | date | PK | Gün. |
| otp_requested | integer | NN | Kod isteyen sayısı. |
| otp_verified | integer | NN | Kodu doğrulayan sayısı → OTP başarı oranı. |
| registered | integer | NN | Üyeliği tamamlayan → üyelik dönüşümü. |
| first_signature | integer | NN | İlk imzasını atan → üyelik → ilk imza oranı. |
| invites_accepted | integer | NN | Kabul edilen davet → davet dönüşüm oranı. |
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 / İndeks | Tür | Uyguladığı kural |
|---|---|---|
| users (phone_e164) | tekillik | Bir 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) | tekillik | Aylık paket bir ay için yalnızca bir kez tanımlanır — eşzamanlı iki istek gelse bile. |
| token_ledger (idempotency_key) | tekillik | Aynı 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 pending | kısmi tekillik | Aynı anda tek açık ek token talebi. |
| user_consents (user_id, document_id) | tekillik | Aynı metin sürümü iki kez onaylanamaz. |
| invites (user_id) | tekillik | Kullanıcı başına tek davet bağlantısı. |
| invites (code) | tekillik | Davet kodları çakışmaz. |
| invite_redemptions (invited_user_id) | tekillik | Bir kullanıcı yalnızca bir kez davet ödülü doğurur. Ödül suistimalinin ana kilidi. |
| transactions (signgate_request_ref) | tekillik | Korelasyon anahtarı çakışmaz; iki işlem aynı SignGate isteğine bağlanamaz. |
| refresh_tokens (token_hash) | tekillik | Aynı jeton iki kayda düşemez. |
| otp_challenges (phone_e164, created_at ↓) | indeks | Numaraya ait son doğrulama oturumunu ve hız sınırı penceresini hızlı bulur. |
| token_grants (user_id, expires_at) WHERE remaining > 0 | kısmi indeks | Bakiye hesabı ve FIFO harcama seçimi. Kısmi olması indeksi küçük tutar: tükenmiş paketler indekste yer almaz. |
| token_ledger (user_id, id ↓) | indeks | Hareket dökümünün imleçli sayfalaması. |
| transactions (user_id, created_at ↓) | indeks | "İşlemlerim" listesi. |
| transactions (status, expires_at) WHERE askıda | kısmi indeks | Gö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) | indeks | Bir işlemin geçiş geçmişini sırayla okur. |
| transactions (document_sha256) | indeks | Aynı belgenin geçmiş işlemlerini bulur. |
| transactions — doğrulama ücretsiz | koş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 = FK | Bir 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 = FK | Bire bir. |
| transaction_timestamps (transaction_id) | PK = FK | Bire 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.
| Alan | Değerler ve anlamı |
|---|---|
| users.status | active normal · suspended giriş kapalı, veri korunur · deleted yumuşak silinmiş |
| otp_challenges.purpose | login mevcut hesaba giriş · register yeni kayıt |
| consent_documents.document_type | terms 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.kind | monthly aylık ücretsiz 10 · invite davet ödülü (30 gün) · manual ek talep onayı · refund iade |
| token_ledger.reason | monthly_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.status | pending bekliyor · approved onaylandı · rejected reddedildi |
| transactions.type | signature mobil imza veya e-imza · timestamp zaman damgası · verification doğrulama |
| transactions.status | queued 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.channel | easy masaüstü USB token · mobileid operatör mobil imzası |
| transactions.format | pades PDF · cades her tür dosya · xades XML |
| transactions.kind | bes temel · t zaman damgalı · xl uzun ömürlü · a arşiv |
| transaction_events.source | api kullanıcı isteği · worker arka plan işi · watchdog gözcü süreç · user kullanıcı iptali |
| feedback.category | issue sorun · feature öneri · praise memnuniyet · support destek |
| feedback.status | new · 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.
| Konu | Durum | Öneri ve gerekçe |
|---|---|---|
| Tek işlem tablosu mu, tip başına ayrı tablo mu | Karar 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 | Öneri | Metin + 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ı | Öneri | token_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üresi | Yö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çimi | Karar bekliyor | Hangi 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üreleri | Karar bekliyor | Doğ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 | Öneri | MVP 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 bekliyor | Token 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ı | Öneri | Uygulamaya 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ı | Öneri | Yalnı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. |
Bir iş kuralı veritabanında zorlanabiliyorsa, orada zorlanır. Uygulama kodu kuralı açıklar; veritabanı kuralı garanti eder.