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

Ustura

Randevu Prototipi

Ustura randevu prototipi şeması Ustura — randevu prototipi Çakışma kuralı uygulamada değil, veritabanında Müşteri projesi değil; kuralın nereye konduğunu denemek için yazılmış bir prototip. Müşteri — web uygun saati seçer Europe/Istanbul görür Personel — panel manuel randevu, blok, izin aynı anda yazabilir Server Action + Zod girdi doğrulama · UTC'ye çevirme Postgres EXCLUDE USING gist staff_id WITH = tstzrange(starts_at, ends_at) WITH && WHERE status = 'confirmed' Çakışan ikinci yazma reddedilir hangi yoldan gelirse gelsin Kuralın yeri neden veritabanı Uygulama katmanındaki "önce oku, boşsa yaz" kontrolü iki eşzamanlı istek arasında yarış bırakır. Kısıt veritabanında durduğunda web formu, personel paneli, seed script'i ve elle atılan SQL aynı kurala tabidir. Zaman: UTC sakla, yerelde göster timestamptz + tstzrange görüntüleme Europe/Istanbul Env: Zod ile derleme anında zorunlu değişken eksikse build düşer opsiyonel entegrasyon sessizce kapalı Bildirim: e-posta ve web push anahtar yoksa özellik kapanır uygulama yine ayağa kalkar
Çakışma kuralı uygulamada değil, veritabanındadır. Hem müşteri web formu hem personel paneli aynı Server Action + Zod yolundan geçer, girdi UTC'ye çevrilir ve Postgres'e yazılır. Postgres'teki EXCLUDE USING gist kısıtı, aynı personel için çakışan ikinci onaylı randevuyu hangi yoldan gelirse gelsin reddeder — seed script'i ve elle atılan SQL dahil. Zaman timestamptz olarak saklanır, Europe/Istanbul olarak gösterilir.

Bir berber için kurduğum randevu sistemi prototipi. Müşteri projesi değil; aynı saatin iki müşteriye verilmesini çalışanın dikkatine değil, veritabanının kendisine engelletmeyi denediğim bir çalışma.

Kullanılan Teknolojiler

  • Next.js 16
  • TypeScript
  • Drizzle ORM
  • Neon Postgres
  • Auth.js
  • Zod
  • date-fns-tz
  • Web Push (VAPID)

Öne Çıkan Özellikler

  • Randevu çakışması veritabanı kısıtıyla imkânsız — uygulama katmanında kontrol yok
  • Zaman UTC saklanıyor, Europe/Istanbul gösteriliyor
  • Uygunluk hesabı saf fonksiyon — veritabanına dokunmadan test ediliyor
  • Ortam değişkenleri Zod ile derleme anında doğrulanıyor
  • Personel, hizmet, çalışma saati, mola ve blok tanımları panelden yönetiliyor
  • Müşteri randevusunu tek kullanımlık bağlantıyla iptal edebiliyor

Geliştirme Süresi

1 ay

Problem

Randevu sistemleri tek bir noktada bozulur: iki kişi aynı personelin aynı saatini aynı anda alır. Bu, senaryo yazarak keşfedilecek bir hata değil; iki isteğin araya girmesiyle oluşuyor ve tek kullanıcıyla test edildiğinde asla ortaya çıkmıyor. Yaygın çözüm "önce oku, boşsa yaz" — ve bu çözüm yanlış, çünkü okuma ile yazma arasında kalan sürede başka bir istek aynı yeri alabiliyor. İkinci mesele zaman: bir randevu sistemi yaz saati geçişini, kullanıcının cihaz saat dilimini ve sunucunun saat dilimini aynı anda doğru ele almak zorunda. Bu proje bir müşteri işi değil; iki sorunun da nereye konulması gerektiğini denemek için yazdığım bir prototip.

Kullanıcılar

İki taraf. Müşteri: telefondan giriyor, hizmeti seçiyor, uygun saati görüyor ve üyelik açmadan randevu bırakıyor; sonradan iptal etmek isterse elinde bir bağlantı olması gerekiyor. İşletme: personel, hizmet süresi, çalışma saatleri, mola ve kapalı gün tanımlarını panelden yönetiyor; günü takvim görünümünde takip ediyor ve elle randevu ekleyebiliyor.

Rolüm

Tek geliştirici, prototipin tamamı. Veri modeli ve kısıtlar, uygunluk hesabı, rezervasyon ve iptal akışları, yönetim paneli, bildirim katmanı.

Mimari

Next.js 16 App Router, Neon Postgres ve Drizzle. Yazma yolu Server Action → Zod → transaction. Sistemin merkezindeki karar veri modelinde: `appointments` tablosunda personel kimliği ve randevu aralığı üzerinde bir exclusion constraint var; kısıt yalnız onaylı randevular için çalışıyor, böylece iptal edilmiş bir kayıt yeri tutmuyor. Uygunluk hesabı ise veritabanından tamamen ayrı: çalışma saati, mola, personel randevuları, işletme blokları, slot adımı, minimum ihbar süresi ve rezervasyon penceresi girdi olarak veriliyor, çıktı uygun başlangıç saatlerinin listesi. Fonksiyon saf olduğu için testleri veritabanı ayağa kaldırmadan çalışıyor. Zaman tarafında kural tek: veritabanında her şey zaman dilimli damga olarak UTC'de duruyor, arayüzde Europe/Istanbul'a çevriliyor; günün sınırları da yerel güne göre hesaplanıyor, sunucunun saat dilimine göre değil.

Kritik Kararlar

  1. Çakışma engeli veritabanında, uygulamada değil

    Kural bir exclusion constraint olarak tanımlı: aynı personel için kesişen iki zaman aralığı aynı anda var olamıyor. Kontrol yazma anında veritabanı tarafından yapıldığı için okuma ile yazma arasındaki yarış penceresi ortadan kalkıyor. Bunun asıl kazancı yalnız eşzamanlılık değil kapsam: web formu, panelden elle eklenen randevu, seed script'i ve elle atılan bir SQL — hepsi aynı kurala tabi. Uygulama katmanındaki bir kontrol yalnız kendi yolunu korur.

    ÖdünleşimHata artık bir doğrulama mesajı olarak değil, veritabanı kısıt ihlali olarak geliyor; çağıran tarafın bu ihlali yakalayıp kullanıcıya anlamlı bir cümleye çevirmesi gerekiyor. Kısıt ayrıca aralık türü üzerinde bir indeks uzantısı istiyor ve kuralın kendisi migration dosyasında duruyor — kodu okuyan biri kuralı uygulama katmanında aradığında bulamıyor.

  2. Uygunluk hesabı saf bir fonksiyon

    Uygunluk, sistemin en çok kenar durumu olan yeri: kapalı gün, mola, personel bazlı doluluk, geçmiş saat, minimum ihbar süresi ve rezervasyon penceresinin sonu. Bunları veritabanı sorgusunun içine gömmek, her senaryoyu ancak gerçek veriyle test edilebilir hâle getirirdi. Hesap girdi/çıktı olarak dışarı alındı; testler tek bir çağrıyla senaryo kuruyor.

    ÖdünleşimVeritabanından okunan aralıkların fonksiyona doğru biçimde taşınması çağıran tarafın sorumluluğu; fonksiyon kendisine eksik veri verildiğini fark edemez, yalnız verilenle hesap yapar. Yani hesabın doğruluğu test edilmiş olsa da, hesabın doğru girdiyle çağrıldığı ayrıca doğrulanmak zorunda.

  3. Saat dilimi kararı tek cümleye indirildi

    UTC sakla, Europe/Istanbul göster. Randevu aralıkları zaman dilimli damga olarak yazılıyor, gün sınırları yerel güne göre hesaplanıyor ve arayüzde gösterim sabit bir dilim üzerinden yapılıyor. Kuralın tek cümle olması, her yeni ekranın kendi çevrimini icat etmesini engelliyor.

    ÖdünleşimSabit bir görüntüleme dilimi, işletme başka bir dilimde çalışmaya başlarsa yetmiyor; dilim bugün bir sabit ve işletme ayarı değil. Testlerde de saat farkının açıkça verilmesi gerekiyor, aksi halde test çalıştığı makinenin dilimine bağımlı hâle geliyor.

  4. Ortam değişkenleri derleme anında doğrulanıyor

    Yapılandırma şeması yapılandırma dosyasından import edildiği için hatalı bir ortam çalışma anında değil, derleme veya başlangıç anında patlıyor. Zorunlu değişkenler yalnız uygulamanın ayağa kalkması için şart olanlar; e-posta, hatırlatma ve bildirim gibi özellikler opsiyonel ve anahtarları girilene kadar kapalı kalıyor. Böylece gizli anahtar olmadan da üretim derlemesi alınabiliyor.

    ÖdünleşimOpsiyonel bir anahtarın eksikliği sessizce özelliği kapatıyor; yanlış girilen bir anahtar ile hiç girilmemiş anahtar dışarıdan aynı görünüyor. Ayrıca kaçış yolu olarak bir doğrulama atlama anahtarı var ve bu anahtar üretimde açık bırakılırsa şemanın sağladığı garanti ortadan kalkıyor.

Teknoloji Seçimi

Postgres bu projede bir tercih değil şart: exclusion constraint ve aralık türleri bu kararın merkezinde ve başka bir veritabanında aynı garanti aynı ucuzlukta kurulamıyor. Drizzle, kısıtı migration dosyasında düz SQL olarak yazmaya izin verdiği için seçildi — kural ORM soyutlamasının arkasında kalmıyor, okunabiliyor. date-fns-tz, yerel gün ve zaman dilimli damga arasındaki çevrimi tek yerde topluyor. Auth.js yalnız yönetim tarafında; müşteri akışında hesap yok, iptal tek kullanımlık bir bağlantı ile yapılıyor.

Güvenlik

Yönetim paneli oturum korumalı; müşteri tarafında hesap yok, dolayısıyla saklanan kimlik verisi de minimum. Randevu iptali, tahmin edilemeyen tek kullanımlık bir belirteç taşıyan bağlantı üzerinden yapılıyor — randevu kimliği adres çubuğunda görünmüyor. Onay metni kaydın üzerinde saklanıyor: kimin, ne zaman, hangi metni onayladığı sonradan tartışmaya açık kalmıyor ve bu ayrı bir testle korunuyor. Yazma yolları tek transaction içinde çalışıyor; eşzamanlı rezervasyon denemesi için yazılmış bir test, ikinci isteğin veritabanı kısıtına takıldığını doğruluyor.

Performans

Bu bir prototip ve yayınlanmış bir kurulumu yok; tarih damgalı bir Lighthouse ölçümü de bulunmadığı için buraya sayfa hızı skoru yazılmadı. Ölçülebilir olan taraf uygunluk hesabı: veritabanına yalnız o günün randevuları ve blokları için tek sorgu atılıyor, slot üretimi bellekte yapılıyor. Bu sayede takvimde gün değiştirmek yeni bir tur sorgu üretmiyor.

Sonuçlar

Aktif geliştirme
2026-07 · 13 commit
TypeScript kaynak dosyası
133
Test dosyası
8 — uygunluk, tarih, telefon, yerel saat, eşzamanlı rezervasyon, iptal, erteleme, onay
Şema
9 tablo · 4 migration
Rota
16 sayfa — site ve yönetim paneli
Mimari garanti
Aynı personel için kesişen iki onaylı randevu veritabanı seviyesinde imkânsız
Mimari garanti
Zorunlu ortam değişkeni eksikse üretim derlemesi başarısız olur

Bugün Ne Farklı Yapardım

Bu prototipin asıl çıktısı kod değil, bir alışkanlık: bir kuralın uygulama katmanında mı yoksa veritabanında mı durması gerektiğini sormak. Buradaki cevabı sonraki işlere taşıdım — Bergaz Gıda'da ödeme callback'inin idempotency'sini benzersizlik kısıtına, Bergaz Operasyon'da mükerrer numune kontrolünü kısmi benzersiz indekse indirdim. Ters yönde de bir kanıt var: Taxilos'ta yetki kapısı fonksiyonların içine dağıtıldığı için tek bir predicate'in üç değerli mantığa düşmesi 45 kapıyı birden açtı. Bugün farklı yapacağım şey ise durum tarafı: randevu bir durum makinesi taşıyor ve izinli geçişler hâlâ kodda dağınık; onları da tek bir tanıma indirir, çakışma kuralında yaptığımın aynısını yapardım.

Prototip

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.