Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Multitenancy Laravel Package

spatie/laravel-multitenancy

Unopinionated multitenancy for Laravel: detect the current tenant per request and define what happens when switching tenants. Supports single or multiple databases, tenant-aware queues/jobs, per-tenant artisan commands, and easy model connection switching.

View on GitHub
Deep Wiki
Context7
## Getting Started

### Minimal Setup
1. **Installation**:
   ```bash
   composer require spatie/laravel-multitenancy
   php artisan vendor:publish --provider="Spatie\Multitenancy\MultitenancyServiceProvider"

This publishes the config file (config/multitenancy.php) and migration for the tenants table.

  1. Run Migrations:

    php artisan migrate
    
  2. Configure Tenant Model: Update config/multitenancy.php to specify your tenant model (e.g., App\Models\Tenant). Ensure your model implements Spatie\Multitenancy\Contracts\IsTenant:

    use Spatie\Multitenancy\Contracts\IsTenant;
    
    class Tenant implements IsTenant
    {
        // Your model logic
    }
    
  3. Define Tenant Finder: The package ships with DomainTenantFinder (for subdomain-based routing). Configure it in multitenancy.php:

    'tenant_finder' => \Spatie\Multitenancy\TenantFinder\DomainTenantFinder::class,
    
  4. First Use Case: Access the current tenant in a route or controller:

    use Spatie\Multitenancy\Contracts\IsTenant;
    
    route('tenant-data', function () {
        $tenant = app(IsTenant::class)->current();
        return $tenant ? "Current tenant: {$tenant->name}" : "No tenant active";
    });
    

Implementation Patterns

Tenant Resolution Workflow

  1. Request Handling: The package automatically resolves the tenant at the start of each request using the configured tenant_finder. Example with subdomains:

    // Tenant model must have a `domain` column (e.g., `tenant1.example.com`)
    Tenant::where('domain', $request->getHost())->first();
    
  2. Middleware Integration: Use Spatie\Multitenancy\Middleware\ResolveTenant in your HTTP kernel to ensure tenant resolution:

    protected $middlewareGroups = [
        'web' => [
            // ...
            \Spatie\Multitenancy\Middleware\ResolveTenant::class,
        ],
    ];
    
  3. Database Switching: Configure switch_tenant_tasks in multitenancy.php to switch database connections dynamically:

    'switch_tenant_tasks' => [
        \Spatie\Multitenancy\Tasks\SwitchDatabaseTask::class,
    ],
    

    Ensure your Tenant model has a database_connection attribute or method.

  4. Model Scoping: Use Spatie\Multitenancy\HasDatabaseConnections trait to scope models to the tenant’s database:

    use Spatie\Multitenancy\HasDatabaseConnections;
    
    class User extends Model
    {
        use HasDatabaseConnections;
    }
    
  5. Artisan Commands: Run commands for all tenants using Spatie\Multitenancy\Commands\TenantCommand:

    php artisan tenant:migrate
    php artisan tenant:db:seed --tenant=1
    

Tenant-Aware Jobs

  1. Dispatching Jobs: Dispatch tenant-aware jobs by implementing TenantAware or using the tenant() helper:

    use Spatie\Multitenancy\Jobs\TenantAware;
    
    class SendWelcomeEmail implements ShouldQueue, TenantAware
    {
        public function handle()
        {
            // Automatically uses the tenant from the job's dispatch context
            // OR explicitly set tenant for retrying jobs (4.1.3+)
            $tenant = Tenant::current();
        }
    }
    
  2. Retrying Jobs with Payload Context (4.1.3+): When retrying jobs, the tenant is now resolved from the payload context (e.g., payload property of the job). This ensures jobs retain their tenant context after failures:

    // Example job with payload context
    class ProcessOrder implements ShouldQueue
    {
        public $tenantId;
        public $orderData;
    
        public function __construct($tenantId, $orderData)
        {
            $this->tenantId = $tenantId;
            $this->orderData = $orderData;
        }
    
        public function handle()
        {
            Tenant::tenant($this->tenantId, function () use ($orderData) {
                // Process order in tenant's context
            });
        }
    }
    
  3. Testing Jobs: Use TenantTestCase to test job behavior with specific tenants:

    public function test_tenant_aware_job()
    {
        $tenant = Tenant::factory()->create();
        SendWelcomeEmail::dispatch()->onQueue('tenant-queue');
    
        $this->assertDatabaseHas('jobs', [
            'payload' => json_contains(['tenant_id' => $tenant->id]),
        ]);
    }
    

Gotchas and Tips

Pitfalls

  1. Database Connection Leaks:

    • Issue: Forgetting to switch back to the default connection after tenant operations can cause queries to fail.
    • Fix: Use app('db')->connection() to verify the active connection or wrap operations in a tenant() helper:
      Tenant::tenant($tenant, function () {
          // Operations here use the tenant's database
      });
      
  2. Job Tenant Resolution (4.1.3+):

    • Issue: Jobs retrying without explicit tenant context may fail if the payload context isn't properly set.
    • Fix: Ensure your job includes the tenant ID in the payload or implements TenantAware:
      // For custom jobs, explicitly set tenant context
      Tenant::tenant($tenantId, function () {
          ProcessOrder::dispatch($tenantId, $orderData)->onQueue('tenant-queue');
      });
      
  3. Circular Dependencies:

    • Issue: Tenant models referencing each other can cause infinite loops during serialization.
    • Fix: Explicitly define relationships and use with() to eager-load:
      $tenant = Tenant::with('users')->find($id);
      
  4. Middleware Order:

    • Issue: ResolveTenant must run before other middleware that relies on tenant context (e.g., auth).
    • Fix: Place it early in the $middleware stack or group it with other tenant-aware middleware.
  5. Configuration Overrides:

    • Issue: Overriding tenant_finder or switch_tenant_tasks dynamically can lead to inconsistent behavior.
    • Fix: Use environment-based configuration or middleware to conditionally apply settings.

Debugging Tips

  1. Log Tenant Context: Add a middleware to log the current tenant for debugging:

    class LogTenantMiddleware
    {
        public function handle($request, Closure $next)
        {
            \Log::debug('Current tenant:', ['tenant' => app(IsTenant::class)->current()]);
            return $next($request);
        }
    }
    
  2. Verify Job Payloads: Check job payloads for tenant context in the jobs table:

    php artisan tinker
    >>> \DB::table('jobs')->where('payload', 'like', '%tenant_id%')->get();
    
  3. Test Tenant Isolation: Use Spatie\Multitenancy\Tests\TenantTestCase to ensure data isolation:

    public function test_tenant_isolation()
    {
        $tenant1 = Tenant::factory()->create();
        $tenant2 = Tenant::factory()->create();
    
        Tenant::tenant($tenant1, function () {
            $user1 = User::create(['name' => 'User 1']);
            $this->assertDatabaseHas('users', ['name' => 'User 1']);
        });
    
        Tenant::tenant($tenant2, function () {
            $this->assertDatabaseMissing('users', ['name' => 'User 1']);
        });
    }
    

Extension Points

  1. Custom Tenant Finders: Extend TenantFinder for complex resolution logic (e.g., API keys, JWT tokens):

    class ApiKeyTenantFinder extends TenantFinder
    {
        public function findForRequest(Request $request): ?IsTenant
        {
            $apiKey = $request->bearerToken();
            return Tenant::where('api_key', $apiKey)->first();
        }
    }
    
  2. Dynamic Switch Tasks: Add logic to SwitchDatabaseTask for custom connection handling:

    class CustomSwitchDatabaseTask implements SwitchTenantTask
    {
        public function makeCurrent(IsTenant $tenant): void
        {
            if ($tenant->uses_custom_connection) {
                \DB::purge('custom');
                \DB::connection('custom');
            }
        }
    }
    
  3. Tenant-Specific Middleware: Register middleware conditionally based on the tenant:

    $tenant = app(IsTenant::class)->current();
    if ($tenant && $tenant->requires
    
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
hexters/coinpayment
rjcodes/rjcms
act-training/laravel-permissions-manager
alimarchal/laravel-chart-of-accounts
babenkoivan/elastic-scout-driver
mkwebdesign/filament-watchdog-v5
renatomarinho/laravel-page-speed
zedmagdy/filament-business-hours
renatovdemoura/blade-elements-ui
devgeek/beacon-admin
benjamin-rqt/data-watcher-bundle
atriumphp/atrium
sandermuller/package-boost-laravel
sandermuller/boost-skills
redaxo/core
yusufgenc/filament-api-forge
l3aro/rating-star-for-filament
leek/filament-subtenant-scope
anil/file-picker
broqit/fields-ai