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

Tenancy Laravel Package

stancl/tenancy

Automatic multi-tenancy for Laravel apps with minimal code changes. Provides tenant identification (e.g., by hostname/subdomains), isolated tenant bootstrapping, and tenancy-aware database/config switching without swapping core Laravel classes or adding model traits.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation:

    composer require stancl/tenancy
    php artisan vendor:publish --provider="Stancl\Tenancy\TenancyServiceProvider"
    

    This publishes the tenancy.php config and migrations.

  2. Run migrations:

    php artisan migrate
    

    This creates the tenants table and any required database structures.

  3. First Tenant Creation:

    php artisan tenant:create your-tenant-name --domain=tenant.example.com
    

    This creates a tenant with a subdomain and initializes its database.

  4. Test Tenant Access: Configure your Hosts file to point tenant.example.com to localhost. Visit https://tenant.example.com—the app should automatically load the tenant’s context.

First Use Case: Domain-Based Tenant Resolution

// In routes/web.php
Route::get('/', function () {
    // Automatically resolves tenant based on hostname
    return "Welcome to " . Tenant::find()->domain;
});

Implementation Patterns

Core Workflows

1. Tenant Identification

  • Default: Uses subdomains (e.g., tenant.example.com).
  • Custom Resolvers: Override TenantResolver for alternative logic (e.g., path-based, header-based).
    // app/Providers/TenantServiceProvider.php
    public function boot()
    {
        Tenant::resolver(function ($request) {
            return Tenant::where('identifier', $request->header('X-Tenant-ID'))->first();
        });
    }
    

2. Middleware Integration

  • Automatic Tenant Bootstrapping:
    // app/Http/Kernel.php
    protected $middlewareGroups = [
        'web' => [
            \Stancl\Tenancy\Middleware\InitializeTenancyByDomain::class,
            // Other middleware...
        ],
    ];
    
    • Universal Routes: Use Route::universal() for routes that should bypass tenant resolution (e.g., admin panel).
      Route::universal('/admin', function () {
          // No tenant context
      });
      

3. Database Operations

  • Model Queries: Automatically scoped to the current tenant.
    $posts = Post::all(); // Queries tenant's database
    
  • Central Database Access: Use CentralDatabase facade for shared tables (e.g., tenants).
    use Stancl\Tenancy\Database\CentralDatabase;
    
    $tenant = CentralDatabase::table('tenants')->where('domain', 'tenant.example.com')->first();
    

4. Queue Jobs

  • Job Tenancy: Jobs inherit the tenant context of the request that dispatched them.
    // In a controller
    SendWelcomeEmail::dispatch($user); // Runs in tenant context
    
    // In the job
    public function handle()
    {
        $this->user->sendEmail(); // Uses tenant's database
    }
    
  • Testing Queues: Use QueueTenancyBootstrapper for testing.
    $this->artisan('queue:work', [
        '--once' => true,
        '--tenant' => 'tenant-id',
    ]);
    

5. Filesystem and Storage

  • Tenant-Specific Storage: Use tenant() helper or TenantStorage facade.
    use Stancl\Tenancy\Facades\TenantStorage;
    
    $path = TenantStorage::disk('public')->path('file.txt');
    
  • Vite Assets: Configure tenant-specific asset paths.
    // vite.config.js
    import { defineConfig } from 'vite';
    import laravel from 'laravel-vite-plugin';
    import tenancy from 'laravel-tenancy/vite';
    
    export default defineConfig({
        plugins: [
            laravel({
                input: ['resources/sass/app.scss'],
                refresh: true,
            }),
            tenancy({
                assetPathResolver: (tenant) => `/assets/${tenant->identifier}/`,
            }),
        ],
    });
    

6. Seeding and Migrations

  • Tenant-Specific Migrations:
    php artisan tenant:migrate your-tenant-name
    
  • Central Database Seeding: Use --central flag for shared tables.
    php artisan db:seed --class=CentralDatabaseSeeder --central
    

7. Impersonation

  • Switch Tenants Temporarily:
    Tenant::impersonate($tenantId, function () {
        // Code runs in the context of $tenantId
        $posts = Post::all();
    });
    

Integration Tips

Laravel Ecosystem

  • Laravel Scout: Tenant-aware search.
    $tenant->searchable()->addIndex('posts');
    
  • Laravel Cashier: Tenant-specific subscriptions.
    $user->newSubscription('monthly', $plan)->create($stripeToken);
    
  • Laravel Notifications: Tenant-scoped notifications.
    Notification::route('mail', $user->email)->notify(new WelcomeNotification());
    

Testing

  • Tenant Context in Tests:
    public function testAsTenant()
    {
        Tenant::actingAs($tenantId, function () {
            $response = $this->get('/');
            $response->assertSee('Tenant Content');
        });
    }
    
  • Central Database Tests:
    public function testCentralDatabase()
    {
        Tenant::actingAsCentral(function () {
            $tenant = Tenant::create(['domain' => 'test.example.com']);
            $this->assertDatabaseHas('tenants', ['domain' => 'test.example.com']);
        });
    }
    

Performance

  • Cache Tenant Resolution:
    // tenancy.php
    'resolver_cache' => true,
    'resolver_cache_ttl' => 60, // Cache for 60 seconds
    
  • Database Connection Pooling: Use pgsql or mysql with connection pooling (e.g., pgbouncer).

Gotchas and Tips

Pitfalls

1. Queue Job Tenancy Context

  • Issue: Jobs may fail if they rely on tenant-specific models before the context is set.
    // ❌ Avoid this in jobs
    public function handle()
    {
        $user = User::find($this->userId); // May fail if tenant context isn't set
    }
    
  • Solution: Use find() or first() to ensure the tenant context is resolved.
    // ✅ Correct approach
    public function handle()
    {
        $user = User::where('id', $this->userId)->first();
    }
    

2. Middleware Order Matters

  • Issue: Placing InitializeTenancyByDomain after authentication middleware can cause tenant resolution to fail if the user isn’t authenticated.
  • Solution: Ensure InitializeTenancyByDomain is the first middleware in the web group.

3. Vite Asset Paths

  • Issue: Tenant-specific Vite assets may not compile correctly if the assetPathResolver isn’t configured.
  • Solution: Use the tenancy() Vite plugin and ensure the resolver returns the correct path.
    // vite.config.js
    tenancy({
        assetPathResolver: (tenant) => `/build/${tenant.identifier}/`,
    }),
    

4. Central Database Queries

  • Issue: Forgetting to use CentralDatabase for shared tables can lead to SQLSTATE[HY000]: General error.
  • Solution: Always prefix central queries with CentralDatabase::.
    // ❌ Wrong
    $tenants = Tenant::all(); // Uses tenant's database
    
    // ✅ Correct
    $tenants = CentralDatabase::table('tenants')->get();
    

5. Seeding in Production

  • Issue: Running php artisan db:seed in production may overwrite tenant data.
  • Solution: Use --force carefully and prefer --central for shared tables.
    php artisan db:seed --class=CentralDatabaseSeeder --central --force
    

6. Tenant Deletion

  • Issue: Deleting a tenant may leave orphaned database connections or cache entries.
  • Solution: Use the tenant:delete command and ensure all related data is cleaned up.
    php artisan tenant:delete your-tenant-name --force
    

7. Path-Based Tenancy

  • Issue: Path-based resolvers may conflict with Laravel’s route model binding.
  • Solution: Use Route::universal() for paths that shouldn’t resolve tenants.

Debugging Tips

1. Check Tenant Context

  • Use the tenant() helper to inspect the current tenant.
    dd(Tenant::find()); // Debug current tenant
    

2. Log Tenant Resolution

  • T
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.
davejamesmiller/laravel-breadcrumbs
artisanry/parsedown
christhompsontldr/phpsdk
enqueue/dsn
bunny/bunny
enqueue/test
enqueue/null
enqueue/amqp-tools
milesj/emojibase
bower-asset/punycode
bower-asset/inputmask
bower-asset/jquery
bower-asset/yii2-pjax
laravel/nova
spatie/laravel-mailcoach
spatie/laravel-superseeder
laravel/liferaft
nst/json-test-suite
danielmiessler/sec-lists
jackalope/jackalope-transport