23.12.2025

Laravel’de Çok Kiracılı (Multi-Tenant) Yapı: Tek Veritabanı ile Tenant İzolasyonu

Tek veritabanında tenant izolasyonu: global scope, middleware ve bağlantı stratejileriyle pratik multi-tenant kurulum.

Neden Multi-Tenant?

SaaS geliştiriyorsanız aynı uygulamayı birden fazla müşteri (tenant) kullanır. Multi-tenant mimaride amaç; veriyi karıştırmadan (izolasyon), performansı düşürmeden ve güvenliği artırarak müşterileri tek uygulamada yönetmektir.

Bu yazıda popüler başlıklardan farklı olarak; tek veritabanı + tenant_id yaklaşımını Laravel’de “kazaya kapalı” hale getiren pratik bir iskelet kuracağız.


Model: Tenant ve tenant_id standardı

En basit senaryo: tüm tenant verileri aynı DB’de, tablolar tenant_id ile ayrılır.

// database/migrations/xxxx_create_tenants.php
Schema::create('tenants', function ($table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->timestamps();
});

Uygulama tablolarınıza tenant_id ekleyin:

Schema::table('projects', function ($table) {
    $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
    $table->index(['tenant_id']);
});


Tenant çözümleme: Subdomain ile tenant seçmek

Örnek: acme.app.com → tenant slug = acme

// routes/web.php
Route::middleware(['web', 'tenant.resolve'])->group(function () {
    Route::get('/projects', fn () => \App\Models\Project::latest()->paginate());
});

Middleware:

// app/Http/Middleware/ResolveTenant.php
namespace App\Http\Middleware;

use Closure;
use App\Models\Tenant;

class ResolveTenant
{
    public function handle($request, Closure $next)
    {
        $host = $request->getHost();
        $subdomain = explode('.', $host)[0] ?? null;

        $tenant = Tenant::where('slug', $subdomain)->firstOrFail();

        app()->instance('tenant', $tenant); // basit tenant konteyneri

        return $next($request);
    }
}

Kernel.php içine ekleyin:

protected $routeMiddleware = [
    'tenant.resolve' => \App\Http\Middleware\ResolveTenant::class,
];


En kritik parça: Global Scope ile otomatik filtre

İnsan hatasını azaltmanın yolu: sorguların tenant filtresini otomatik eklemek.

Bir trait oluşturalım:

// app/Models/Concerns/BelongsToTenant.php
namespace App\Models\Concerns;

use Illuminate\Database\Eloquent\Builder;

trait BelongsToTenant
{
    protected static function bootBelongsToTenant()
    {
        static::addGlobalScope('tenant', function (Builder $builder) {
            if (app()->bound('tenant')) {
                $builder->where($builder->getModel()->getTable().'.tenant_id', app('tenant')->id);
            }
        });

        static::creating(function ($model) {
            if (app()->bound('tenant') && empty($model->tenant_id)) {
                $model->tenant_id = app('tenant')->id;
            }
        });
    }
}

Model’de kullanın:

class Project extends Model
{
    use \App\Models\Concerns\BelongsToTenant;

    protected $fillable = ['name'];
}

Artık Project::query() tenant filtresi olmadan “yanlışlıkla” başka tenant verisi döndüremez.

Yönetici ekranında tüm tenant verilerini görmek isterseniz:
Project::withoutGlobalScope('tenant')->...


Güvenlik notu: Route Model Binding tuzağı

/projects/{project} gibi rotalarda model binding tenant scope’u sayesinde otomatik filtrelenir. Ancak bazı durumlarda scope devre dışı kalırsa risk oluşur. Bu yüzden global scope yaklaşımı kritik.

Ek önlem: Policy ile tenant kontrolü.

public function view($user, Project $project)
{
    return $project->tenant_id === app('tenant')->id;
}


Performans: Index ve cache

  • Tablolarda tenant_id index’i şart.
  • Tenant çözümlemesini cache’leyin:
$tenant = cache()->remember(
    'tenant:'.$subdomain,
    now()->addMinutes(10),
    fn() => Tenant::whereSlug($subdomain)->firstOrFail()
);


Sonuç

Tek veritabanında multi-tenant yapı kurarken amaç “çalışması” değil, yanlış çalışmamasıdır. ResolveTenant + Global Scope + creating hook kombinasyonu; hem geliştirici ergonomisini artırır hem de veri izolasyonunu varsayılan hale getirir.

Bir sonraki adım olarak; tenant’a özel dosya depolama (Storage::disk) ve job/queue izolasyonunu (örn. tenant_id ile) aynı mantıkla standardize edebilirsiniz.