Multi-tenancy, tek bir uygulama örneğinin birden fazla müşteri (tenant) tarafından paylaşılması anlamına gelir. SaaS ürünleri geliştirirken bu mimari kararı yanlış vermek, ilerleyen dönemde büyük maliyetlere yol açar. Ben AquilaFlow projesinde bu hatayı bizzat yaşadım ve ikinci kez yazmak zorunda kaldım. Bu yazıda öğrendiklerimi aktarıyorum.

Üç Temel Yaklaşım

Multi-tenant mimaride üç ana yaklaşım bulunur. Her birinin avantajları ve ödünleşimleri vardır.

1. Shared Database, Shared Schema

  • Nasıl çalışır: Tüm tenantların verisi aynı tablolarda, tenant_id sütunuyla ayrılır
  • Avantaj: En ucuz, en kolay deployment, en az kaynak tüketimi
  • Dezavantaj: Veri izolasyonu zayıf, tek hatalı sorgu tüm tenantları etkiler
  • Ne zaman: Bootstrap aşaması, düşük veri güvenliği gereksinimleri

2. Shared Database, Separate Schema

  • Nasıl çalışır: Aynı veritabanı, her tenant için ayrı şema (PostgreSQL şemaları)
  • Avantaj: İyi izolasyon, makul maliyet, migration kolaylığı
  • Dezavantaj: PostgreSQL bağımlılığı (MySQL şema desteği zayıf)
  • Ne zaman: Orta ölçekli SaaS, GDPR gereksinimleri

3. Separate Database per Tenant

  • Nasıl çalışır: Her tenant için ayrı veritabanı bağlantısı
  • Avantaj: Tam izolasyon, bağımsız yedekleme/geri yükleme
  • Dezavantaj: Yüksek maliyet, çok sayıda DB bağlantısı yönetimi
  • Ne zaman: Enterprise müşteriler, yüksek güvenlik gereksinimleri

Laravel'de Shared Schema Implementasyonu

Çoğu SaaS projesi için önerdiğim yaklaşım budur. tenancy/tenancy paketini kullanmadan sıfırdan nasıl yapılır, göstereyim.

1. Tenant Modeli ve Middleware

Her HTTP isteğinde hangi tenantın aktif olduğunu belirlemeniz gerekir. Bunu subdomain veya custom domain üzerinden yapabilirsiniz:

// app/Http/Middleware/IdentifyTenant.php
public function handle(Request $request, Closure $next)
{
    $host   = $request->getHost();
    $tenant = Tenant::where('domain', $host)
                    ->orWhere('subdomain', explode('.', $host)[0])
                    ->firstOrFail();

    app()->instance(Tenant::class, $tenant);
    config(['app.tenant_id' => $tenant->id]);

    return $next($request);
}

2. Global Scope ile Otomatik Filtreleme

Her modele manuel where('tenant_id', ...) eklemek yerine global scope kullanın:

// app/Models/Concerns/BelongsToTenant.php
trait BelongsToTenant
{
    protected static function bootBelongsToTenant(): void
    {
        static::addGlobalScope(new TenantScope());
        static::creating(function ($model) {
            $model->tenant_id ??= config('app.tenant_id');
        });
    }
}

// app/Models/Scopes/TenantScope.php
class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where($model->getTable() . '.tenant_id', config('app.tenant_id'));
    }
}

3. Migration Stratejisi

Tenant tablolarına her zaman tenant_id foreign key ekleyin ve composite index kullanın:

$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->index(['tenant_id', 'created_at']); // bileşik index
$table->index(['tenant_id', 'status']);      // sorgularınıza göre

Dikkat Edilmesi Gereken Tuzaklar

  • Queue jobs: Job dispatch edildiğinde tenant context kaybolur. Job içinde tenant_id'yi serialize edin ve işlem başında yeniden yükleyin.
  • Scheduled commands: Artisan schedule tüm tenantlar için çalışmalı mı? Her tenant için ayrı schedule planlaması yapın.
  • File storage: S3 bucket'larında tenant prefix kullanın: tenants/{tenant_id}/files/
  • Cross-tenant sorgular: Admin paneli için global scope'u withoutGlobalScope(TenantScope::class) ile devre dışı bırakın.
  • Cache izolasyonu: Cache key'lere tenant_id prefix ekleyin: cache()->tags(["tenant:{$tenantId}"])

Ne Zaman Separate Database'e Geçmeli?

Shared schema yaklaşımıyla başlayın. Aşağıdaki durumlardan biri gerçekleştiğinde separate database'e migre edin:

  • Bir tenant, toplam veritabanı boyutunun %30'undan fazlasını oluşturmaya başladığında
  • Enterprise bir müşteri SOC 2 veya ISO 27001 kapsamında tam veri izolasyonu talep ettiğinde
  • Tenant bazında farklı yedekleme veya veri saklama politikaları uygulamanız gerektiğinde
  • GDPR kapsamında "sil" talebi geldiğinde tek bir veritabanını drop etmek istediğinizde

Sonuç

Multi-tenant mimaride erken aşamada en basit yaklaşımla başlamak, zamanında değer üretmenizi sağlar. Shared schema ile başlayın, BelongsToTenant trait'ini her modele uygulayın ve test kapsamını yüksek tutun. İlerleyen dönemde migrate etmek için stratejinizi şimdiden düşünün.

Sorularınız varsa iletişim formundan ulaşabilirsiniz.