Skip to content
projelere açık000%
Tüm yazılar
ENTRY-001Altyapı7 Ağustos 2026 · 7 dk okuma

SER-03 · SER-03.01 · SaaS Sistemleri: Mimari, Maliyet ve Sadelik

Bir SaaS’ın Supabase faturası neden kontrolden çıkar?

Supabase’i suçlamak kolay. Ama fatura semptomdu: asıl mesele uygulamanın backend ile neden, ne zaman ve ne kadar konuştuğuydu.

Supabase’i suçlamak kolay.

Bir ay sonunda beklediğinizden yüksek bir fatura geldiğinde ilk refleks genellikle kullandığınız servisin pahalı olduğunu düşünmek oluyor. Benim için de ilk tepki buna yakındı.

Ordovia büyürken Supabase tarafındaki kullanım yaklaşık 2,6 milyon aylık isteğe kadar çıktı. Sonrasında karşıma yaklaşık 62 dolarlık bir Supabase faturası geldi. Tek başına 62 dolar dramatik bir rakam değil. Ama mesele zaten rakam değildi.

Henüz büyük bir kullanıcı tabanına ulaşmamış bir ürün, neden bu kadar fazla backend trafiği üretiyordu?

Bu soru Supabase’in fiyatlandırmasından çok daha önemliydi. Çünkü cevabı ürünün mimarisinde saklıydı.

Supabase pahalı mıydı, yoksa biz mi çok konuşuyorduk?

Modern backend servislerinin en büyük avantajlarından biri geliştirme hızıdır. Authentication istiyorsunuz: var. PostgreSQL istiyorsunuz: var. Realtime istiyorsunuz: var. Storage, Edge Functions, Row Level Security, API… Hepsi birkaç tık uzakta.

Bu olağanüstü rahatlık aynı zamanda küçük bir tehlike yaratıyor: backend ile konuşmanın maliyetini hissetmiyorsunuz. Bir REST API’yi sıfırdan yazıyor olsaydınız muhtemelen her endpoint’i daha dikkatli tasarlardınız. Ama şöyle bir kod yazmak çok kolay:

ts
const { data } = await supabase  .from("tasks")  .select("*")  .eq("user_id", user.id);

Bir sorgu. Ne olabilir ki? Sorun bir sorguda değil. Sorun bu sorgunun:

  • kaç bileşende çalıştığında,
  • kaç kez tekrarlandığında,
  • kaç kullanıcı tarafından çağrıldığında,
  • görünmeyen sekmelerde çalışmaya devam ettiğinde,
  • aynı verinin farklı context’ler tarafından tekrar tekrar istendiğinde ortaya çıkıyor.

Bir SaaS’ın maliyeti çoğu zaman büyük bir mimari hatadan değil, yüzlerce küçük masum karardan oluşuyor.

İlk büyük hata: polling’i normal kabul etmek

Gerçek zamanlı veri istediğinizde birkaç seçeneğiniz var. Realtime subscription kullanabilirsiniz. Event-driven bir mimari kurabilirsiniz. Cache kullanabilirsiniz. Ya da belirli aralıklarla backend’e sorabilirsiniz: bir şey değişti mi?

Sonuncusu polling. Basittir, çalışır ve kontrol edilmezse çok hızlı büyür.

ts
setInterval(async () => {  await refreshTasks();}, 30_000);

İlk bakışta masum. 30 saniyede bir sorgu. Bir kullanıcı için:

txt
2 request / dakika120 request / saat2.880 request / gün86.400 request / ay

Ve bu yalnızca tek bir polling döngüsü. Aynı uygulamada görevler, takvim, alışkanlıklar, bildirimler ve başka domain’ler ayrı ayrı polling yapıyorsa sayı katlanıyor.

Üstelik kullanıcı uygulamayı gerçekten kullanıyor olmak zorunda bile değil. Sekme açık. Telefon arka planda. Desktop uygulaması açık. Polling çalışmaya devam ediyor.

“Cloud-first” demek “her şeyi sürekli cloud’dan çek” demek değil

Ordovia zaman içinde cloud-first bir yapıya dönüştü. Bunun iyi nedenleri vardı: telefon, web ve masaüstü arasında tutarlı bir deneyim istiyordum. Bir cihazda oluşturulan görev diğerinde görünmeliydi. Takvim güncel kalmalıydı. Notlar senkronize olmalıydı.

Fakat burada çok önemli bir ayrım var:

ts
cloudFirst !== cloudEverySecond
Cloud-first: kaynağın doğrusu cloud’dur. Cloud-every-second: kullanıcı bir şey yaptı mı diye sürekli backend’e sor.

Bunlar aynı şey değil. Bugün bir sistemi tekrar tasarlarken önce şu soruyu soruyorum:

ts
type SyncQuestion = {  changed: boolean;  reason: "user_action" | "remote_event" | "visibility" | "scheduled_refresh";};

Bir veri neden yeniden çekiliyor? Gerçekten değişmiş olma ihtimali var mı? Yoksa sadece “belki değişmiştir” diye mi soruyoruz?

İkinci hata: component lifecycle’ını veri stratejisi sanmak

React dünyasında çok kolay yapılan bir hata var. Bir component mount oluyor:

ts
useEffect(() => {  loadData();}, []);

Sonra başka bir component aynı şeyi yapıyor. Sonra başka bir route. Sonra bir context. Sonra modal. Hepsi aynı tabloyu sorguluyor olabilir.

UI açısından bağımsız görünen component’ler backend açısından bağımsız değildir. Eğer hepsi aynı kullanıcı verisini istiyorsa aslında aynı veri domain’inin müşterileridir. Bu yüzden veri erişimi mümkün olduğunca merkezileşmeli.

Kötü

ts
function TodayTasks() {  useEffect(() => {    fetchTasks();  }, []);}
function UpcomingTasks() {  useEffect(() => {    fetchTasks();  }, []);}
function TaskSidebar() {  useEffect(() => {    fetchTasks();  }, []);}

Daha iyi

ts
const tasks = useTasksStore();

ve store:

ts
const tasksStore = {  data: [],  lastFetchedAt: null,  stale: true,};

Böylece soru “bu component veri çekmeli mi?” olmaktan çıkıp “bu veri şu anda gerçekten stale mi?” haline geliyor. Bu küçük zihinsel değişiklik backend trafiğini ciddi biçimde değiştirebilir.

Üçüncü hata: cache’i yalnızca performans optimizasyonu sanmak

Cache genellikle “sayfa daha hızlı açılsın” diye anlatılıyor. Doğru, ama eksik. Cache aynı zamanda bir maliyet kontrol mekanizmasıdır. Bir kullanıcı 10 saniye önce aldığı veriyi tekrar istiyorsa backend’e gitmek zorunda olmayabilir.

Basit bir stale time bile fark yaratır.

ts
const CACHE_TTL = 5 * 60 * 1000;
function shouldRefresh(lastFetchedAt: number) {  return Date.now() - lastFetchedAt > CACHE_TTL;}

Her veri için beş dakika doğru değildir. Ama prensip önemlidir. Bir veri domain’i için şu dört sorunun cevabı olmalı:

ts
interface CachePolicy {  sourceOfTruth: "cloud" | "local";  staleAfter: number;  invalidateOn: string[];  refreshOnVisibility: boolean;}

Bunların hiçbirini tanımlamazsanız uygulama doğal olarak şu modele kayıyor:

ts
await fetchEverything();

Dördüncü hata: her değişiklikte her şeyi yeniden çekmek

Bir görev tamamlandı. UI şöyle davranıyor:

ts
await completeTask(taskId);await refetchTasks();await refetchProjects();await refetchStats();await refetchToday();await refetchAchievements();

Tek kullanıcı aksiyonu, altı backend çağrısı. Daha sonra XP sistemi geliyor. Takvim geliyor. Bildirim sistemi geliyor. Bir görev tamamlamanın sistem maliyeti fark etmeden büyüyor.

Bunun yerine mutation’ın etkisini açıkça modellemek gerekiyor.

ts
type TaskMutationEffect = {  task: true;  project?: true;  stats?: true;  rewards?: true;};

Ardından yalnızca gerekli domain’ler invalidate edilir. Daha da iyisi, mümkünse sunucudan güncellenmiş canonical state döndürülür.

Beşinci hata: görünür olmayan uygulamanın çalışmaya devam etmesi

Bir browser sekmesi arka planda olabilir. Bir PWA minimize edilmiş olabilir. Bir masaüstü uygulaması saatlerce açık kalabilir. Bu yüzden şu kontrol küçümsenmemeli:

ts
if (document.visibilityState !== "visible") {  return;}

Her polling döngüsünü çözmez. Ama kullanıcının bakmadığı bir UI için sürekli veri taşımayı ciddi biçimde azaltabilir. Ordovia gibi uzun süre açık kalabilen uygulamalarda bu detaylar giderek daha önemli hale geliyor.

Maliyet problemi aslında observability problemidir

Asıl sıkıntım faturanın gelmesi değildi; fatura yalnızca semptomdu. Gerçek problem, uygulamanın hangi davranışının ne kadar request ürettiğini yeterince görünür biçimde bilmiyor olmamdı.

Bir ürün büyüdükçe şu metrikleri izlemek gerekiyor:

ts
interface BackendTelemetry {  requestsPerUser: number;  requestsPerSession: number;  requestsPerRoute: number;  requestsPerFeature: number;  cacheHitRate: number;  mutationCount: number;  realtimeConnections: number;}

“Bu ay 2 milyon request yaptık” tek başına yeterli bilgi değil. Asıl soru: neden? Örneğin 2 milyon isteğin 1,5 milyonu tek bir görünürlük polling sisteminden geliyorsa çözüm nettir. Ama ölçmüyorsanız sadece tahmin edersiniz.

Supabase’i bırakmalı mıyım?

Benim vardığım sonuç bu olmadı. Bir altyapı maliyeti yükseldiğinde servis değiştirmek çok çekici geliyor. Supabase pahalı? Firebase’e geçelim. Firebase pahalı? Cloudflare D1 yapalım. Sonra başka bir şey.

Bu noktada fark ettim ki teknoloji değiştirmek bazen gerçek problemi çözmek yerine mimari borcu başka yere taşıyor.

ts
badArchitecture * cheaperInfrastructure

hâlâ kötü bir sistem olabilir. Hatta migration maliyetini de eklediğinizde daha pahalı hale gelebilir. Bu yüzden bugün önce üç adım uyguluyorum:

ts
const optimizationOrder = [  "measure",  "fix architecture",  "then evaluate infrastructure",];

Önce ölç. Sonra gereksiz tüketimi azalt. Bundan sonra fiyatlandırmayı değerlendir.

Cloudflare burada neden ilgimi çekiyor?

Cloudflare’in benim için cazip tarafı yalnızca ucuz olması değil. Uygulamanın zaten önemli bölümleri Cloudflare üzerinde çalışıyorsa şunlar aynı operasyonel yüzey içinde toplanabiliyor:

  • edge caching,
  • Workers,
  • Pages,
  • R2,
  • çeşitli API katmanları.

Ancak yine aynı prensip geçerli:

Altyapı sadeleştirmek için kullanılmalı, yeni bir hobi projesi yaratmak için değil.

Sırf birkaç dolar kazanmak için çalışan bir Supabase sistemini tamamen D1 + Workers + özel auth mimarisine çevirmek çok kolay biçimde ekonomik optimizasyondan çıkıp mühendislik eğlencesine dönüşebilir. Bu tuzağa düşmek istemiyorum.

Bugün aynı sistemi kursaydım ne yapardım?

Başlangıçta her domain için bir sync policy tanımlardım. Örneğin:

ts
export const syncPolicies = {  tasks: {    staleAfter: 5 * MINUTE,    refreshOnFocus: true,    realtime: true,  },  calendar: {    staleAfter: 10 * MINUTE,    refreshOnFocus: true,    realtime: false,  },  profile: {    staleAfter: 60 * MINUTE,    refreshOnFocus: false,    realtime: false,  },} satisfies Record<string, SyncPolicy>;

Sonra bütün network katmanının bu politikalardan geçmesini isterdim. Component’lerin doğrudan database ile konuşmasına izin vermezdim. Ve daha ilk haftadan şu metriği görünür yapardım:

txt
requests / active user / day

Çünkü kullanıcı sayısı büyüdüğünde toplam request sayısı zaten bunun sonucudur.

En önemli ders

Supabase faturam bana aslında Supabase hakkında çok az şey öğretti. Daha çok kendi ürünüm hakkında bir şey öğretti:

Kullanıcıdan sakladığınız karmaşıklık yok olmaz. Bir yerde maliyet olarak geri döner.

Bazen bu maliyet UX’tir. Bazen teknik borçtur. Bazen latency’dir. Bazen de gerçekten faturadır.

Modern platformların sağladığı hız çok değerli. Ben hâlâ Supabase gibi araçların küçük ekipler ve bağımsız geliştiriciler için olağanüstü kaldıraç sağladığını düşünüyorum. Ama kaldıraç iki yönde çalışıyor: iyi bir mimariyi hızlandırdığı gibi kötü bir mimarinin maliyetini de hızlandırıyor.

Bu yüzden bugün backend seçerken ilk sorum “hangisi daha ucuz?” değil, şu: “Uygulamam bu backend ile neden, ne zaman ve ne kadar konuşacak?” Çünkü çoğu zaman faturanın gerçek cevabı orada.

  • Supabase
  • SaaS
  • Backend
  • Cloudflare
  • Performans
  • Sistem Mimarisi

SER-03 · SER-03.01

SaaS Sistemleri: Mimari, Maliyet ve Sadelik

Backend faturası, cloud seçimi ve ürün karmaşıklığını aynı sistem problemi olarak ele alan saha notları ve karar çerçeveleri.

PROJELERE AÇIK · ÜRÜN VE SİSTEM MİMARI · DEVRALMA / STABİLİZASYON / OPERASYON · WEB · ANDROID · WINDOWS · AHMET CANAL

İletişim

Ürününüz sisteminden hızlı mı büyüdü?

Mevcut durumu, en büyük tıkanmayı ve istediğiniz sonucu yazın. Kapsamı birlikte netleştirelim.

Durum

Yeni danışmanlık ve proje işlerine açık.