
Dahili Sistem
Bergaz Gıda mandıra işletmesinin operasyonu için yazdığım dört uygulamalık sistem: süt kabul ve müstahsil cari muhasebesi, kalite analizi, stok ve telefondan kullanılan salt-okunur yönetici paneli. Dördü ayrı Neon veritabanında çalışıyor; 2007'den beri kullanılan masaüstü Paradox sisteminin 19 yıllık verisi ETL ile taşındı.
Mağaza tarafı bu sistemin parçası değil, ayrı yürütülüyor — bkz. Bergaz Gıda e-ticaret vakası.
İşletme 2007'den beri tek bilgisayarda çalışan bir masaüstü sistem kullanıyordu: Suttek.exe, Delphi/BDE üzerinde Paradox dosyaları. Geri kalan her şey kâğıda ve elektronik tablolara dağılmıştı. Sorunlar birbirini besliyordu — tek noktadan erişim, çok kullanıcı yok, mobil erişim yok, dosya tabanlı depolamanın bozulma riski (veri taşıma sırasında bir tablonun kayıt sayısı iki farklı okumada tutmadı) ve muhasebe programına makbuzların elle aktarılması. Bunların üstünde daha sessiz bir sorun vardı: mevcut ekranların tamamı operatör için tasarlanmıştı — yoğun form, çok sekme, klavye kısayolu. Yönetici bu ekranlardan bilgiye ulaşamıyordu; sorusunun cevabı sistemde duruyordu ama ona ulaşmanın yolu bir başkasından rica etmekti.
Dört rol, dört farklı ihtiyaç. Operatör: gün içinde çok sayıda tekrarlı süt kabul kaydı giriyor, mükerrer kayıt riski yüksek, hız her şeyden önemli. Muhasebeci: avans mahsuplaşması, ay kapanışı, kesinti hesabı ve makbuz üretiyor — kuruş hatası kabul etmiyor. Kalite sorumlusu: numune değerlerinin referans dışına çıktığını kaydı bitirmeden görmek zorunda ve referans aralığı süt türüne göre değişiyor. Yönetici: teknolojiye yatkın değil, telefondan bakıyor, az sayıda ekran ve büyük rakam istiyor — ve sisteme yanlışlıkla dokunma ihtimalinin sıfır olmasını istiyor.
Tek geliştirici, dört repo. Sistem sınırlarının çizilmesi, dört veri modeli ve migration'ları, Paradox ETL hattı ve doğrulaması, para ve kesinti hesap motorları, iki ayrı auth katmanı, operatör panelleri, yönetici PWA'sı ve dağıtım.
Dört bağımsız Next.js 16 App Router uygulaması, dört ayrı Neon Postgres veritabanı. Aralarında foreign key, cross-DB sorgu ve senkronizasyon mekanizması yok. Yazma yolu her uygulamada aynı: Server Action → Zod doğrulama → db.transaction(). Okuma yolu Server Component → sorgu katmanı. Muhasebe tarafında avans mahsuplaşması FIFO ve tek transaction içinde SELECT ... FOR UPDATE ile yürüyor; süt girişi (üretici, tarih) çiftine alınan advisory lock ile korunuyor; makbuz numarası Postgres SEQUENCE'inden geliyor. Analiz tarafında kesinti parametreleri kayıt anında satıra kopyalanıyor (deduction snapshot), dönem COMPLETED → LOCKED ile kilitleniyor ve kilitli dönem hem numune hem üretim kayıtlarına kapanıyor; günlük aggregate tablosu yazma anında güncelleniyor, böylece bölge ve üretici analizleri geçmişin tamamını yeniden taramıyor. Yönetim uygulaması dört bağlantı taşıyor: kendi auth veritabanına okuma+yazma, diğer üçüne salt-okuma. Diğer şemaların aynaları kolon kolon kopyalanıyor, index ve relation tanımları atılıyor; users ve audit_log hiçbir aynaya alınmıyor.
Önceki tasarım iki sistemin ortak bir şemayı paylaşmasını öngörüyordu; bu kaldırıldı. Her uygulama kendi migration, yedek ve PITR döngüsünde bağımsız; biri bozulduğunda diğerleri etkilenmiyor ve geliştirme hızları ayrışabiliyor — analiz tarafı Auth.js ve bcrypt üzerinde kalırken muhasebe argon2id'e geçebildi. Sınır keyfi değil, iş sınırıyla çakışıyor: para bir tarafta, kalite ve üretim öbür tarafta.
ÖdünleşimAynı kişi iki sistemde iki ayrı kayıt olarak duruyor; yeni bir üretici veya köy tanımı iki yere elle giriliyor ve eşleşme ad + köy üzerinden manuel. İki sistemi birleştiren bir rapor tek sorguyla alınamıyor — her uygulamadan ayrı ayrı dışa aktarıp dışarıda birleştirmek gerekiyor. Kullanıcılar her sisteme ayrı hesapla giriyor; tek oturum açma yok.
Postgres'ten gelen numeric kolonlar string olarak okunuyor ve hiçbir noktada Number'a çevrilmiyor; tüm aritmetik decimal.js ile tam precision yapılıyor. Yuvarlama yalnızca veritabanına yazma ve ekrana yazdırma anında, HALF_UP ile — banker's rounding değil, çünkü makbuzdaki kuruş elle yapılan hesapla birebir tutmak zorunda. Ayrıca yuvarlama birikimi ayrı bir kalem: mahsuplaşmada her satır için fiyat × kg yapılırsa son tahsis bir kuruş eksik kalabiliyor, son satırda kalan tutar üzerinden hesap yapılıyor.
ÖdünleşimHer aritmetik ifade bir Decimal sarmalı gerektiriyor; `a + b` yazılamıyor, hesap kodu belirgin biçimde daha uzun ve okunması daha yorucu. Daha kötüsü, kural derleyici tarafından zorlanmıyor — bir yerde string'i Number'a çeviren tek satır sessizce çalışır ve hatayı ancak kuruş tutmadığında fark edersin.
Yöneticinin ihtiyacı yeni bir giriş ekranı değil, mevcut veriye okunabilir bir pencereydi; yazma yeteneği eklemek yalnızca yanlış dokunma riski getirirdi. Salt-okunurluğun nerede garanti altına alındığı ise asıl karar: plan bunu Neon tarafında ayrı bir read-only rol olarak öngörüyordu, ancak bağlantılar pratikte hâlâ sahip rolünde kaldığı için garanti uygulama içindeki sorgu kalkanına düştü. Kalkan, sorgunun `select` veya `with` ile başlamasını şart koşuyor ve yazan bir anahtar kelime geçerse sorgu ağa çıkmadan reddediliyor — yazılabilir CTE dahil.
ÖdünleşimGaranti veritabanının değil uygulamanın içinde duruyor; kalkandan geçmeyen yeni bir sorgu yolu eklendiği anda koruma ortadan kalkıyor ve bunu hatırlatan hiçbir mekanizma yok. Kelime tabanlı reddetme ayrıca yanlış pozitif üretebiliyor: adında yasaklı bir kelime geçen meşru bir kolon ya da CTE sorguyu bloke ediyor, o noktada sorguyu kalkanın anlayacağı şekilde yeniden yazmak gerekiyor.
Paradox okuyucusu byte akışını latin1 ve cp437 karışımı bir mojibake olarak veriyor; düz cp857 veya cp1254 çözümlemesi başarısız oluyor. Çözücü iki aşamalı: önce gözlenmiş bozulmaları Türkçe harflere eşleyen deterministik bir harita, ardından kalan yüksek byte'lar için cp1254 ile cp857 arasında skor yarışması. İçe aktarım tek başına yeterli sayılmadı — yıl bazında toplam karşılaştırması, sayım karşılaştırması ve ayrı bir doğrulama koşusu eklendi, çözülemeyen satırlar elle inceleme listesine düşürüldü.
ÖdünleşimBirinci aşama gözleme dayalı bir harita: kaynakta daha önce görülmemiş bir bozulma çıktığında kendiliğinden genellemiyor, elle eklenmesi gerekiyor. İkinci aşama olasılıklı; iki kodlamanın da makul göründüğü kısa alanlarda yanlış tarafa düşebiliyor. Bu yüzden göç hiçbir zaman "çalıştır ve unut" olmadı, her koşu bir doğrulama turu ve insan kararı gerektirdi. Okuyucunun kendisi de x86_64 Python istiyor; Apple Silicon'da geliştirme ortamına Rosetta katmanı eklendi.
Uygulama çalışma anında havuzlanmış bağlantıyı kullanıyor; Vercel'de her serverless fonksiyon örneğinin ayrı bağlantı açması yüzünden oluşan bağlantı patlamasını bu önlüyor. Migration ise doğrudan bağlantı istiyor: pgbouncer'ın transaction modu çok ifadeli migration'ı bozuyor. Bu yüzden iki ayrı bağlantı dizesi tutuluyor ve migration yolu, doğrudan bağlantı tanımlı değilse çalışmayı reddediyor.
ÖdünleşimTek yerine iki bağlantı dizesi taşınıyor ve ikisinin de her ortamda (yerel, preview, production) doğru ayarlanması gerekiyor. Karıştırma hatası derleme anında değil çalışma anında ortaya çıkıyor — üstelik çoğu zaman migration'ın ortasında, yani en pahalı yerde.
Dört uygulamanın da Next.js 16 App Router üzerinde olması bilinçli: aynı yazma deseni (Server Action → Zod → transaction) dört repoda tekrar ettiği için bir repodan diğerine geçerken zihinsel model değişmiyor. Neon, dağıtımla aynı serverless modelde kalırken WebSocket sürücüsüyle gerçek transaction verebiliyor — FIFO mahsuplaşmadaki SELECT ... FOR UPDATE ve çok ifadeli yazmalar bunu zorunlu kılıyordu. Drizzle ORM SQL'e yakın duruyor: partial unique index, enum ve FK davranışı migration dosyasında açıkça görünüyor; veri bütünlüğünün merkezde olduğu bir sistemde kısıtların ORM soyutlamasının arkasında kalmaması önemliydi. decimal.js para hesabında float yuvarlamasını baştan dışarıda tutuyor. Zod tek şema olarak hem formda hem Server Action girdisinde çalışıyor. Yönetim tarafında Serwist, uygulamayı telefona kurulabilir hale getirip son görülen veriyi önbellekte tutuyor. ETL'in Python olmasının tek sebebi Paradox: çalışan tek okuyucu orada.
İki ayrı auth yığını bilerek yan yana duruyor. Analiz ve stok tarafında Auth.js credentials + bcrypt, JWT session; yetki üç noktada denetleniyor — proxy katmanında optimistik rota koruması, layout seviyesinde tam rol doğrulaması ve Server Action içinde sahiplik kontrolü, böylece giriş personeli yalnızca kendi kayıtlarını düzenleyip silebiliyor. Muhasebe ve yönetim tarafında kendi yazdığım auth: argon2id (OWASP 2024 parametreleri), muhasebede dört rol (yönetici / muhasebe / operatör / görüntüleyici), yönetimde uzun ömürlü oturum ve altı haneli PIN. Audit log'da kimlik numarası, IBAN ve sigorta numarası gibi alanlar ham yazılmıyor, yalnızca "değişti" bilgisi tutuluyor; diğer tüm INSERT, UPDATE ve DELETE eski ve yeni değeriyle JSONB olarak loglanıyor ve eski kayıtlar batch'li bir komutla temizleniyor. Rate limiting girişte ve tüm yazma action'larında açık, silinmiş kullanıcı giriş yapamıyor, dinamik rota parametreleri UUID olarak doğrulanıyor. Dördü de kapalı sistem: robots.txt'te tümüne kapalı ve yanıt başlığında noindex. Yönetimden çıkan her sorgu ayrıca sorgu kalkanından geçiyor.
Bu dört uygulamanın hiçbiri halka açık değil, dolayısıyla saklanmış bir Lighthouse ölçümü yok ve olmayan bir skoru yazmıyorum; buradaki ölçü sayfa hızı değil sorgu davranışı. Analiz tarafında günlük aggregate tablosu yazma anında güncelleniyor, böylece bölge ve üretici analizleri geçmişin tamamını taramıyor; yavaş sorgular ayarlanabilir bir eşik üzerinden loglanıyor ve admin sorgularına EXPLAIN ANALYZE profili almak için repoda hazır bir komut var. Muhasebe tarafında materialized view kurulmadı ama karar açık bırakıldı: tarihsel verinin tamamı canlıya taşındıktan sonra p95 iki saniyeyi geçerse yeniden değerlendirilecek. Yönetim uygulaması yalnızca telefon için tasarlandı — 390px tasarım genişliği, breakpoint yok, 17px gövde ve 34–40px KPI rakamı, en az 48×48px dokunma hedefi; yakınlaştırma bilerek engellenmedi. Son görülen veri service worker önbellekte tutuluyor ve ekranda "kaç dakika önce güncellendi" damgası gösteriliyor.
Salt-okunurluk bugün uygulama katmanında bir sorgu kalkanı olarak duruyor, oysa doğru yeri veritabanı: Neon'da ayrı bir read-only rol açılsaydı garanti uygulamanın dışında olurdu ve kalkanı atlayan yeni bir sorgu yolu onu delemezdi. Aynı dersi bu sistemin içinde bir kez zaten almıştım — mükerrer numune kontrolünü form doğrulamasından çıkarıp partial unique index'e taşıdığımda kural artık hangi yoldan gelinirse gelinsin geçerliydi. Görev Kahramanı'nda da aynı desen eşzamanlılık tarafında çıkmıştı: ödül yazımını uygulama katmanında karşılaştırmalı güncellemeyle korumak, her yeni yazma yolunun aynı korumayı yeniden kurmasını gerektiriyor. Tekrarlayan sonuç şu: garanti uygulamada durduğu sürece her yeni yol onu yeniden doğrulamak zorunda, ve er geç biri unutuyor. İkinci nokta test: dört repodan yalnızca birinde otomatik test var. Para hesabı — stopaj, fiyat, sapma — saf fonksiyon ve test edilmesi en kolay yer; yönetimde oradan başladım ama muhasebede aynısını yapmadım. Bugün sırayı ters çevirir, testi en çok paranın döndüğü repoda yazardım.
Ne yapmak istediğinizi anlatın; ne kadar süreceğini ve nereden başlanacağını baştan söylerim.