Ana içeriğe geç
Projelere Dön
Taxilos logo

Taxilos

Operasyon Platformu

Kısaca

Önce
Taksi işletmesinin telefonu asıl talep hattıydı; kaçırılan bir çağrının kaydı olmadığı için ertesi gün kimin aradığı bilinmiyordu.
Sonra
Telefonu Almanca konuşan bir yapay zekâ asistanı karşılayıp rezervasyonu kendisi açıyor; ofis rezervasyonu, şoförü ve aracı tek panelden yönetiyor, şoför yalnızca kendi sürüşlerini görüyor.
Ne değişti
Telefondan ve web'den gelen rezervasyon aynı kayda düşüyor, aynı çağrı iki rezervasyon açamıyor ve fiyat her yerde tek tarifeden okunuyor; mobil uygulama henüz planlama aşamasında.
Taxilos mimari şeması Taxilos Üç bileşen · tek rezervasyon kontratı · tek veritabanı Bileşenler bağımsız yayınlanır, ortak olan booking.v1 şeması ve tek Postgres şemasıdır. AI Çağrı Merkezi Python 3.13 · FastAPI / Granian Azure ACS · OpenAI · Speech · Cosmos AI Search · Container Apps kullanımda telefonu karşılar, rezervasyonu yazar Web Next.js 16 · Supabase Postgres Üç yüzey: site · yönetim · şoför portalı RLS + SECURITY DEFINER RPC yayında rezervasyon, operasyon, muhasebe Mobil Expo hedefleniyor Kapsam ve ekran akışı yazıldı Uygulama kodu henüz yok planlama faz 2 — sıraya alındı booking.v1 — platformdan bağımsız rezervasyon kontratı JSON Schema · durum ve hizmet tipi enum'ları · hata zarfı · idempotency anahtarı Python ve TypeScript aynı şemayı okur Supabase Postgres — tek şema 39 tablo · 43 migration · 70 RPC · 113 RLS politikası create_booking çağrı ve web aynı RPC'yi çağırır get_active_tariff tarife tek kaynak, bot 15 dk cache rol predicate'leri NULL-güvenli yetki kapısı Telefonla açılan rezervasyon ile web'den açılan rezervasyon aynı satırdır; ikinci bir senkronizasyon katmanı yoktur. Aynı çağrı iki kez işlenirse idempotency anahtarı ikinci kaydı engeller.
Üç bileşen bağımsız yayınlanır: kullanımdaki Python/Azure yapay zekâ çağrı merkezi, yayındaki Next.js web uygulaması ve henüz planlama aşamasındaki Expo mobil uygulaması. Üçü de platformdan bağımsız booking.v1 kontratına ve tek bir Supabase Postgres şemasına bağlanır; telefonla ve web'den açılan rezervasyon aynı satırdır, ikinci bir senkronizasyon katmanı yoktur.

Almanya'nın Schöppingen kasabasındaki bir taksi işletmesi için platform: telefonu yapay zekâ asistanı karşılıyor, rezervasyon ve operasyon web uygulamasında yürüyor, mobil uygulama planlama aşamasında. Telefondan ve web'den gelen her rezervasyon aynı sisteme düşüyor.

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.

Kullanılan Teknolojiler

  • Next.js 16
  • TypeScript
  • Supabase Postgres
  • Python 3.13
  • FastAPI
  • Azure Communication Services
  • Azure OpenAI
  • Azure Speech
  • Azure Cosmos DB
  • Zod

Öne Çıkan Özellikler

  • Telefonu karşılayan, rezervasyonu kendisi açan Almanca sesli asistan
  • Telefon ve web aynı rezervasyon kaydını yazıyor — senkronizasyon katmanı yok
  • Platformdan bağımsız booking.v1 kontratı: Python ve TypeScript aynı şemayı okuyor
  • Üç ayrı yüzey — tanıtım sitesi ve müşteri hesabı, yönetim paneli, şoför portalı
  • Tarife tek kaynaktan okunuyor; bot kendi sabitini taşımıyor
  • İçerik iddiası guard'ları — sitenin var olmayan bir özelliği vaat etmesi teste takılıyor

Geliştirme Süresi

5 ay (sürüyor)

Problem

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

Kullanıcılar

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.

Rolüm

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.

Mimari

Üç 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.

Kritik Kararlar

  1. İki bileşen tek veritabanına yazıyor — entegrasyon katmanı yok

    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.

  2. Rezervasyon yazımında idempotency anahtarı çağrının kimliğinden üretiliyor

    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.

  3. Tarife ve fiyat bilgisi kodda değil, veritabanında

    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.

  4. Site metinlerindeki iddialar testle korunuyor

    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.

  5. Tek dil — Almanca. Çok dilli destek kaldırıldı

    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.

Teknoloji Seçimi

Ç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.

Güvenlik

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.

Performans

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.

Sonuçlar

Aktif geliştirme
2026-04 → 2026-09 · web 160 commit · çağrı merkezi 45 commit
TypeScript kaynak dosyası
451 (web)
Python kaynak dosyası
70 (çağrı merkezi)
Test dosyası
77 (web)
Şema
39 tablo · 43 migration · 70 RPC · 113 RLS politikası
Rota
79 sayfa — site ve müşteri hesabı, yönetim paneli, şoför portalı
Bileşen olgunluğu
Çağrı merkezi kullanımda · web yayında · mobil planlama aşamasında, kodu henüz yazılmadı
Mimari garanti
Telefon ve web aynı RPC'yi çağırıyor; idempotency anahtarı aynı çağrının ikinci kez rezervasyon üretmesini engelliyor
Mimari garanti
Yetki predicate'leri NULL-güvenli — rolü boş bir hesap yönetim RPC'lerini geçemiyor

Bugün Ne Farklı Yapardım

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.

İç sistem

Nereden başlayacağınızı birlikte bulalım

Ne yapmak istediğinizi anlatın; ne kadar süreceğini ve en mantıklı ilk adımı baştan söylerim.