İlk sohbet uygulamamı yazdığımda backend diye bir şey yazmamıştım. Firebase'e bir referans açtım, `onValue` dinledim, mesaj `push` ettim ve iki tarayıcıda aynı anda mesajlaşmayı gördüm. O an gerçekten büyülü geldi. Yıllar sonra Karalama'yı yazarken aynı problemi kendi sunucumla çözdüm ve o günkü büyünün neyi sakladığını anladım.
Firebase Neyi Doğru Yapıyor
Realtime Database'in modeli tek cümleyle şu: veritabanı bir JSON ağacı, sen bir düğümü dinliyorsun, o düğüm değiştiğinde sana haber geliyor.
import { getDatabase, ref, push, onChildAdded, serverTimestamp } from 'firebase/database'
const db = getDatabase(app)
const messagesRef = ref(db, `rooms/${roomId}/messages`)
// Gönderme
await push(messagesRef, {
text,
uid: auth.currentUser!.uid,
// İstemcinin saatine güvenmiyoruz — sunucu damgası
createdAt: serverTimestamp(),
})
// Dinleme: onValue değil onChildAdded.
// onValue her değişimde TÜM listeyi yeniden gönderir; 500 mesajlık bir
// odada her yeni mesaj 500 kayıt indirmek demektir.
onChildAdded(messagesRef, (snap) => {
setMessages((prev) => [...prev, { id: snap.key!, ...snap.val() }])
})O `onValue` / `onChildAdded` ayrımı ilk sürümde yaptığım hataydı. Uygulama çalışıyordu ama sohbet uzadıkça telefonda ısınma başlıyordu. Ağ sekmesine bakınca sebep açıktı: her mesajda bütün geçmiş yeniden iniyordu.
İpucu
Sorguya sınır koymak da şart: `query(messagesRef, limitToLast(50))`. Odaya ilk giren biri, kurulduğu günden beri yazılmış her mesajı indirmemeli. Bunu eklemeden önce bir odanın ilk yüklemesi 2 MB'a çıkmıştı.
Asıl Mesele: Güvenlik Kuralları
Firebase'in "backend yazmıyorsun" vaadinin bedeli burada ödeniyor. Backend yazmıyorsun ama yetkilendirmeyi yazmak zorundasın — ve bunu tanıdık bir dilde değil, kendi kural dilinde yazıyorsun.
{
"rules": {
"rooms": {
"$roomId": {
"messages": {
".read": "auth != null",
"$msgId": {
// Yalnızca kendi adına yazabilirsin
".write": "auth != null && !data.exists() && newData.child('uid').val() === auth.uid",
".validate": "newData.hasChildren(['text','uid','createdAt']) && newData.child('text').isString() && newData.child('text').val().length <= 500"
}
}
}
}
}
}`!data.exists()` kısmı önemli: mesajın sonradan düzenlenmesini engelliyor. `.validate` ise şema doğrulaması — bunu yazmadan uygulamamı yayına aldığımda biri konsoldan 3 MB'lık bir string gönderebilirdi.
Firebase'de "sunucu kodu yok" demek "sunucu mantığı yok" demek değil. Mantık var, sadece JSON içine yazılmış bir kural dilinde yaşıyor ve testi zor.
Kural dosyasının en can sıkıcı yanı hata ayıklaması. Bir yazma reddedildiğinde istemci sadece "permission denied" görüyor; hangi kuralın hangi satırda reddettiğini konsoldan anlamıyorsunuz. Firebase'in kural simülatörü var ama gerçek veriyle çalışmıyor.
Nerede Duvara Çarptım
Sohbet için Firebase gayet iyiydi. Duvara, sohbetin üstüne oyun mantığı koymaya çalıştığımda çarptım.
Basit bir soru: bir tahmin doğru mu? Cevabı bilen taraf kim? Firebase modelinde veritabanı bir depo, karar verici değil. Doğru cevap veritabanında yazıyorsa istemci onu okuyabilir; okuyamasın diye gizlerseniz karşılaştırmayı kim yapacak?
| Soru | Firebase RTDB | Kendi sunucun |
|---|---|---|
| Kim karar verir | Kural dili + istemci | Sunucu kodu |
| Gizli durum tutulabilir mi | Zor — okuma izni ya var ya yok | Doğal, istemci hiç görmez |
| Zamanlayıcı (tur süresi) | Cloud Functions gerekir | setTimeout, aynı süreçte |
| Ölçekleme | Kendiliğinden | Sen ilgilenirsin |
| İlk çalışan sürüm | Bir akşam | Birkaç gün |
Firebase'in cevabı Cloud Functions. Yani sonuçta backend yazıyorsunuz — ama parçalanmış hâlde, farklı bir çalışma ortamında ve soğuk başlama gecikmesiyle. Bir tur zamanlayıcısını Cloud Functions ile kurmayı denedim; iş çalışıyordu ama tur bitişi bazen üç saniye gecikiyordu.
Aynı Problem, İkinci Deneme
Karalama'da aynı soruya farklı cevap verdim: odalar sunucunun belleğinde bir `Map` içinde, kararları sunucu veriyor.
handleGuess(playerId: string, text: string) {
// Çizen kişi tahmin edemez, zaten bilenler tekrar puan alamaz
if (playerId === this.drawerId) return null
if (this.guessedPlayerIds.has(playerId)) return null
const normalized = normalizeGuess(text)
const answer = normalizeGuess(this.currentWord) // ← istemciye hiç gitmedi
if (normalized === answer) {
this.guessedPlayerIds.add(playerId)
const score = calculateGuesserScore({ /* ... */ })
// ...
}
}Buradaki `this.currentWord` istemciye hiçbir zaman gönderilmiyor. Firebase'de bunu yapmanın yolu kelimeyi okuma izni olmayan bir düğümde tutup karşılaştırmayı bir Cloud Function'a yaptırmak olurdu — yani her tahminde bir fonksiyon çağrısı.
Bugün Hangisini Seçerdim
Cevap tek soruya bakıyor: sunucunun istemcinin bilmediği bir şeyi bilmesi gerekiyor mu?
- 1
Gerekmiyorsa Firebase (ya da Supabase Realtime)
Sohbet, bildirim, canlı beğeni sayacı, işbirlikli liste. Herkes her şeyi görebilir, kurallar sadece kimin yazabileceğini sınırlar. Bu senaryoda kendi sunucunu yazmak boşa emek.
- 2
Gerekiyorsa kendi sunucun
Oyun mantığı, gizli durum, sunucu tarafı zamanlayıcı, hile karşıtı kontrol. Karar veren tarafın kodu senin elinde olmalı.
- 3
Arada kalmışsan
Postgres + SSE ya da WebSocket ile başla. Firebase'in kural dilini öğrenmek, basit bir sunucu yazmaktan daha uzun sürüyor — ve öğrendiğin şey taşınabilir değil.
Son maddeyi biraz açayım. Firebase kural dili öğrendiğim ve bir daha hiç kullanmadığım bir bilgi oldu. Socket.io ile öğrendiğim şeyler — olay tabanlı mimari, oda kavramı, sunucu otoritesi, yeniden bağlanma — her gerçek zamanlı sistemde geçerli.
Firebase'i Hâlâ Sevdiğim Yer
Bu yazı Firebase eleştirisi gibi okunuyorsa dengeleyeyim: bugün bir hafta sonu projesi yazacak olsam ve gerçek zamanlı bir sohbete ihtiyacım olsa, yine Firebase ile başlardım.
- Bağlantı yönetimini tamamen unutuyorsunuz. Ağ koptu, geri geldi, uygulama arka plandaydı — hepsi hallediliyor.
- Çevrimdışı desteği bedava geliyor. Kullanıcı tünelde mesaj yazıyor, sinyal gelince gönderiliyor.
- `serverTimestamp()` küçük ama önemli: istemci saatleri güvenilmez ve sıralamayı ona bağlarsanız mesajlar karışıyor.
- Kimlik doğrulama aynı ekosistemde. Anonim oturum tek satır ve `auth.uid` doğrudan kurallarda kullanılabiliyor.
Dikkat
Anonim oturum açmayı unutmayın. Kurallarda `auth != null` yazıp uygulamada `signInAnonymously()` çağırmazsanız her yazma reddedilir ve hata mesajı bunu söylemez. İlk sürümde tam bir akşamımı bu aldı.
Geriye Dönüp Bakınca
Bu iki projeden çıkardığım şey teknoloji tercihi değil, bir soru: "bu sistemde kim otorite?" Cevabı baştan verirseniz, geri kalan kararlar kendiliğinden geliyor.
O ilk sohbet uygulamasında bu soruyu hiç sormamıştım. Çalıştı, çünkü sohbette otoriteye ihtiyaç yok. Oyuna geçince aynı yaklaşım kırıldı ve neden kırıldığını anlamak bana bir sürü şey öğretti.