Multi-Tenant Uygulama Mimarisi: Tek Veritabanı mı, Ayrı Veritabanı mı?

Piyasaya B2B SaaS yaparken en büyük teknik karar: Shared Schema mı Separate DB mi? Supabase Row Level Security (RLS) ile yanılmaz veri izolasyonu kurulumu.

İş Problemi: “Yanlışlıkla Başka Müşterinin Verisini Göstermek!”

AstaFlow gibi B2B çalışan SaaS projeleri yatırımı aldığında ilk kritik mimari tartışma başlar: Şirket A’nın faturaları ile Şirket B’nin personelleri veritabanında nasıl yalıtılır?

Geleneksel web frameworklerinde (Express, Django, Laravel vs.) geliştiriciler veritabanına bir tenant_id sütunu koyar ve yazılım kodunun içinde filtre uydururlar: SELECT * FROM Siparisler WHERE sirket_id = AktifSirket

Bu çok zehirli bir yaklaşımdır. Neden mi? Proje devasa boyutlara ulaştığında junior bir yazılımcı koda girip o WHERE fıkrasını unutursa veya Dropdown filtresinde sirket_id=ALL çekerse, Müşteri A giriş yaptığı ekranda Müşteri B’nin tüm finansal bilançosunu görür. (Ünlü SaaS skandallarının %90’ı böyle başlar).

Mimarilere Seçim Rehberi

Üç tür izolasyon modeli vardır:

  1. Ayrı Veritabanları (Database-per-Tenant): Çok güvenli ama çok pahalı. AWS RDS faturası batırır. Migration’lar (sütun ekleme vs.) 50 müşteri için ayrı ayrı tetiklenmelidir.
  2. Ayrı Şemalar (Schema-per-Tenant): Tek fiziksel DB ama tenantA.Siparis, tenantB.Siparis. (Büyük ölçeklerde Postgres’in hafızasını şişiren Anti-Pattern’dir).
  3. Ortak Şema (Shared Schema + RLS): Tek tablo, tek veritabanı. Ancak Supabase Postgres RLS (Row Level Security) sayesinde izolasyon Yazılım Koduyla değil, bizzat Veritabanı Motoru ile yapılır.

Çözüm: Supabase RLS ile Asla Kırılamayan Duvarlar

Biz AstaFlow modelinde Supabase’in gücünden faydalanarak İzin verilmeyenin yasaklandığı RLS duvarları inşa ederiz. Supabase, Auth.jwt() payload’ı içine kullanıcının ait olduğu tenant_id’yi enjekte edebilir.

İşte mükemmel SaaS izolasyon scripti:

-- 1. ADIM: TABLOLARIN OLUŞTURULMASI
CREATE TABLE sirketler (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  isim TEXT NOT NULL,
  domain TEXT UNIQUE NOT NULL
);

CREATE TABLE urunler (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  sirket_id UUID NOT NULL REFERENCES sirketler(id) ON DELETE CASCADE,
  urun_adi TEXT NOT NULL,
  fiyat NUMERIC
);

-- KİLİT ADIM: Tabloda RLS özelliğini aktifleştiriyoruz (Artık db boş döner!)
ALTER TABLE urunler ENABLE ROW LEVEL SECURITY;

-- 2. ADIM: GÜVENLİ BAĞLAMIN OKUNMASI (JWT)
-- Kullanıcının access_token (JWT) dosyasının app_metadata bölümünden şirket kimliğini çeken güvenli fonksiyon.
CREATE OR REPLACE FUNCTION get_active_tenant() 
RETURNS UUID AS $$
  SELECT (auth.jwt() -> 'app_metadata' ->> 'sirket_id')::UUID;
$$ LANGUAGE SQL STABLE SECURITY DEFINER;

-- 3. ADIM: İZOLASYON POLİTİKALARI (The Wall)
-- Okuma Politkası:
CREATE POLICY "SirketSadeceKendiUrunleriniGorebilir" ON urunler
  FOR SELECT
  USING (sirket_id = get_active_tenant());

-- Yazma (Insert/Update) Politikası:
CREATE POLICY "BaskaSirketeUrunYazilamaz" ON urunler
  FOR INSERT
  WITH CHECK (sirket_id = get_active_tenant());

Artık Frontend geliştiriciniz şuraya bir fetch() atıp tüm ürünleri çekse bile (supabase.from('urunler').select('*')), PosgreSQL motoru otomatik olarak JWT’yi deşifre eder ve sadace o müşteriye ait olan satırları yollar. Kod hatası ihtimali sıfırlanmıştır!

Edge Cases (Tehlike Çanları) ve Performans Analizi

  1. “SuperAdmin” Krizi: Sistemi kodladınız, her şey harika. Peki AstaFlow geliştiricileri olarak siz, tüm şirketlerin toplam sipariş hacmini görmek istiyorsunuz? Normalde RLS sizi engeller! Çözüm: RLS Bypass yetkisine sahip service_role key’ini (asla frontend’e çıkmayan gizli anahtar) Backend Node/Edge Function katmanından kullanarak özel admin Dashboard’larınıza veri çekmektir.
  2. Güvenlik Çatlağı (SECURITY DEFINER): Supabase üzerinde bir SQL Fonksiyonu (Stored Procedure) yazdığınızda, eğer komutu SECURITY DEFINER yetkisiyle etiketlerseniz, o fonksiyon “Postgres (Admin)” modunda çalışır ve RLS kurallarını yok sayar. Multi-Tenant yapılarda yazdığınız prosedürleri SECURITY INVOKER olarak etiketlemelisiniz ki fonksiyon, çağıran kişinin bağlamından çıkıp başkasının verisini kanatlandırmasın.
  3. Noisy Neighbor (Gürültülü Komşu): Tek tabloluk yapılarda Şirket A, API’nize saniyede 10.000 veri pompalamaya başlarsa, veritabanının CPU’su %100 olur ve Şirket B’nin sistemi de yavaşlar/çöker. (SaaS yapılarının en büyük kabusu). Önlem olarak Supabase tarafında Connection Pooling (PgBouncer vs Supavisor) aktif edilmeli ve Cloudflare/Vercel üzerinden Rate Limiting uygulanmalıdır.

Bu Bilgiyi Nereden Biliyoruz? (Kaynaklar)

📚 İlgili Yazılar