Bir B2B SaaS platformu geliştirirken, kritik bir mimari karar vermek durumunda kaldım: her kiracıya ayrı bir veritabanı mı yoksa mantıksal izolasyon ile paylaşılan bir veritabanı mı kullanılmalı?
Ben paylaşılan veritabanını seçtim. İşte nedenleri ve nasıl sağlam veri izolasyonu sağladığımı anlatan süreç.
Neden Paylaşılan Veritabanı?
Neden Paylaşılan Veritabanı?
Bir B2B SaaS için, veritabanı evrimi oldukça kritik:
Artılar:
- Atomik migration’lar (bir sütun eklemek, 500 kez değil)
- Basitleştirilmiş yedekleme/geri yükleme
- Daha düşük alt yapı maliyetleri
Eksiler:
- Fiziksel değil, mantıksal izolasyon
- Gürültücü komşu riski
Eğer 500 kiracıya ulaşırsam, her özellik için 500 ayrı migration yönetmek tam zamanlı bir iş olurdu. Paylaşılan veritabanı kazanıyor.
Mimari
Mimari
USER REQUEST
│
▼
MIDDLEWARE (EnsureTenantAccess)
│ 1. Verify authentication
│ 2. Extract tenant ID
│ 3. Store in TenantContext (singleton)
▼
ELOQUENT MODELS (with BelongsToTenant trait)
│ 4. Apply GlobalScope automatically
▼
DATABASE QUERY
SELECT * FROM table WHERE tenant_id = [X]
TenantContext: Doğrunun Kaynağı
TenantContext: Doğrunun Kaynağı
class TenantContext
{
private ?string $tenantId = null;
public function set(string $id): void
{
$this->tenantId = $id;
}
public function id(): string
{
return $this->tenantId;
}
public function isSet(): bool
{
return $this->tenantId !== null;
}
}
Singleton. Her istek için bir örnek. Hiçbir belirsizlik olmadan.
BelongsToTenant Trait
BelongsToTenant Trait
Burası sihrin gerçekleştiği yer:
trait BelongsToTenant
{
public static function bootBelongsToTenant(): void
{
// OKUMA: Tüm sorguları otomatik olarak filtrele
static::addGlobalScope('tenant', function (Builder $query) {
$context = app(TenantContext::class);
if ($context->isSet()) {
$query->where('tenant_id', $context->id());
}
});
// YAZMA: Oluşturma işlemi sırasında tenant_id'yi otomatik olarak atayın
static::creating(function (Model $model) {
$context = app(TenantContext::class);
if ($context->isSet() && !$model->tenant_id) {
$model->tenant_id = $context->id();
}
});
}
}
Her kiracıya özel model (Müşteriler, Araçlar, Taşıma) bu trait’i kullanır. Manuel tenant_id ataması yok. Unutkanlık hataları yok.
Kiracılar Arası İzolasyonu Test Etme
Kiracılar Arası İzolasyonu Test Etme
public function test_cross_tenant_isolation(): void
{
$tenantA = Tenant::factory()->create();
$tenantB = Tenant::factory()->create();
Product::factory()->for($tenantA)->create(['name' => 'Item A']);
Product::factory()->for($tenantB)->create(['name' => 'Item B']);
$response = $this->actingAs($this->userInTenant($tenantA))
->getJson('/api/v1/products');
$response->assertOk();
$response->assertJsonCount(1, 'data');
$response->assertJsonPath('data.0.name', );
}
Bu test olmadan, GlobalScope’un çalıştığını sadece umut ediyorum. Umut, bir strateji değil.
Süper Yönetici Taklit Etme
Süper Yönetici Taklit Etme
Destek, müşterilerin gördüklerini görebilmelidir. Çözüm:
$tenantId = $user->isSuperAdmin()
? session()
: $user->tenant_id;
Süper yönetici, sabit bir tenant_id’ye sahip değildir. Panel üzerinden bir kiracıyı taklit etmek için seçer. Oturum, ID’yi saklar. Middleware, TenantContext’i doldurur. Tam olarak müşteriyle aynı görünümler.
Salt Okuma Modu
Salt Okuma Modu
if ($tenant->is_read_only && $request->isMethodSafe() === false) {
abort(403, );
}
Kullanım senaryoları:
- Ödeme durdurmayı
- Bakım pencereleri
- Soruşturma kilitlerini
Tüm bunlar middleware içinde merkezi hale getirilir. Controller kirliliği yok.
Öğrenilen Anti-Durumlar
Öğrenilen Anti-Durumlar
- Manuel tenant_id ataması: Unutacaksınız. Trait kullanın.
- Tenant_id olmadan benzersiz indeksler: Her zaman bileşik (tenant_id + unique_field) kullanın.
- Kiraçılar için artan ID’ler: UUID’leri kullanın. ID tahmin saldırılarını önler.
Bir Paket Kullanır Mıyım?
Bir Paket Kullanır Mıyım?
Spatie’nin spatie/laravel-multitenancy paketi sağlamdır.
Ancak kendi çözümümü geliştirmek bana şu avantajları sağladı:
- Sınırlı durumlar üzerinde tam kontrol
- Süper yönetici taklit etme (paketlerle basit değil)
- İzolasyon sınırları hakkında derin anlayış
Öğreniyorsanız veya özel akışlara ihtiyacınız varsa, kendiniz inşa edin. Hız ve standart desenlere ihtiyacınız varsa, bir paket kullanın.
Sonuç
Sonuç
Laravel ile çok kiracılılık, otomasyonunuza güvenmek, insan hafızanıza güvenmemek ile ilgilidir.
Global Scopes + TenantContext + kapsamlı testler = gece rahat uyuyun.
Kaynak: Orijinal Makale


