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:
- 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.
- 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). - 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
- “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_rolekey’ini (asla frontend’e çıkmayan gizli anahtar) Backend Node/Edge Function katmanından kullanarak özel admin Dashboard’larınıza veri çekmektir. - Güvenlik Çatlağı (SECURITY DEFINER): Supabase üzerinde bir SQL Fonksiyonu (Stored Procedure) yazdığınızda, eğer komutu
SECURITY DEFINERyetkisiyle etiketlerseniz, o fonksiyon “Postgres (Admin)” modunda çalışır ve RLS kurallarını yok sayar. Multi-Tenant yapılarda yazdığınız prosedürleriSECURITY INVOKERolarak etiketlemelisiniz ki fonksiyon, çağıran kişinin bağlamından çıkıp başkasının verisini kanatlandırmasın. - 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 Limitinguygulanmalıdır.
Bu Bilgiyi Nereden Biliyoruz? (Kaynaklar)
- AstaFlow Case Study: Çok Şubeli Supabase / React Kurguları
- İlgili Çözüm: PostgreSQL Merkezi Loglama (Auditing)