---
title: "Multi-Tenant Uygulama Mimarisi: Tek Veritabanı mı, Ayrı Veritabanı mı?"
description: "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."
date: 2026-04-14
category: mimari
tags: ["multi-tenant", "mimari", "supabase", "rls", "saas", "veritabani-tasarimi", "astaflow"]
url: https://mikroerp.dev/blog/multi-tenant-uygulama-mimarisi-rehberi/
---

## İş 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:

```sql
-- 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)

*   **AstaFlow Case Study:** [Çok Şubeli Supabase / React Kurguları](/case-study/)
*   **İlgili Çözüm:** [PostgreSQL Merkezi Loglama (Auditing)](/blog/postgresql-merkezi-audit-log-sistemi/)