Jetimza MVP Servis & Kod Mimarisi
imzaio-jet-api deposunun servis topolojisi, proje düzeni,
servisler arası iletişim biçimi ve kod yerleşim kuralları. imza.io SignGate'in bölme
mantığı referans alınmıştır.
Bölme çizgisi dış sınır ve ölçekleme eksenidir — asla veri değil. Aynı işlem bloğunda yazılan şeyler aynı süreçte kalır.
Bölme İlkesi
SignGate'in yapısına bakıldığında iki karar birlikte okunmalı — sadece neyi böldüğü değil, neyi birleştirdiği de öğretici.
| SignGate kararı | Gerekçe |
|---|---|
| Kanallar ayrı PFX · MobileID · Easy | Her biri farklı bir dış sisteme bağlı: diskteki sertifika dosyası, operatör servisi, masaüstü istemcisi. Farklı hata modları, farklı zaman aşımları, farklı ölçekleme davranışı. |
| Profiller ayrı CAdES · PAdES · XAdES | Ağır yerel kütüphane taşıyorlar. Ayrı süreçte olmaları, birinin sorununun diğerlerini etkilememesini sağlıyor. |
| Gateway BİRLEŞTİRİLDİ | Ayrı bir ağ geçidi servisi anlamsız bir dolaylılıktı; işlevi çekirdek servise katıldı, proje eskiye taşındı. Bu dokümanın en önemli referansı budur. |
Sebep varsa böl, sebep bittiyse birleştir. Bir servisin varlık gerekçesi tek cümleyle söylenemiyorsa, o servis olmamalıdır.
Jetimza'da bölme gerekçeleri
Aynı ölçüt uygulandığında üç dağıtım birimi çıkıyor. Her birinin gerekçesi Bölüm 03'te tek tek verilmiştir; özet olarak: API istek trafiğine, Worker kuyruk derinliğine, Notifier gönderim hacmine göre ölçeklenir ve üçünün hata modları birbirinden bağımsızdır.
Neyi BÖLMÜYORUZ
- Ayrı bir token servisi — token askısı ile işlem kaydı aynı veritabanı işlem bloğunda yazılıyor; ayırmak dağıtık işlem gerektirir
- Ayrı bir kimlik/auth servisi — aynı kullanıcı tablosuna bakıyor, ayırmanın tek sonucu ağ üzerinden okuma olur
- Ayrı bir ağ geçidi — SignGate'in geri aldığı karar; tekrarlamayız
- Katman başına ayrı proje (Domain / Application / Infrastructure) — bunlar dağıtım birimi değil, klasör
- Her servise ayrı veritabanı — aşağıdaki dürüst adlandırmaya bakınız
Bu yapı tek veri kümesini paylaşan çok süreçli bir uygulamadır; "her servise ayrı veritabanı" anlamında bir mikroservis mimarisi değildir. SignGate de aynısını yapıyor — üç servis ailesi tek veri projesini paylaşıyor. Bu bilinçli bir tercih: token muhasebesi ile işlem kaydının tek bir işlem bloğunda yazılabilmesi, bu üründe dağıtık tutarlılıktan çok daha değerlidir. Adını doğru koymak, ileride yanlış beklenti üretmemek için önemlidir.
Servis Topolojisi
Jet.Api iş yazar, iş yapmaz; uzun süren
her şey Jet.Worker'a, gönderim ise
Jet.Notifier'a düşer. Kaynak belgeyi API nesne deposuna yazar,
imzalı çıktıyı Worker yazar. Easy, jet-web tarafından derin bağlantıyla açılır ve
kendi oturumunu SignGate'in SignalR ucuna kurar — jet servisleri Easy ile
doğrudan konuşmaz.
Diyagramda iki yerde ok kesişmesi vardır (
Api→PostgreSQL ile
Worker→SignGate; Worker→Nesne deposu ile
Notifier→PostgreSQL). İkisi de yalnızca çizim yönlendirmesidir,
bir bağımlılık ifade etmez: Worker sağa, Notifier sola gittiği için yollar bir noktada
birbirini geçer.
Servisler
ImzaIo.Jet.Api
- Sorumluluk
- OTP ve oturum, profil, token bakiyesi ve dökümü, işlem başlatma, geçmiş, davet, geri bildirim.
- Yapmaz
- Dış servis çağrısı beklemez, SMS göndermez, zamanlanmış iş çalıştırmaz. Kayıt yazar, kuyruğa haber verir, döner.
- Ölçekleme
- İstek hacmi. Durumsuz — istenildiği kadar örnek.
- Ayrı olma gerekçesi
- Kısa ömürlü istek/yanıt işi; uzun süren işlerle aynı süreçte olması istek havuzunu tüketir.
ImzaIo.Jet.Worker
- Sorumluluk
- İmza işçisi (SignGate çağrısı), gözcü (askıda kalanı kapatır, token iade eder), saklama temizliği, günlük özet üretimi.
- Yapmaz
- Dışarıya HTTP yüzeyi açmaz. Yalnızca sağlık ucu.
- Ölçekleme
- Kuyruk derinliği. Zamanlanmış işler tek örnekte (Bölüm 08).
- Ayrı olma gerekçesi
- Dört somut neden: (1) Easy imzası kullanıcı PIN girene kadar dakikalarca açık kalır; (2) API dağıtımı uçuştaki imzayı öldürmemeli; (3) zamanlanmış işler lider seçimi ister; (4) ölçekleme ekseni farklı.
ImzaIo.Jet.Notifier
- Sorumluluk
- SMS ve bildirim gönderimi, sağlayıcı yeniden deneme/geri çekilme, teslim bildirimi ucunu karşılama, maliyet kaydı.
- Yapmaz
- İş kuralı yürütmez; ne gönderileceğine API/Worker karar verir, o yalnızca gönderir.
- Ölçekleme
- Gönderim hacmi.
- Ayrı olma gerekçesi
- SignGate terimleriyle bir kanal: dış sağlayıcı sınırı, kendi hata modu, kendi hız sınırı ve gelen teslim bildirimi ucu. Ayrıca sağlayıcı kimlik bilgileri API sürecinden çıkmış olur.
Gönderimin ayrı serviste olması, kullanıcının kodu daha geç alacağı anlamına gelmez.
API kaydı yazıp pg_notify ile haber verir; Notifier
LISTEN üzerinde beklediği için yoklama gecikmesi
yoktur, milisaniyeler içinde uyanır. Gecikmenin gerçek kaynağı operatör
şebekesidir ve o her iki tasarımda da aynıdır.
Paylaşılan Kütüphaneler
Paylaşım kütüphane düzeyindedir, katman düzeyinde değil. Her biri tek bir somut şey yapar; hiçbiri "iş mantığı katmanı" değildir.
| Proje | Kullanan | İçerik ve sınırı |
|---|---|---|
| ImzaIo.Jet.ServiceDefaults | 3 servis | Serilog kurulumu, OpenTelemetry, sağlık uçları, CorrelationIdMiddleware, günlük zenginleştirici, ölçüm tanımları ve histogram kovaları. SignGate'teki eşadlı projenin birebir karşılığı — üç servis tek satırla aynı gözlemlenebilirlik davranışını alır. |
| ImzaIo.Jet.Data | 3 servis | JetDbContext, varlıklar, eşlemeler, göçler. Kısıtlar ve kısmi indeksler burada tanımlanır. Sınır: iş kuralı içermez — yalnızca şema ve erişim. |
| ImzaIo.Jet.Contracts | 3 servis | Paylaşılan numaralandırmalar (işlem tipi, durum, token gerekçesi), kuyruk kayıt tipleri, ortak sabitler. Kimseye bağımlı değildir. |
| ImzaIo.Jet.SignGate | Worker | ISignGateClient + gerçek ve sahte uygulamaları, oturum jetonu önbelleği, yeniden deneme politikası. API bu projeye bağımlı değildir. |
| ImzaIo.Jet.Storage | Api · Worker | IObjectStore + disk uygulaması. Nesne deposu seçimi geldiğinde tek sınıf eklenir. |
| ImzaIo.Jet.AppHost | geliştirme | Aspire orkestrasyonu: üç servisi + PostgreSQL + Valkey birlikte ayağa kaldırır, telemetri uçlarını otomatik bağlar. Üretimde kullanılmaz. |
Kütüphaneler birbirine bağlanmaz; yalnızca servisler kütüphanelere bağlanır.
Jet.Data içinden Jet.SignGate
çağırmak, tam olarak kaçındığımız katmanlı yapının başlangıcıdır. Tek istisna:
herkes Jet.Contracts'a bağlanabilir, çünkü o hiçbir şeye bağlı değildir.
Servisler Arası İletişim
Servisler birbirini doğrudan çağırmaz. İletişim, zaten var olan iş tabloları üzerinden yürür.
| Kuyruk | Yazan → Okuyan | Tetikleyen kayıt |
|---|---|---|
| transactions | Api → Worker | status = 'queued' olan imza işlemleri |
| notifications | Api / Worker → Notifier | status = 'pending' olan bildirimler |
| sms_deliveries | Notifier ↔ sağlayıcı | status = 'queued' gönderimler; teslim bildirimi aynı satırı günceller |
Uyandırma ve çekme
LISTEN/NOTIFY uyandırır — yoklama gecikmesi yoktur.
FOR UPDATE SKIP LOCKED çeker — birden çok Worker örneği aynı
satırı almaz, birbirini de beklemez. Haber kaybolsa bile düşük sıklıklı bir yedek tarama
işi kayıtları yakalar; yani haber bir hızlandırıcıdır, doğruluk kaynağı değildir.
İş zaten kalıcı bir tabloda duruyor. Ayrı bir kuyruk ürünü eklemek ikinci bir
doğruluk kaynağı yaratır ve beraberinde şu sınıf hataları getirir: "kuyrukta var
ama tabloda yok", "tabloda var ama kuyruğa düşmemiş", "kuyruk sildi ama işlem bloğu geri
alındı". Bunları çözmek için yazılan şey zaten giden-kutusu desenidir — biz de
doğrudan onu kullanıyoruz, arada bir ürün olmadan.
Ne zaman değişir: gönderim hacmi tek veritabanının tolere edeceği
seviyeyi aştığında ya da jet dışından tüketiciler çıktığında. O noktada tablolar
giden-kutusu olarak kalır, üzerine bir yayıcı eklenir. Bugünkü karar geri
dönülemez bir kapı kapatmıyor.
Servisler arası doğrudan çağrı
Yok. Ne HTTP ne gRPC. Bir servisin diğerinden senkron cevap beklemesi gereken bir akış bulunmuyor; çıkarsa önce o akışın gerçekten senkron olması gerekip gerekmediği sorgulanacaktır.
Kod Yerleşimi
Klasörler katman değil, özellik
Features/Tokens/TokensController.cs doğrudan
JetDbContext'e dokunur. Araya depo, iş servisi ya da aracı
girmez. TokenLedger gibi sınıflar bir servis katmanı
değildir; controller'ın çağırdığı yardımcılardır — arayüzleri yoktur,
bağımlılık enjeksiyonu dolaylılığı yoktur.
Yalnızca gerçekten iki uygulaması olduğunda. Bugün üç yer: SignGate istemcisi (gerçek + sahte), SMS sağlayıcısı (günlüğe yazan + gerçek), nesne deposu (disk + ileride başka). "İleride gerekebilir" gerekçesiyle arayüz yazmak, test edilmeyen bir soyutlama ve okunması zor bir kod tabanı üretir.
Türkçe kültür tuzağı
Kullanıcı adı normalleştirmesi invariant kültürle yapılır. Türkçe kültürde
"I" küçültüldüğünde "ı" olur;
@Ilker ile @ilker eşleşmez ve
benzersizlik kontrolü sessizce yanlış çalışır. Aynı sebeple veritabanı tarafındaki
benzersizlik de lower(username) ifade indeksiyle kurulur.
Çalışma Zamanı & Portlar
SignGate 17xxx aralığını kullanıyor. Jetimza aynı düzeni 18xxx ile izler; böylece iki yığın aynı makinede çakışmadan çalışabilir.
| Bileşen | Port | Not |
|---|---|---|
| ImzaIo.Jet.Api — HTTP | 18000 | Yerel geliştirme |
| ImzaIo.Jet.Api — HTTPS | 18001 | Ana yüzey |
| ImzaIo.Jet.Worker | 18010 | Yalnız /health /alive /ready |
| ImzaIo.Jet.Notifier | 18020 | Sağlık + sağlayıcı teslim bildirimi ucu |
| Valkey | 18300 | Hız sınırı sayaçları, SignGate jetonu önbelleği |
| PostgreSQL | 18310 | Veri + iş kuyruğu |
Geliştirme ve dağıtım
| Ortam | Nasıl ayağa kalkar |
|---|---|
| Yerel — tam | Jet.AppHost (Aspire): üç servis + PostgreSQL + Valkey tek komutla; telemetri uçları otomatik bağlanır. |
| Yerel — sade | compose.yml yalnızca PostgreSQL + Valkey; servisler doğrudan çalıştırılır. |
| Dağıtım | Üç ayrı görüntü, üç ayrı dağıtım birimi. Hedef ortam henüz açık karar; görüntü üretimi ilk günden kurulur. |
Eşzamanlılık & Lider
| İş | Çalışma biçimi | Nasıl korunur |
|---|---|---|
| SignatureWorker | Her örnekte | SKIP LOCKED — örnekler aynı satırı almaz. Örnek sayısı serbestçe artırılabilir. |
| SmsDispatcher | Her örnekte | Aynı biçimde SKIP LOCKED. |
| WatchdogWorker | Her örnekte | Süre aşımı kapatma tek satırı hedefler; SKIP LOCKED yeterli. |
| RetentionWorker | Tek örnekte | Danışmanlık kilidi (pg_try_advisory_lock). Kilidi alamayan örnek turu atlar. |
| RollupWorker | Tek örnekte | Aynı kilit yöntemi. Ayrıca özet satırı (user_id, date) tekilliğiyle korunur — iki kez çalışsa bile satır çoğalmaz. |
| Aylık token tanımlama | Talep anında | Zamanlanmış iş değildir; kullanıcı bakiyeye dokunduğunda tanımlanır ve tekil anahtarla korunur. |
PostgreSQL'in danışmanlık kilitleri bu iş için yeterlidir: kilit bağlantı ömrüyle sınırlıdır, süreç çökerse kendiliğinden serbest kalır, ayrı bir bileşen gerektirmez. Zamanlanmış işlerimiz dakikalar mertebesinde ve yeniden çalıştırılabilir olduğu için daha güçlü bir mekanizmaya ihtiyaç yoktur.
Yapı Kuralları
Aşağıdakiler kod incelemesinde reddedilme sebebidir. Amaç, zamanla farkında olmadan kaçındığımız yapıya kaymamak.
- Depo (repository) sınıfı yazmak.
DbContextzaten odur. - Controller ile veri arasına iş servisi katmanı koymak. Yardımcı sınıf olur, katman olmaz.
- Tek uygulaması olan şeye arayüz yazmak.
- Katman başına proje açmak (Domain / Application / Infrastructure).
- Servisler arası senkron çağrı eklemek — akış kuyruk üzerinden kurulur.
- Uygulama günlüklerini veritabanına yazmak.
- Metrik etiketine kullanıcı kimliği, telefon, kimlik numarası veya işlem kimliği koymak.
- Kaydetme adımına takılan genel denetim kancası yazmak.
- Yerel saat yazmak — her şey UTC.
- Türkçe kültürle metin küçültmek — invariant kullanılır.
- Kimlik numarasını, OTP kodunu veya jetonu günlüğe yazmak.
Deponun CLAUDE.md ve AGENTS.md
dosyalarında özetlenir; böylece hem ekip hem de yardımcı araçlar aynı sınırları görür.
jet-web ve jet-site depolarında aynı desen zaten kullanılıyor.
Faz Planı
| Faz | Çıktı | İçerik |
|---|---|---|
| 0 | İskele | Üç servis projesi + paylaşılan kütüphaneler + ServiceDefaults + AppHost + compose + CI + sağlık uçları. Göç ve iş kodu yok. |
| 1 | api/openapi.yaml | Sözleşme yazılır ve depoya girer → jet-web paralel ilerlemeye başlar. |
| 2 | Şema | 23 tablo, kısıtlar, kısmi indeksler, ilk göç. |
| 3 | Üyelik | OTP → JWT → kayıt → profil. Notifier günlüğe yazan sağlayıcıyla gerçek akışta. |
| 4 | Token | Tanımlama, bakiye, döküm, askı/kesinleşme/iade. |
| 5 | İşlem | İmza başlatma, Worker, gözcü, durum makinesi. SignGate sahte istemciyle. |
| 6 | Gerçek entegrasyon | SignGate erişim bilgileri ve SMS sağlayıcısı geldiğinde iki sınıf değişir. |
Her faz çalışan bir dikey kesit bırakır. Gözlemlenebilirlik Faz 0'dadır — sonradan eklenen bir katman değil, ilk günün parçasıdır.