rylxes/laravel-multitenancy

Simple by default, powerful when needed. Single-database and multi-database multitenancy for Laravel.

Maintainers

Package info

github.com/rylxes/laravel-multitenancy

pkg:composer/rylxes/laravel-multitenancy

Statistics

Installs: 0

Dependents: 0

Suggesters: 1

Stars: 0

Open Issues: 0

v1.0.0 2026-03-08 03:02 UTC

This package is auto-updated.

Last update: 2026-03-08 03:13:50 UTC


README

Latest Version on Packagist Total Downloads License PHP Version

Simple, safe multi-tenancy for Laravel. Single-database by default, multi-database when you need it.

Why This Package?

Pain Point Solution
Data leakage between tenants BelongsToTenant trait + safeguard mode throws on unscoped queries
Cache pollution PrefixCacheTask auto-prefixes cache keys per tenant
Storage file mixing PrefixFilesystemTask isolates disk paths per tenant
Queue jobs losing context First-class integration with laravel-tenant-jobs
No fail-safe for missing scopes Safeguard throws NoCurrentTenantException on unscoped queries
Migration complexity tenant:migrate runs migrations across all tenants
Testing difficulty ActingAsTenant trait for clean test setup
No CLI tenant management 6 artisan commands out of the box

Requirements

  • PHP 8.1+
  • Laravel 10, 11, or 12

Installation

composer require rylxes/laravel-multitenancy

Publish config and run migrations:

php artisan vendor:publish --tag=multitenancy-config
php artisan migrate

Quick Start (5 Minutes)

1. Create a tenant

php artisan tenant:create "Acme Corp" --domain=acme.example.com

2. Add tenant_id to your models' tables

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

3. Use the BelongsToTenant trait

use Illuminate\Database\Eloquent\Model;
use Rylxes\Multitenancy\Concerns\BelongsToTenant;

class Project extends Model
{
    use BelongsToTenant;
}

That's it. All queries on Project are now automatically scoped to the current tenant, and tenant_id is auto-filled on creation.

4. Add middleware to your routes

// routes/web.php or bootstrap/app.php
Route::middleware('tenancy')->group(function () {
    Route::resource('projects', ProjectController::class);
});

Tenant Identification

The package resolves tenants from incoming requests. Choose a strategy in config/multitenancy.php:

'tenant_finder' => \Rylxes\Multitenancy\Finders\DomainTenantFinder::class,
Finder How it works Best for
DomainTenantFinder Matches request->getHost() Custom domains (acme.com)
SubdomainTenantFinder Extracts first subdomain part SaaS subdomains (acme.app.com)
PathTenantFinder Uses first URL segment Path-based (app.com/acme/...)
HeaderTenantFinder Reads X-Tenant-ID header APIs and SPAs

Data Isolation

BelongsToTenant Trait

Add to any Eloquent model to auto-scope all queries and auto-fill tenant_id:

use Rylxes\Multitenancy\Concerns\BelongsToTenant;

class Invoice extends Model
{
    use BelongsToTenant;
}

// Automatically adds WHERE tenant_id = ? to all queries
$invoices = Invoice::all(); // Only current tenant's invoices

// tenant_id auto-filled on create
$invoice = Invoice::create(['amount' => 100]); // tenant_id set automatically

Safeguard Mode

When safeguard is true (default), the package throws NoCurrentTenantException if you query a tenant-scoped model without an active tenant. This prevents accidental data leakage:

// Without any tenant context active:
Invoice::all(); // Throws NoCurrentTenantException

Admin/Landlord Queries

Bypass tenant scoping when needed:

// Get all invoices across all tenants (admin use)
$allInvoices = Invoice::withoutTenantScope()->get();

Cache & Storage Isolation

Enabled by default via switch tasks:

// config/multitenancy.php
'switch_tenant_tasks' => [
    PrefixCacheTask::class,        // Cache keys: tenant_1_key
    PrefixFilesystemTask::class,   // Storage: storage/app/tenant_1/
    ClearResolvedInstancesTask::class,
    // SwitchDatabaseTask::class,  // Uncomment for multi-database
],

Multi-Database Support

Uncomment SwitchDatabaseTask in config and set your tenant's database field:

php artisan tenant:create "Acme Corp" --domain=acme.example.com --database=tenant_acme
// config/multitenancy.php
'database' => [
    'strategy' => 'multi',
    'tenant_connection' => 'tenant',
    'landlord_connection' => env('DB_CONNECTION', 'mysql'),
],

Programmatic Usage

use Rylxes\Multitenancy\Facades\Tenant;

// Set current tenant
$tenant = \Rylxes\Multitenancy\Models\Tenant::find(1);
Tenant::makeCurrent($tenant);

// Check current tenant
Tenant::current();   // Returns Tenant model or null
Tenant::check();     // Returns bool

// Run code as a specific tenant
Tenant::execute($tenant, function () {
    // All queries scoped to $tenant here
    $projects = Project::all();
});
// Previous tenant context (or none) is restored after

// Global helper
tenant();            // Returns current Tenant or null

// Forget current tenant
Tenant::forgetCurrent();

Artisan Commands

Command Description
tenant:list List all tenants
tenant:create {name} Create a tenant (--domain=, --database=)
tenant:delete {id} Delete a tenant (with confirmation)
tenant:migrate Run migrations for all tenants (--tenant= for one)
tenant:seed Seed databases for all tenants
tenant:artisan {command} Run any artisan command for all tenants

Events

Event When
TenantMadeCurrent After a tenant is set as current
TenantForgotten After tenant context is cleared
TenantCreated After a new tenant is created
TenantDeleted After a tenant is deleted

Queue Jobs

For tenant-aware queue jobs, install laravel-tenant-jobs:

composer require rylxes/laravel-tenant-jobs

Zero-config integration — laravel-tenant-jobs auto-detects this package and handles tenant context in all queued jobs, batch callbacks, retries, scheduled tasks, and notifications.

Testing

Use the ActingAsTenant trait in your tests:

use Rylxes\Multitenancy\Testing\ActingAsTenant;

class ProjectTest extends TestCase
{
    use ActingAsTenant;

    public function test_projects_are_scoped_to_tenant(): void
    {
        $tenant = Tenant::create(['name' => 'Test']);
        $this->actingAsTenant($tenant);

        $project = Project::create(['name' => 'My Project']);

        $this->assertEquals($tenant->id, $project->tenant_id);
    }
}

Configuration Reference

// config/multitenancy.php
return [
    'tenant_model'   => \Rylxes\Multitenancy\Models\Tenant::class,
    'tenant_finder'  => \Rylxes\Multitenancy\Finders\DomainTenantFinder::class,
    'tenant_column'  => 'tenant_id',
    'safeguard'      => true,  // Throw on unscoped tenant queries

    'switch_tenant_tasks' => [
        PrefixCacheTask::class,
        PrefixFilesystemTask::class,
        ClearResolvedInstancesTask::class,
        // SwitchDatabaseTask::class, // Uncomment for multi-DB
    ],

    'database' => [
        'strategy' => 'single', // 'single' or 'multi'
        'tenant_connection' => 'tenant',
        'landlord_connection' => env('DB_CONNECTION', 'mysql'),
    ],

    'cache' => ['prefix_base' => 'tenant_'],
    'filesystem' => [
        'suffix_base' => 'tenant_',
        'disks' => ['local', 'public'],
    ],
];

Architecture

src/
  TenantManager.php              — Core singleton: makeCurrent/forgetCurrent/execute
  MultitenancyServiceProvider.php — Config, migrations, commands, middleware
  Models/Tenant.php              — Eloquent model
  Facades/Tenant.php             — Facade for TenantManager
  Contracts/TenantFinder.php     — Interface for identification strategies
  Finders/                       — Domain, Subdomain, Path, Header finders
  Concerns/
    BelongsToTenant.php          — Trait: global scope + auto-fill tenant_id
    TenantAware.php              — Trait: iterate commands over tenants
  Scopes/TenantScope.php         — Global scope: WHERE tenant_id = ?
  Tasks/                         — Cache, Filesystem, Database, Instance switching
  Middleware/                    — InitializeTenancy, EnsureTenantSession
  Events/                       — TenantMadeCurrent, Forgotten, Created, Deleted
  Console/Commands/              — 6 tenant management commands
  Testing/ActingAsTenant.php     — Test helper trait

License

MIT. See LICENSE.