Multi-tenant'ı baştan yapmak: KOBİ SaaS'ı için neden tek doğru karar bu
Multi-tenant mimariyi sonradan eklemek, kart kulesini yeniden inşa etmek gibidir. Omnirago'yu inşa ederken neden gün 1'den itibaren multi-tenant tasarladığımız ve Postgres row-level security yaklaşımımız.
KOBİ SaaS inşa eden ekiplerin en sık yaptığı hata: multi-tenant’ı sonraya bırakmak.
Mantık şöyle ilerliyor: “Şimdilik tek müşteri için yapalım, ürün-pazar uyumunu bulduktan sonra ölçekleriz.” Bu mantığın sorunu, “sonra” hiç gelmemesi. Geldiğinde ise pratik olarak yeniden yazım demek oluyor — tüm sorgulara WHERE tenant_id = ? ekleyip dua ederek.
Omnirago’yu inşa ederken gün 1’den itibaren multi-tenant tasarladık. Bu yazıda kararları, takasları ve Postgres RLS yaklaşımımızı paylaşıyorum.
Üç multi-tenancy modeli
Bilinen 3 model var:
- Tek veritabanı, paylaşılan şema. Tek Postgres veritabanı, her tabloda
tenant_idkolonu. En sık tercih edilen. - Tek veritabanı, şema-per-tenant. Her kiracı için ayrı Postgres şeması. Orta düzey izolasyon.
- Veritabanı-per-tenant. Her kiracı için ayrı veritabanı. Maksimum izolasyon, maksimum operasyonel yük.
KOBİ SaaS için #1 doğru cevap. Sebep: KOBİ kiracılarının veri boyutu (ortalama 50K-500K kayıt) tek veritabanında rahatça duruyor. Şema-per-tenant migration cehennemine yol açar; veritabanı-per-tenant ise 1000 kiracıda 1000 backup, 1000 connection pool, 1000 ayrı patch yönetimi demek.
Row-Level Security ile uygulama katmanını korumak
Tek veritabanı + tenant_id modelinin korkusu basit: bir gün biri WHERE tenant_id = ? eklemeyi unutur, ve kiracı A, kiracı B’nin verisini görür. Bu durum, uygulama katmanına güvenmenin yan etkisi.
Postgres Row-Level Security (RLS) bu güveni veritabanına devreder. Şöyle çalışır:
ALTER TABLE customers ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON customers
USING (tenant_id = current_setting('app.current_tenant')::uuid);
Şimdi her sorgu, oturum değişkeninde app.current_tenant ne ise sadece o kiracının satırlarını döndürür. Geliştirici unutsa bile veritabanı durdurur.
Uygulama tarafında ise her isteğin başında oturum değişkenini ayarlamak yetiyor:
await pg.query(`SET LOCAL app.current_tenant = '${tenantId}'`);
SET LOCAL transaction kapsamında çalıştığı için connection pool’da sızma yok.
Connection pool ve SET LOCAL ikilemi
PgBouncer ile transaction-mode pooling kullanıyorsanız, SET komutu sorun çıkarır (session-level state pool’da yanlış connection’a düşebilir). SET LOCAL ise transaction içinde geçerli olduğu için güvenli.
Pratik kural: her HTTP isteği bir transaction başlat, SET LOCAL ile tenant’ı ata, sorguları çalıştır, commit et. Bir middleware bunu otomatize eder, geliştirici hiç düşünmez.
Üst yönetici (admin) erişimi
Genel destek ekibinin sorun gidermek için tüm kiracıların verilerine bakması gerekebilir. RLS’i bypass etmenin doğru yolu, ayrı bir veritabanı kullanıcısı tanımlamaktır:
ALTER ROLE app_admin BYPASSRLS;
app_admin rolü ile bağlanan destek aracı tüm kiracıları görebilir, son kullanıcı uygulaması ise RLS’i devre dışı bırakamaz. Audit log’un buradan geçtiğinden emin olun.
Holding/grup yapısı için subaccount desteği
Türkiye’de KOBİ’lerin önemli kısmı holding yapısında çalışıyor. Bir tekstil grubunun 3 markası, her marka için ayrı bayi ağı, ortak finans kaydı — bu yapı için tek tenant_id yetersiz.
Omnirago’da hiyerarşik tenant modeli kullanıyoruz: tenant.parent_tenant_id self-referencing FK. RLS politikası ise kullanıcının erişebileceği tenant kümesini hesaplayan bir fonksiyon üzerinden çalışıyor:
CREATE POLICY tenant_hierarchy ON customers
USING (tenant_id IN (
SELECT id FROM accessible_tenants(current_setting('app.current_user')::uuid)
));
Üst seviyedeki holding yöneticisi tüm bağlı tenant’ları görür, marka müdürü sadece kendi markasını.
Çok para birimi, çok dil — kiracı bazında
Multi-tenant tasarımın doğal sonucu: her kiracının kendi para birimi, vergi rejimi, dil tercihi olabilir. Bunları tenant tablosunda saklamak ve uygulama katmanında her isteğin başında yüklemek, “global state” anti-paterninden kaçınmanın en temiz yolu.
İhracatçı bir KOBİ aynı anda TRY, USD, EUR fatura kesebilmeli; yurtdışı müşterilerine İngilizce iletişim, yurt içi bayilere Türkçe gitmeli. Bu konfigürasyon tenant başına, kullanıcı oturumu ile birleşince doğal sonuç veriyor.
Maliyet: gerçekten ne kadar?
Multi-tenant’ı gün 1’den yapmanın bedeli:
- Her tabloya
tenant_idkolonu + index → minimal disk + minimal sorgu maliyeti - RLS politika çağrıları → sorgu başına %2-5 ek yük (ölçtük, fark edilmez)
- Connection bazında
SET LOCAL→ ihmal edilebilir
Multi-tenant’ı sonradan yapmanın bedeli: tüm sorguları gözden geçirmek, tüm cache anahtarlarına tenant_id eklemek, tüm background job’ları multi-tenant’a uyarlamak, ve bunları yaparken üretim trafiğini bozmamak için 3-6 ay.
Bu fark, KOBİ SaaS’ı kuran ekipler için “doğru iş, doğru zamanda, doğru sırada” demek.
Devamı
Bir sonraki yazıda Omnirago’nun WhatsApp Business API hız limitlerini nasıl yönettiğini ve kiracı başına kotaları nasıl ayrıştırdığımızı anlatacağım.
Ragomind, KOBİ’ler için yapay zeka destekli B2B yazılımlar üretir. Mühendislik iş birlikleri için hello@ragomind.com.