Jetimza Brand Logo
v1.0  ·  Servis & Kod Mimarisi

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.

DurumOnaylandı
Versiyonv1.0
Depoimza-io/imzaio-jet-api
Servis3 dağıtım birimi
Port18xxx
Mimarinin Temel Sözü

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.
Çıkarılan Kural

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
Dürüst adlandırma

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

İSTEMCİ SERVİS ÇALIŞMA ZAMANI DIŞ SERVİS imzaio-jet-web Vite + React app.jetimza.com imza.io Easy Masaüstü oturumunu SignGate SignalR'e kurar ImzaIo.Jet.Api 18000/18001 İstek/yanıt · JWT Kuyruğa iş yazar, iş yapmaz ImzaIo.Jet.Worker 18010 İmza işçisi · Gözcü Saklama · Günlük özet ImzaIo.Jet.Notifier 18020 SMS/bildirim gönderimi Teslim bildirimi ucu Valkey 18300 Hız sınırı sayaçları SignGate jeton önbelleği PostgreSQL 18310 Tek doğruluk kaynağı + İŞ KUYRUĞU Nesne deposu IObjectStore Kaynak belge + çıktı delete_after ile silinir imza.io SignGate v2 dış sign-file · validate · pdf-timestamp SMS sağlayıcı dış gönderim + teslim bildirimi HTTPS · JWT deeplink hız sınırı · önbellek INSERT + pg_notify LISTEN + SKIP LOCKED imzalı çıktı LISTEN + SKIP LOCKED REST · Bearer gönderim · teslim bildirimi
Okuma sırası: yukarıdan aşağı — istemci, servis, çalışma zamanı, dış servis. Üç servis de aynı PostgreSQL örneğine bakar; kuyruk da orada. Jet.Apiyazar, 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

HTTP 18000 · HTTPS 18001
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

18010 · yalnız sağlık
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

18020 · sağlık + geri bildirim
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.
OTP gecikmesi sorun olmuyor mu

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.

ProjeKullananİçerik ve sınırı
ImzaIo.Jet.ServiceDefaults3 servisSerilog 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.Data3 servisJetDbContext, 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.Contracts3 servisPaylaşılan numaralandırmalar (işlem tipi, durum, token gerekçesi), kuyruk kayıt tipleri, ortak sabitler. Kimseye bağımlı değildir.
ImzaIo.Jet.SignGateWorkerISignGateClient + gerçek ve sahte uygulamaları, oturum jetonu önbelleği, yeniden deneme politikası. API bu projeye bağımlı değildir.
ImzaIo.Jet.StorageApi · WorkerIObjectStore + disk uygulaması. Nesne deposu seçimi geldiğinde tek sınıf eklenir.
ImzaIo.Jet.AppHostgeliştirmeAspire orkestrasyonu: üç servisi + PostgreSQL + Valkey birlikte ayağa kaldırır, telemetri uçlarını otomatik bağlar. Üretimde kullanılmaz.
Bağımlılık yönü

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.

KuyrukYazan → OkuyanTetikleyen kayıt
transactionsApi → Workerstatus = 'queued' olan imza işlemleri
notificationsApi / Worker → Notifierstatus = 'pending' olan bildirimler
sms_deliveriesNotifier ↔ sağlayıcıstatus = 'queued' gönderimler; teslim bildirimi aynı satırı günceller

Uyandırma ve çekme

-- Yazan taraf: kayıt ve haber aynı işlem bloğunda INSERT INTO transactions (...) VALUES (...); SELECT pg_notify('jet_tx', 'queued'); -- Okuyan taraf: LISTEN ile uyanır, SKIP LOCKED ile çeker LISTEN jet_tx; SELECT * FROM transactions WHERE status = 'queued' ORDER BY created_at FOR UPDATE SKIP LOCKED LIMIT 10;

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.

Neden mesaj kuyruğu ürünü eklemiyoruz

İş 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

imzaio-jet-api/ ├── ImzaIo.Jet.slnx ├── Directory.Build.props net10.0 · nullable · uyarı = hata ├── .editorconfig · .gitattributes · .gitignore ├── compose.yml · .env.example ├── README.md · CLAUDE.md · AGENTS.md │ ├── api/ │ └── openapi.yaml SÖZLEŞME — jet-web bunu tüketir │ ├── .github/ │ ├── workflows/ci.yml derleme · test · biçim · sözleşme sapması │ └── pull_request_template.md │ ├── src/ │ ├── ImzaIo.Jet.Api/ │ │ ├── Program.cs │ │ ├── Features/ ← DİKEY KESİTLER │ │ │ ├── Auth/ AuthController · OtpCodes · JwtIssuer │ │ │ ├── Me/ MeController │ │ │ ├── Tokens/ TokensController · TokenLedger │ │ │ ├── Transactions/ Signatures · Verifications · Timestamps · Preflight │ │ │ ├── Invites/ InvitesController │ │ │ └── Feedback/ FeedbackController │ │ ├── Security/ Masking · FieldEncryption · Hmac │ │ └── Common/ ProblemDetails · Paging · Idempotency · IClock │ │ │ ├── ImzaIo.Jet.Worker/ │ │ ├── Program.cs │ │ ├── TransactionQueue.cs LISTEN/NOTIFY + SKIP LOCKED │ │ ├── SignatureWorker.cs │ │ ├── WatchdogWorker.cs │ │ ├── RetentionWorker.cs │ │ └── RollupWorker.cs │ │ │ ├── ImzaIo.Jet.Notifier/ │ │ ├── Program.cs │ │ ├── NotificationQueue.cs │ │ ├── SmsDispatcher.cs │ │ ├── DeliveryCallbackEndpoint.cs │ │ └── Providers/ ISmsProvider · LogSmsProvider │ │ │ ├── ImzaIo.Jet.Data/ DbContext · Entities · Configurations · Migrations │ ├── ImzaIo.Jet.Contracts/ numaralandırmalar · kuyruk tipleri · sabitler │ ├── ImzaIo.Jet.SignGate/ ISignGateClient · Http… · Stub… │ ├── ImzaIo.Jet.Storage/ IObjectStore · DiskObjectStore │ ├── ImzaIo.Jet.ServiceDefaults/ Serilog · OTel · sağlık · korelasyon · ölçümler │ └── ImzaIo.Jet.AppHost/ Aspire orkestrasyonu │ └── tests/ └── ImzaIo.Jet.Tests/

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.

Arayüz ne zaman yazılır

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şenPortNot
ImzaIo.Jet.Api — HTTP18000Yerel geliştirme
ImzaIo.Jet.Api — HTTPS18001Ana yüzey
ImzaIo.Jet.Worker18010Yalnız /health /alive /ready
ImzaIo.Jet.Notifier18020Sağlık + sağlayıcı teslim bildirimi ucu
Valkey18300Hız sınırı sayaçları, SignGate jetonu önbelleği
PostgreSQL18310Veri + iş kuyruğu

Geliştirme ve dağıtım

OrtamNasıl ayağa kalkar
Yerel — tamJet.AppHost (Aspire): üç servis + PostgreSQL + Valkey tek komutla; telemetri uçları otomatik bağlanır.
Yerel — sadecompose.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çimiNasıl korunur
SignatureWorkerHer örnekteSKIP LOCKED — örnekler aynı satırı almaz. Örnek sayısı serbestçe artırılabilir.
SmsDispatcherHer örnekteAynı biçimde SKIP LOCKED.
WatchdogWorkerHer örnekteSüre aşımı kapatma tek satırı hedefler; SKIP LOCKED yeterli.
RetentionWorkerTek örnekteDanışmanlık kilidi (pg_try_advisory_lock). Kilidi alamayan örnek turu atlar.
RollupWorkerTek örnekteAynı 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ımlamaTalep anındaZamanlanmış iş değildir; kullanıcı bakiyeye dokunduğunda tanımlanır ve tekil anahtarla korunur.
Neden ayrı bir lider seçim altyapısı yok

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. DbContext zaten 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.
Bu kurallar nerede yaşar

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.
1api/openapi.yamlSözleşme yazılır ve depoya girer → jet-web paralel ilerlemeye başlar.
2Şema23 tablo, kısıtlar, kısmi indeksler, ilk göç.
3ÜyelikOTP → JWT → kayıt → profil. Notifier günlüğe yazan sağlayıcıyla gerçek akışta.
4TokenTanımlama, bakiye, döküm, askı/kesinleşme/iade.
5İşlemİmza başlatma, Worker, gözcü, durum makinesi. SignGate sahte istemciyle.
6Gerçek entegrasyonSignGate erişim bilgileri ve SMS sağlayıcısı geldiğinde iki sınıf değişir.
Faz İlkesi

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.