Core Concepts › Multi-tenancy

Multi-tenancy (optional package)

NestLaravel is single-tenant by default. Multi-tenancy is an installable package, nestlaravel/tenancy (packages/laravel-tenancy), using the single-database / tenant_id-column model — simple to operate, works with the per-service database rule, and verified by cross-tenant attack tests.

npx nestlaravel add tenancy                 # all services
npx nestlaravel add tenancy --service orders,api

#How the tenant is decided (and why it can't be spoofed)

The gateway takes the tenant from the authenticated user ($user->tenant_id) — never from a client header — and puts it in the HMAC-signed service call (X-Gateway-Tenant). In the service, VerifyGatewaySignature exposes the verified value as the request attribute gateway_tenant_id; the IdentifyTenant middleware copies it into TenantContext and clears it after the request. Requests with no tenant get 403.

#Wiring (once per service)

class="tk-c">// migration
Schema::create(class="tk-s">'invoices', function (Blueprint class="tk-v">$table) {
    class="tk-v">$table->id();
    class="tk-v">$table->tenantId();              class="tk-c">// indexed string tenant_id
    ...
});

class="tk-c">// model
use NestLaravel\Tenancy\Concerns\BelongsToTenant;
class Invoice extends Model { use BelongsToTenant; }

class="tk-c">// routes provider
->middleware([class="tk-s">'api', VerifyGatewaySignature::class, IdentifyTenant::class])

#What is isolated

ConcernMechanismTested
Eloquent reads/updates/deletesglobal TenantScope (fails closed with TenantNotResolved if no tenant)✅ read other tenant's row by id, by where, mass update()/delete()
Writestenant_id stamped on create; creating for, or moving a row to, another tenant throws CrossTenantAccess✅
Queued jobstenant captured in the payload and restored in the worker (previous tenant restored after; safe with sync)✅
Kafkaevents carry tenant_id; wrap consumers in TenantAwareHandler (events without a tenant are dead-lettered)✅
CacheTenantContext::cacheKey('reports') → t:<tenant>:reports✅
File storageTenantContext::path('uploads/a.png') → tenants/<tenant>/uploads/a.png (strips ..)✅
Admin/maintenanceTenantContext::withoutTenancy(fn) — explicit, greppable✅

Not covered (by design of the single-DB model): noisy-neighbour isolation and per-tenant encryption keys/backups. For hard isolation use database-per-tenant (a different package) or separate deployments per tenant.

Run the package tests: cd packages/laravel-tenancy && composer install && php vendor/bin/phpunit.

Edit this page on GitHub