İçeriğe geç
Teknik YazıRealtime

İlk Sohbet Uygulamam: Gerçek Zamanlıda Otorite Kimde?

İlk gerçek zamanlı uygulamamı Firebase ile yazdım, yıllar sonra aynı problemi kendi sunucumla çözdüm. İkisi arasındaki farkı en net gösteren şey, kimin karar verdiği sorusu.

5 Ocak 20266 dk okuma

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

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

database.rules.json
{
  "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?

SoruFirebase RTDBKendi sunucun
Kim karar verirKural dili + istemciSunucu kodu
Gizli durum tutulabilir miZor — okuma izni ya var ya yokDoğal, istemci hiç görmez
Zamanlayıcı (tur süresi)Cloud Functions gerekirsetTimeout, aynı süreçte
ÖlçeklemeKendiliğindenSen ilgilenirsin
İlk çalışan sürümBir akşamBirkaç 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.

apps/server/src/game/Room.ts
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. 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. 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. 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.