
Operasyon Platformu
Almanya'nın Schöppingen kasabasındaki bir taksi işletmesi için üç bileşenli platform: telefonu karşılayan yapay zekâ çağrı merkezi, rezervasyon ve operasyonu yürüten web uygulaması ve planlama aşamasındaki mobil uygulama. Üç bileşenin olgunluğu farklı; ortak olan tek rezervasyon kontratı ve tek veritabanı.
Tarife değerleri ve telefon numarası bu vakada bilerek yer almıyor: repodaki karşılıkları henüz onaylanmamış placeholder değerlerdir ve gerçek sanılmaları hâlinde yanlış bilgi yayılmış olur.
İşletmenin telefonu, gerçek talep hattı. Çağrının kaçırılması yalnız bir iş kaybı değil; kaçırılan çağrının kaydı da olmadığı için ertesi gün kimin aradığı bilinmiyor. Buna karşılık bir taksi çağrı merkezinde zor olan kısım konuşmayı anlamak değil: konuşmanın sonunda ortaya çıkan rezervasyonun, web'den açılan rezervasyonla aynı kayıt olması. İki sistem iki ayrı veritabanına yazarsa senkronizasyon problemi doğar ve o problemin çözülmediği yer her zaman aynıdır — aynı saate iki araç gönderilir. İkinci mesele hukuki: Almanya'da yayınlanan bir site, var olmayan bir özelliği vaat ederse (mobil uygulama, canlı araç takibi) bu bir haksız rekabet kalemi. Bu tür bir yanlış, kod hatası gibi patlamıyor — build geçiyor, testler yeşil, yalnızca sayfa yalan söylüyor.
Dört taraf, dört farklı kısıt. Telefondaki müşteri: yaşlı olabiliyor, uygulama indirmiyor, adresi konuşarak veriyor ve karşısındakinin Almanca'yı doğal konuşmasını bekliyor. Web'deki müşteri: kendi hesabından geçmiş sürüşünü tekrar edebilmek, adres defterini yönetebilmek istiyor. Ofis: rezervasyon, şoför, araç, vardiya, seri sürüş, kurumsal faturalandırma, hasta taşıma evrakı ve atölye kaydını tek panelde görüyor. Şoför: telefondan bakıyor, yalnızca kendi vardiyasını ve kendi sürüşlerini görmeli — panelin geri kalanına erişmemeli.
Tek geliştirici, üç repo. Sistem sınırlarının çizilmesi, iki bileşenin ortak rezervasyon kontratı, Postgres şeması ve yetki modeli, çağrı merkezi tarafının işletmeye uyarlanması (araç tipleri, hizmet türleri, tarife okuma, rezervasyon yazma), web uygulamasının üç yüzeyi ve dağıtım.
Üç bileşen ayrı yayınlanıyor, ortak olan iki şey var: `booking.v1` JSON Schema kontratı ve tek Supabase Postgres şeması. Çağrı merkezi Python 3.13 üzerinde FastAPI/Granian ile çalışıyor; telefon hattı, konuşma tanıma ve sentez, model çağrısı ve çağrı durumu Azure tarafında (Communication Services, Speech, OpenAI, Cosmos DB, AI Search, Container Apps). Bot konuşma sırasında topladığı bilgiyi bir "claim" nesnesinde biriktiriyor; rezervasyonu kapatırken bu claim'i `booking.v1` gövdesine çevirip Supabase'in `create_booking` RPC'sine yazıyor. Web tarafı Next.js 16 App Router; yazma yolu Server Action → Zod → RPC, okuma yolu Server Component → sorgu katmanı. Yetki üç kademede duruyor: `proxy.ts` içinde rota koruması, RLS politikaları ve SECURITY DEFINER fonksiyonların içindeki rol kapıları. Uygulama üç yüzeye bölünmüş — kamuya açık site ve müşteri hesabı, yönetim paneli, şoför portalı — ama tek veritabanı ve tek rol modeli üzerinde çalışıyorlar. Tarife hiçbir bileşende sabit değil: `get_active_tariff` RPC'sinden okunuyor, bot bunu on beş dakikalık bir önbellekte tutuyor ve okuma başarısız olursa son geçerli değere düşüyor.
Alternatif, çağrı merkezinin kendi kaydını tutup web'e bir kuyruk veya webhook üzerinden aktarmasıydı. O tasarımda rezervasyon iki yerde var oluyor ve "hangisi doğru" sorusunun cevabı bir uzlaşma kuralına kalıyor. Bunun yerine çağrı merkezi doğrudan aynı şemaya, web'in de kullandığı RPC üzerinden yazıyor. Telefonla açılan rezervasyon ile siteden açılan rezervasyon aynı satır; ofis panelinde ikisini ayıran tek şey kaynak alanı.
ÖdünleşimPython tarafı artık veritabanı şemasına bağımlı: RPC imzası değişirse çağrı merkezi de değişmek zorunda ve bu bağımlılık derleyici tarafından yakalanmıyor, çalışma anında hata olarak çıkıyor. Ayrıca çağrı merkezi servis rolü anahtarı taşıyor — yani RLS'i baypas eden bir anahtar, kendi altyapısında duruyor. Kontrat dosyası bu riski azaltıyor ama ortadan kaldırmıyor.
Sesli bir akışta "onayla" adımı tek seferlik değil: model aracı yeniden çağırabiliyor, bağlantı kopup çağrı devam edebiliyor, operatör aynı çağrıyı tekrar işleyebiliyor. Anahtar çağrı kimliğinden türetildiği için aynı çağrının ikinci yazma denemesi yeni bir rezervasyon üretmiyor.
ÖdünleşimAnahtar çağrı başına tek olduğu için aynı çağrıda bilinçli olarak ikinci bir rezervasyon açmak — müşteri "bir de dönüş için kayıt alın" dediğinde — doğrudan mümkün değil; bu durumun ayrıca ele alınması gerekiyor. Garanti tek yazma yolunu koruyor, iş kuralını değil.
Fiyat üç yerde görünüyor: botun söylediği tahmin, sitedeki fiyat sayfası ve ofisin kestiği fatura. Bu üçü ayrışırsa hata müşteriye yansıyor ve geri alınamıyor. Tarife tek tabloda tutuluyor, üç yüzey de aynı RPC'den okuyor. Bot tarafında on beş dakikalık önbellek var; okuma başarısız olduğunda son geçerli değerle devam ediyor, sessizce sıfıra düşmüyor.
ÖdünleşimYönetim panelinden yapılan bir tarife değişikliği canlı çağrıya anında yansımıyor — önbellek dolana kadar bot eski değeri söylüyor. Ayrıca "son geçerli değere düşme" davranışı, veritabanı uzun süre erişilemezse botun eskimiş bir fiyatı güvenle söylemesi anlamına geliyor; bu bilinçli bir kabul.
Var olmayan bir özelliği vaat etmek Almanya'da haksız rekabet kalemi ve bu hata hiçbir teknik alarma takılmıyor. Bugün ne mobil uygulama ne canlı konum takibi var; bunları anlatan bir cümlenin sayfaya girmesi testte düşüyor. Aynı guard bir sayım hatasını da yakalıyor: başlıkta kalem sayısı kelimeyle yazıldığı için listeye yeni bir hizmet eklendiğinde başlık sessizce yanlış oluyordu.
ÖdünleşimBeklenen değerler testin içine elle yazılmış durumda; kaynak modülden türetilseydi test tautoloji olur ve içerikle birlikte sessizce kayardı. Bunun bedeli, meşru bir içerik değişikliğinin de testi kırması: metin her değiştiğinde testin elle güncellenmesi gerekiyor.
Sesli asistanda dil desteği bir çeviri dosyası meselesi değil: her dil ayrı ses, ayrı telaffuz ipucu, ayrı yer adı sözlüğü ve ayrı test yükü demek. Hizmet bölgesi tek dilli olduğu için Türkçe ve İngilizce yolları kaldırıldı; hem bot hem site tek dilde kaldı.
ÖdünleşimAlmanca konuşmayan bir müşteri bugün telefonla hizmet alamıyor; onu insana aktarmaktan başka bir yol yok. Dil geri eklenmek istenirse yalnız metin değil, ses ve telaffuz ipuçları da yeniden kurulacak.
Çağrı merkezi tarafının Python olmasının sebebi dil tercihi değil, ekosistem: telefon hattı, akış hâlinde konuşma tanıma ve sentez, model çağrısı ve çağrı durumunun kalıcılığı aynı bulut sağlayıcının hazır parçaları olarak geliyor ve bu parçaların olgun istemcileri Python tarafında. Bölge ve dağıtım tipi seçimi de teknik değil hukuki: veri Almanya bölgesinde kalacak şekilde ve model dağıtımı bölge dışına taşmayan tipte kuruldu. Web tarafında Next.js 16 App Router, üç yüzeyi tek kod tabanında tutarken yetkiyi sunucuda bırakıyor. Supabase'in seçilme sebebi hazır arayüzü değil, RLS ve SECURITY DEFINER fonksiyonların birlikte kullanılabilmesi: yetki kuralı uygulamada değil, verinin yanında duruyor ve Python tarafı da aynı kurala tabi. Zod, form girdisi ile Server Action girdisinde tek şema olarak çalışıyor; `booking.v1` ise iki dilin ortak sözleşmesi — TypeScript tarafında tip, Python tarafında doğrulama olarak okunuyor.
Yetki üç kademeli: rota koruması, RLS politikaları ve SECURITY DEFINER fonksiyonların içindeki rol kapıları. Şema 39 tablo, 43 migration, 70 RPC ve 113 RLS politikası taşıyor. Bu katmanın en pahalı dersi ölçümle çıktı: rol okuyucu fonksiyon, rolü olmayan bir hesapta NULL döndürüyordu ve rol predicate'leri bu NULL'u yalın karşılaştırmayla sardığı için üçü de NULL üretiyordu. plpgsql'de `IF NOT (NULL)` koşulu NULL olur ve dal hiç çalışmaz — yani 45 yönetim RPC'sinin yetki kapısı yükseltme yapmadan geçiliyordu. Erişilebilirlik tarafı da teoride değildi: rolü boş kalan tek kullanıcı sınıfı, onboarding'i tamamlamamış bir sağlayıcı hesabıydı. Predicate'ler NULL-güvenli hâle getirildi; RLS davranışı değişmedi çünkü USING ve WITH CHECK yalnız TRUE'yu geçirir. İkinci bulgu şoför portalındaydı: şoför kaydını oturum kullanıcısına bağlayan alan repoda hiçbir yerde yazılmıyordu — portal canlıda boş çalışırdı. Servis rolü anahtarı yalnız sunucu tarafında okunuyor; dağıtım hattında bu anahtarın varlığı ayrıca doğrulanıyor ve persona metninin sürüm kontrolü dışında kalmasına karşı bir sapma kontrolü çalışıyor.
Bu platformun yönetim ve şoför yüzeyleri kapalı; saklanmış, tarih damgalı bir Lighthouse çıktısı bulunmadığı için buraya sayfa hızı skoru yazılmadı. Ölçülebilir olan taraf çağrı yolu: sesli bir akışta gecikme doğrudan konuşmanın içinde duyuluyor, bu yüzden fiyat ve uygunluk sorgusu gibi konuşma ortasında çalışan adımlar önbelleğe alındı — tarife on beş dakika, mesafe hesabı çağrı boyunca. Modül seviyesindeki önbellek kilitli ve çift kontrollü; aynı anda gelen iki çağrı tek sorgudan fazlasını üretmiyor. Web tarafında fiyat sayfası ISR ile saatlik tazeleniyor, yönetim ve şoför yüzeyleri tamamen dinamik.
Yetki kapısını bugün olduğu gibi plpgsql fonksiyonlarının içine dağıtmazdım. Kural 45 ayrı yerde tekrarlandığı için, tek bir predicate'in üç değerli mantığa düşmesi kapıların tamamını aynı anda açtı; kural tek bir yerde ve NULL üretemeyecek şekilde tanımlansaydı bu sınıf hata mümkün olmazdı. Aynı ders portfolyodaki iki projede daha çıktı: Bergaz Operasyon'da salt-okunurluk garantisi uygulama içindeki bir sorgu kalkanında duruyor ve kalkandan geçmeyen her yeni sorgu yolu onu deliyor; Bergaz Gıda'da ödeme idempotency'sini önce uygulama katmanında kurup sonra veritabanı kısıtına taşımak zorunda kaldım. Tekrarlayan sonuç şu: garanti veriye ne kadar yakınsa, onu atlamak o kadar zor. İkinci nokta ölçüm: iki planlanmamış bulgunun ikisi de görev listesinde yoktu, canlı sistemi sorgulayınca çıktı. Bir yetki kapısının çalıştığını varsaymak yerine "yetkisiz çağrı ne dönüyor" diye sormak, bu projede yazdığım hiçbir testin yakalamadığı iki açığı buldu. Üçüncüsü mobil: üç bileşenli bir platformu tek başına yürütürken üçüncü bileşen her zaman sıranın sonuna düşüyor. Bugün mobil için baştan daha dar bir kapsam yazardım — tam uygulama yerine yalnız şoför tarafı — çünkü dar kapsam sıraya girebiliyor, geniş kapsam giremiyor.
Ne yapmak istediğinizi anlatın; ne kadar süreceğini ve nereden başlanacağını baştan söylerim.