<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Laravel With Rohit]]></title><description><![CDATA[Laravel With Rohit]]></description><link>https://laravel-with-rohit.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Laravel With Rohit</title><link>https://laravel-with-rohit.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 10 Sep 2026 15:08:02 GMT</lastBuildDate><atom:link href="https://laravel-with-rohit.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Multi-Database Multi-Tenancy with Spatie in Laravel]]></title><description><![CDATA[A practical guide for developers setting up per-tenant databases using spatie/laravel-multitenancy.

Table of Contents

What is Multi-Tenancy?

Multi-Tenancy Strategies

Use Cases

Overview

Installat]]></description><link>https://laravel-with-rohit.hashnode.dev/multi-database-multi-tenancy-with-spatie-in-laravel</link><guid isPermaLink="true">https://laravel-with-rohit.hashnode.dev/multi-database-multi-tenancy-with-spatie-in-laravel</guid><category><![CDATA[Laravel]]></category><category><![CDATA[laravel spatie]]></category><category><![CDATA[multitenant]]></category><category><![CDATA[#multitenancy]]></category><dc:creator><![CDATA[Rohit Shakya]]></dc:creator><pubDate>Tue, 31 Mar 2026 06:02:33 GMT</pubDate><content:encoded><![CDATA[<p>A practical guide for developers setting up per-tenant databases using <code>spatie/laravel-multitenancy</code>.</p>
<hr />
<h2>Table of Contents</h2>
<ol>
<li><p><a href="#what-is-multi-tenancy">What is Multi-Tenancy?</a></p>
</li>
<li><p><a href="#multi-tenancy-strategies">Multi-Tenancy Strategies</a></p>
</li>
<li><p><a href="#use-cases">Use Cases</a></p>
</li>
<li><p><a href="#overview">Overview</a></p>
</li>
<li><p><a href="#installation">Installation</a></p>
</li>
<li><p><a href="#database-structure">Database Structure</a></p>
</li>
<li><p><a href="#migrations-setup">Migrations Setup</a></p>
</li>
<li><p><a href="#database-configuration">Database Configuration</a></p>
</li>
<li><p><a href="#running-landlord-migrations">Running Landlord Migrations</a></p>
</li>
<li><p><a href="#creating-tenants">Creating Tenants</a></p>
</li>
<li><p><a href="#running-tenant-migrations">Running Tenant Migrations</a></p>
</li>
<li><p><a href="#configuring-multitenancyphp">Configuring multitenancy.php</a></p>
</li>
<li><p><a href="#tenant-finder">Tenant Finder</a></p>
</li>
<li><p><a href="#switch-tenant-tasks">Switch Tenant Tasks</a></p>
</li>
<li><p><a href="#tenant-asset-handling">Tenant Asset Handling</a></p>
</li>
<li><p><a href="#virtual-host-setup-nginx">Virtual Host Setup (Nginx)</a></p>
</li>
<li><p><a href="#tips--gotchas">Tips &amp; Gotchas</a></p>
</li>
</ol>
<hr />
<h2>What is Multi-Tenancy?</h2>
<p>Multi-tenancy is a software architecture where a <strong>single application instance serves multiple independent customers</strong> (tenants). Each tenant believes they have a dedicated product — their own data, their own configuration, sometimes their own branding — but under the hood, they all run on the same codebase and infrastructure.</p>
<p>Think of it like an apartment building: one building, many separate units. Residents share the structure, the plumbing, and the electricity, but each unit is private. Nobody walks into their neighbour's flat.</p>
<p>In web applications, a <strong>tenant</strong> is typically an organisation, a company, or a distinct customer account. The <strong>landlord</strong> is the platform itself — the part that manages tenants, billing, and global configuration.</p>
<pre><code class="language-plaintext">                  ┌─────────────────────────────────────┐
                  │       Your Laravel Application       │
                  │          (Single Codebase)           │
                  └──────────┬──────────────┬────────────┘
                             │              │
               ┌─────────────▼──┐      ┌───▼─────────────┐
               │   Tenant A     │      │   Tenant B       │
               │  acme.app.com  │      │  globex.app.com  │
               │  [own DB]      │      │  [own DB]        │
               └────────────────┘      └──────────────────┘
</code></pre>
<hr />
<h2>Multi-Tenancy Strategies</h2>
<p>There are three common approaches, each with different trade-offs:</p>
<table>
<thead>
<tr>
<th>Strategy</th>
<th>Description</th>
<th>Isolation</th>
<th>Complexity</th>
<th>Cost</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Single database, shared tables</strong></td>
<td>All tenants share the same tables. A <code>tenant_id</code> column filters rows.</td>
<td>Low</td>
<td>Low</td>
<td>Cheapest</td>
</tr>
<tr>
<td><strong>Single database, separate schemas</strong></td>
<td>One DB, but each tenant gets their own schema (namespace).</td>
<td>Medium</td>
<td>Medium</td>
<td>Moderate</td>
</tr>
<tr>
<td><strong>Separate database per tenant</strong></td>
<td>Every tenant has their own database.</td>
<td>High</td>
<td>Higher</td>
<td>Higher</td>
</tr>
</tbody></table>
<p><strong>This guide uses the separate database strategy.</strong> It offers the strongest data isolation, makes it trivial to backup or restore a single tenant, and lets you place tenants on different database servers if needed. The trade-off is slightly more operational overhead when onboarding new tenants.</p>
<hr />
<h2>Use Cases</h2>
<p>Multi-tenancy is the backbone of most SaaS products. You might need it if you're building:</p>
<ul>
<li><p><strong>HR or payroll platforms</strong> — each company's employee data must be completely isolated from others</p>
</li>
<li><p><strong>Project management tools</strong> — think Jira, Asana, Linear; each organisation manages their own projects and users</p>
</li>
<li><p><strong>CRM systems</strong> — customer and sales data is sensitive and must never leak between organisations</p>
</li>
<li><p><strong>E-commerce platforms</strong> — each store owner manages their own products, orders, and customers</p>
</li>
<li><p><strong>School or university management systems</strong> — each institution has its own students, courses, and grades</p>
</li>
<li><p><strong>Accounting or invoicing software</strong> — financial data requires strict isolation for compliance reasons</p>
</li>
<li><p><strong>Helpdesk / ticketing systems</strong> — each company's support tickets and agents are private</p>
</li>
<li><p><strong>White-label SaaS</strong> — you build one product, and other businesses run it under their own brand and domain</p>
</li>
</ul>
<p>The common thread: <strong>you are building one product that many independent organisations pay to use</strong>, and those organisations must not see each other's data.</p>
<hr />
<h2>Overview</h2>
<p>This guide walks through a <strong>multiple database</strong> multi-tenancy setup using <a href="https://github.com/spatie/laravel-multitenancy">spatie/laravel-multitenancy</a>. The architecture is:</p>
<ul>
<li><p><strong>One landlord database</strong> — stores tenant records (domain, credentials, etc.)</p>
</li>
<li><p><strong>One database per tenant</strong> — fully isolated data per tenant</p>
</li>
</ul>
<p>Each incoming request is matched to a tenant by its domain. The app then dynamically switches the database connection, log channel, and storage paths for that tenant.</p>
<hr />
<h2>Installation</h2>
<pre><code class="language-bash">composer require spatie/laravel-multitenancy
</code></pre>
<p>Publish the config and migrations:</p>
<pre><code class="language-bash">php artisan vendor:publish --provider="Spatie\Multitenancy\MultitenancyServiceProvider" --tag="multitenancy-migrations"
php artisan vendor:publish --provider="Spatie\Multitenancy\MultitenancyServiceProvider" --tag="multitenancy-config"
</code></pre>
<hr />
<h2>Database Structure</h2>
<p>Organise your migrations into two folders to keep landlord and tenant migrations separate:</p>
<pre><code class="language-plaintext">database/
  migrations/
    landlord/       ← landlord-only migrations (tenants table, etc.)
    tenant/         ← per-tenant migrations (your app tables)
</code></pre>
<p>This makes it much cleaner to migrate each side independently.</p>
<hr />
<h2>Migrations Setup</h2>
<p>After publishing, move the generated <code>create_tenants_table</code> migration into <code>database/migrations/landlord/</code>. Then update it to include the fields your tenants need:</p>
<pre><code class="language-php">&lt;?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('tenants', function (Blueprint $table) {
            $table-&gt;id();

            $table-&gt;string('name');
            $table-&gt;string('domain')-&gt;unique();
            $table-&gt;string('database');

            $table-&gt;string('db_driver');
            $table-&gt;string('db_host');
            $table-&gt;unsignedInteger('db_port');
            $table-&gt;string('db_username');
            $table-&gt;text('db_password');   // store encrypted (recommended)
            $table-&gt;string('db_charset');
            $table-&gt;string('db_collation');

            $table-&gt;timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('tenants');
    }
};
</code></pre>
<blockquote>
<p><strong>Why remove</strong> <code>nullable()</code><strong>?</strong> Every tenant should have full database credentials. Making them non-nullable enforces data integrity and prevents silent connection failures caused by missing credentials.</p>
</blockquote>
<hr />
<h2>Database Configuration</h2>
<p>Add two named connections to <code>config/database.php</code>: one for the landlord database and a template for tenant connections.</p>
<pre><code class="language-php">'landlord' =&gt; [
    'driver'    =&gt; 'mysql',
    'url'       =&gt; env('DATABASE_URL'),
    'host'      =&gt; env('DB_HOST', '127.0.0.1'),
    'port'      =&gt; env('DB_PORT', '3306'),
    'database'  =&gt; env('DB_DATABASE', 'forge'),
    'username'  =&gt; env('DB_USERNAME', 'forge'),
    'password'  =&gt; env('DB_PASSWORD', ''),
    'unix_socket' =&gt; env('DB_SOCKET', ''),
    'charset'   =&gt; 'utf8mb4',
    'collation' =&gt; 'utf8mb4_unicode_ci',
    'prefix'    =&gt; '',
    'prefix_indexes' =&gt; true,
    'strict'    =&gt; true,
    'engine'    =&gt; null,
    'options'   =&gt; extension_loaded('pdo_mysql') ? array_filter([
        (PHP_VERSION_ID &gt;= 80500
            ? \Pdo\Mysql::ATTR_SSL_CA
            : \PDO::MYSQL_ATTR_SSL_CA) =&gt; env('MYSQL_ATTR_SSL_CA'),
    ]) : [],
],

'tenant' =&gt; [
    'driver'    =&gt; 'mysql',
    'host'      =&gt; env('DB_HOST'),
    'port'      =&gt; env('DB_PORT', 3306),
    'database'  =&gt; null,   // ← filled dynamically at runtime
    'username'  =&gt; env('DB_USERNAME'),
    'password'  =&gt; env('DB_PASSWORD'),
    'charset'   =&gt; 'utf8mb4',
    'collation' =&gt; 'utf8mb4_unicode_ci',
],
</code></pre>
<p>The <code>tenant</code> connection has <code>database: null</code> intentionally — it acts as a <strong>template</strong>. At runtime, <code>SwitchTenantDatabaseTask</code> copies this config and fills in the actual values from the tenant record.</p>
<hr />
<h2>Running Landlord Migrations</h2>
<pre><code class="language-bash">php artisan migrate --path=database/migrations/landlord --database=landlord
</code></pre>
<p>This creates the <code>tenants</code> table in your landlord database only.</p>
<hr />
<h2>Creating Tenants</h2>
<p>You can create tenants directly in the database or via Tinker:</p>
<pre><code class="language-bash">php artisan tinker
</code></pre>
<pre><code class="language-php">Tenant::create([
    'name'         =&gt; 'acme',
    'domain'       =&gt; 'acme.yourdomain.com',
    'database'     =&gt; 'acme_db',
    'db_driver'    =&gt; 'mysql',
    'db_host'      =&gt; '127.0.0.1',   // see note below
    'db_port'      =&gt; 3306,
    'db_username'  =&gt; 'root',
    'db_password'  =&gt; 'your_password',
    'db_charset'   =&gt; 'utf8mb4',
    'db_collation' =&gt; 'utf8mb4_unicode_ci',
]);
</code></pre>
<blockquote>
<p><strong>Note on</strong> <code>db_host</code> <strong>in Docker:</strong> If your app is running inside a Docker container, <code>127.0.0.1</code> refers to the container itself — not your host machine or the database container. Use the <strong>Docker service name</strong> (e.g. <code>mysql</code>, <code>db</code>) defined in your <code>docker-compose.yml</code> instead. For example, if your database service is named <code>mysql</code>, use <code>db_host: 'mysql'</code>.</p>
</blockquote>
<blockquote>
<p><strong>Note on</strong> <code>domain</code> <strong>for localhost:</strong> If you're running on <code>localhost:8080</code>, set the domain as <code>localhost</code> or use a subdomain like <code>app.localhost</code>. Note that assets may not resolve correctly on non-standard ports — see the <a href="#virtual-host-setup-nginx">Virtual Host Setup</a> section.</p>
</blockquote>
<hr />
<h2>Running Tenant Migrations</h2>
<p>Run migrations for <strong>all</strong> tenants at once:</p>
<pre><code class="language-bash">php artisan tenants:artisan "migrate --database=tenant"
</code></pre>
<p>If your tenant migrations are in a specific folder (as set up in this guide):</p>
<pre><code class="language-bash">php artisan tenants:artisan "migrate --path=database/migrations/tenant --database=tenant"
</code></pre>
<p>Run migrations for a <strong>single</strong> tenant by ID:</p>
<pre><code class="language-bash">php artisan tenants:artisan "migrate --path=database/migrations/tenant --database=tenant" --tenant=1
</code></pre>
<p>The <code>tenants:artisan</code> command loops through all tenants (or the specified one), makes each tenant current, and runs the Artisan command in that tenant's context.</p>
<hr />
<h2>Configuring multitenancy.php</h2>
<p>Below is a full annotated <code>config/multitenancy.php</code>:</p>
<pre><code class="language-php">return [
    /*
     |--------------------------------------------------------------------------
     | Tenant Finder
     |--------------------------------------------------------------------------
     | Resolves which tenant owns the current request.
     | Your custom finder should extend TenantFinder.
     */
    'tenant_finder' =&gt; \App\Multitenancy\TenantFinder\DomainTenantFinder::class,

    /*
     | Fields used by `tenants:artisan` to match a tenant (e.g. --tenant=1).
     */
    'tenant_artisan_search_fields' =&gt; ['id'],

    /*
     |--------------------------------------------------------------------------
     | Switch Tenant Tasks
     |--------------------------------------------------------------------------
     | These tasks run in order whenever a tenant becomes "current".
     | Each must implement SwitchTenantTask.
     */
    'switch_tenant_tasks' =&gt; [
        \Spatie\Multitenancy\Tasks\PrefixCacheTask::class,           // prefixes cache keys
        \App\Multitenancy\Tasks\SwitchTenantDatabaseTask::class,     // switches DB connection
        \Spatie\Multitenancy\Tasks\SwitchRouteCacheTask::class,      // scopes route cache
        \App\Multitenancy\Tasks\SwitchTenantLogTask::class,          // per-tenant log files
        \App\Multitenancy\Tasks\SwitchTenantStorageTask::class,      // per-tenant file storage
    ],

    'tenant_model' =&gt; \Spatie\Multitenancy\Models\Tenant::class,

    /*
     | When true, queued jobs automatically carry the current tenant's ID
     | and restore that tenant when the job executes.
     */
    'queues_are_tenant_aware_by_default' =&gt; true,

    'tenant_database_connection_name'  =&gt; 'tenant',
    'landlord_database_connection_name' =&gt; 'landlord',

    'current_tenant_context_key'   =&gt; 'tenantId',
    'current_tenant_container_key' =&gt; 'currentTenant',

    'shared_routes_cache' =&gt; false,

    'actions' =&gt; [
        'make_tenant_current_action'    =&gt; \Spatie\Multitenancy\Actions\MakeTenantCurrentAction::class,
        'forget_current_tenant_action'  =&gt; \Spatie\Multitenancy\Actions\ForgetCurrentTenantAction::class,
        'make_queue_tenant_aware_action' =&gt; \Spatie\Multitenancy\Actions\MakeQueueTenantAwareAction::class,
        'migrate_tenant'                =&gt; \Spatie\Multitenancy\Actions\MigrateTenantAction::class,
    ],

    'queueable_to_job' =&gt; [
        \Illuminate\Mail\SendQueuedMailable::class          =&gt; 'mailable',
        \Illuminate\Notifications\SendQueuedNotifications::class =&gt; 'notification',
        \Illuminate\Queue\CallQueuedClosure::class          =&gt; 'closure',
        \Illuminate\Events\CallQueuedListener::class        =&gt; 'class',
        \Illuminate\Broadcasting\BroadcastEvent::class      =&gt; 'event',
    ],

    'tenant_aware_interface'     =&gt; \Spatie\Multitenancy\Jobs\TenantAware::class,
    'not_tenant_aware_interface' =&gt; \Spatie\Multitenancy\Jobs\NotTenantAware::class,

    'tenant_aware_jobs'     =&gt; [],
    'not_tenant_aware_jobs' =&gt; [],
];
</code></pre>
<hr />
<h2>Tenant Finder</h2>
<p>The <code>TenantFinder</code> is called on every request to resolve <em>which</em> tenant is active.</p>
<pre><code class="language-php">&lt;?php

namespace App\Multitenancy\TenantFinder;

use Illuminate\Http\Request;
use Spatie\Multitenancy\Models\Tenant;
use Spatie\Multitenancy\TenantFinder\TenantFinder;

class DomainTenantFinder extends TenantFinder
{
    public function findForRequest(Request $request): ?Tenant
    {
        // getHost() strips the port number automatically
        \(host = \)request-&gt;getHost();

        // In production, bypass landlord/admin domains — they don't belong to a tenant
        if (env('APP_ENV') === 'production') {
            if (in_array($host, ['localhost', 'landlord.yourdomain.com'], true)) {
                return null;
            }
        }

        return Tenant::query()-&gt;where('domain', $host)-&gt;first();
    }
}
</code></pre>
<p><strong>How it works:</strong></p>
<ol>
<li><p><code>$request-&gt;getHost()</code> extracts just the hostname (no port). For <code>tenant1.example.com:8080</code> it returns <code>tenant1.example.com</code>.</p>
</li>
<li><p>In production, certain hostnames (admin panels, the landlord app) are excluded — returning <code>null</code> means no tenant context is set, so the app behaves as the landlord/default app.</p>
</li>
<li><p>A simple <code>WHERE domain = ?</code> query finds the matching tenant record. If none is found, <code>null</code> is returned and Spatie will leave the app in "no tenant" mode.</p>
</li>
</ol>
<hr />
<h2>Switch Tenant Tasks</h2>
<p>Tasks are classes that run (in order) when a tenant becomes current, and again when a tenant is forgotten (i.e., at the end of a request or job). They are the heart of the isolation mechanism.</p>
<h3>SwitchTenantDatabaseTask</h3>
<p>This is the most critical task — it physically switches the active database connection.</p>
<pre><code class="language-php">&lt;?php

namespace App\Multitenancy\Tasks;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Config;
use Spatie\Multitenancy\Contracts\IsTenant;
use Spatie\Multitenancy\Tasks\SwitchTenantTask;

class SwitchTenantDatabaseTask implements SwitchTenantTask
{
    public function makeCurrent(IsTenant $tenant): void
    {
        $connectionName = config('multitenancy.tenant_database_connection_name', 'tenant');

        // Copy the template connection config from database.php
        \(base = Config::get("database.connections.{\)connectionName}", []);

        // Override with tenant-specific values
        \(new = \)base;
        \(new['database']   = \)tenant-&gt;database;
        \(new['driver']     = \)tenant-&gt;db_driver    ?: ($base['driver']    ?? 'mysql');
        \(new['host']       = \)tenant-&gt;db_host      ?: ($base['host']      ?? null);
        \(new['port']       = \)tenant-&gt;db_port      ?: ($base['port']      ?? null);
        \(new['username']   = \)tenant-&gt;db_username  ?: ($base['username']  ?? null);
        \(new['password']   = \)tenant-&gt;db_password  ?: ($base['password']  ?? null);
        \(new['charset']    = \)tenant-&gt;db_charset   ?: ($base['charset']   ?? null);
        \(new['collation']  = \)tenant-&gt;db_collation ?: ($base['collation'] ?? null);

        // Apply new config, purge old connection, open a fresh one
        Config::set("database.connections.{\(connectionName}", \)new);
        DB::purge($connectionName);
        DB::reconnect($connectionName);

        // Make this the default connection so models don't need explicit connection names
        DB::setDefaultConnection($connectionName);
    }

    public function forgetCurrent(): void
    {
        $connectionName = config('multitenancy.tenant_database_connection_name', 'tenant');
        DB::purge($connectionName);

        // Revert to landlord as the default
        $landlord = config('multitenancy.landlord_database_connection_name', 'landlord');
        DB::setDefaultConnection($landlord);
    }
}
</code></pre>
<p><strong>How it works:</strong></p>
<ul>
<li><p><code>makeCurrent</code> takes the <code>tenant</code> connection from <code>config/database.php</code> as a base template, then patches it with the current tenant's credentials. It then calls <code>DB::purge()</code> to discard any cached connection, and <code>DB::reconnect()</code> to open a fresh one.</p>
</li>
<li><p>Setting <code>DB::setDefaultConnection()</code> means all Eloquent models that don't specify a connection will automatically use the tenant database.</p>
</li>
<li><p><code>forgetCurrent</code> tears down the tenant connection and hands the default back to the landlord — preventing data leaking between requests.</p>
</li>
</ul>
<hr />
<h3>SwitchTenantLogTask</h3>
<p>Isolates logs so each tenant's activity is written to its own log file.</p>
<pre><code class="language-php">&lt;?php

namespace App\Multitenancy\Tasks;

use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Spatie\Multitenancy\Contracts\IsTenant;
use Spatie\Multitenancy\Tasks\SwitchTenantTask;

class SwitchTenantLogTask implements SwitchTenantTask
{
    public function makeCurrent(IsTenant $tenant): void
    {
        \(tenantKey = (string) \)tenant-&gt;name; // or domain, id, uuid — your choice

        \(dir = storage_path("logs/tenants/{\)tenantKey}");
        File::ensureDirectoryExists($dir);

        Config::set('logging.channels.daily.path', "{$dir}/laravel.log");

        // Force the logger to re-resolve with the new path
        app()-&gt;forgetInstance('log');
        Log::getLogger();
    }

    public function forgetCurrent(): void
    {
        Config::set('logging.channels.daily.path', storage_path('logs/laravel.log'));
        app()-&gt;forgetInstance('log');
        Log::getLogger();
    }
}
</code></pre>
<p><strong>How it works:</strong></p>
<ul>
<li><p>Overrides the <code>daily</code> channel's <code>path</code> to point to <code>storage/logs/tenants/{name}/laravel.log</code>.</p>
</li>
<li><p>After changing config, the logger instance is flushed from the container so the next log write picks up the new path.</p>
</li>
<li><p><code>forgetCurrent</code> restores the default path.</p>
</li>
</ul>
<blockquote>
<p><strong>Tip:</strong> Use <code>\(tenant-&gt;id</code> or <code>\)tenant-&gt;domain</code> as the key if <code>name</code> might contain spaces or characters that are invalid in directory names.</p>
</blockquote>
<hr />
<h3>SwitchTenantStorageTask</h3>
<p>Redirects file storage so uploaded files from different tenants don't collide.</p>
<pre><code class="language-php">&lt;?php

namespace App\Multitenancy\Tasks;

use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\File;
use Spatie\Multitenancy\Contracts\IsTenant;
use Spatie\Multitenancy\Tasks\SwitchTenantTask;

class SwitchTenantStorageTask implements SwitchTenantTask
{
    protected array $original = [];

    public function __construct()
    {
        // Cache the original values so forgetCurrent can restore them
        $this-&gt;original = [
            'public_root' =&gt; config('filesystems.disks.public.root'),
            'public_url'  =&gt; config('filesystems.disks.public.url'),
        ];
    }

    public function makeCurrent(IsTenant $tenant): void
    {
        \(tenantKey = (string) \)tenant-&gt;getKey();

        \(publicRoot = storage_path("app/public/tenants/{\)tenantKey}");
        File::ensureDirectoryExists($publicRoot);

        $scheme = request()?-&gt;getScheme() ?? 'http';
        \(publicUrl = "{\)scheme}://{\(tenant-&gt;domain}/storage/tenants/{\)tenantKey}";

        Config::set('filesystems.disks.public.root', $publicRoot);
        Config::set('filesystems.disks.public.url', $publicUrl);

        // Flush the filesystem manager so it re-resolves with new config
        app()-&gt;forgetInstance('filesystem');
    }

    public function forgetCurrent(): void
    {
        Config::set('filesystems.disks.public.root', $this-&gt;original['public_root']);
        Config::set('filesystems.disks.public.url', $this-&gt;original['public_url']);
        app()-&gt;forgetInstance('filesystem');
    }
}
</code></pre>
<p><strong>How it works:</strong></p>
<ul>
<li><p><strong>Physical path:</strong> Files are stored under <code>storage/app/public/tenants/{id}/</code>. This keeps files from all tenants under the same <code>storage/app/public</code> tree, but isolated in subfolders.</p>
</li>
<li><p><strong>Public URL:</strong> The <code>url</code> is set to <code>https://{tenant.domain}/storage/tenants/{id}</code>, which maps through Laravel's <code>storage:link</code> symlink (<code>public/storage =&gt; storage/app/public</code>).</p>
</li>
<li><p>The filesystem singleton is flushed so the new disk config is picked up immediately.</p>
</li>
</ul>
<hr />
<h2>Tenant Asset Handling</h2>
<p>Add this helper function (e.g., in <code>app/helpers.php</code>):</p>
<pre><code class="language-php">if (! function_exists('tenant_asset')) {
    function tenant_asset(string \(path, string \)disk = 'public'): string
    {
        return \Illuminate\Support\Facades\Storage::disk(\(disk)-&gt;url(\)path);
    }
}
</code></pre>
<p>Register it in your <code>composer.json</code> autoload section:</p>
<pre><code class="language-json">"autoload": {
    "files": [
        "app/helpers.php"
    ]
}
</code></pre>
<p>Then run <code>composer dump-autoload</code>.</p>
<p><strong>Usage in Blade:</strong></p>
<pre><code class="language-blade">&lt;img src="{{ tenant_asset('avatars/profile.jpg') }}" alt="Profile"&gt;
</code></pre>
<p><strong>Why not</strong> <code>asset()</code><strong>?</strong> The standard <code>asset()</code> helper generates URLs relative to the <code>public/</code> folder, with no awareness of tenant context. <code>tenant_asset()</code> uses the <code>Storage</code> disk, which at this point has already been redirected to the tenant's subdirectory by <code>SwitchTenantStorageTask</code>.</p>
<blockquote>
<p><strong>Important:</strong> This only works correctly when the domain is accessed directly (e.g. <code>tenant.yourdomain.com</code>). If you're on <code>localhost:8080</code>, the generated URL will be <code>http://localhost/storage/...</code> — missing the port — and assets will fail to load. See the next section for the fix.</p>
</blockquote>
<hr />
<h2>Virtual Host Setup (Nginx)</h2>
<p>Running on <code>localhost:8080</code> breaks asset URLs because <code>SwitchTenantStorageTask</code> builds the URL from the tenant's domain (e.g. <code>http://app.localhost</code>), not <code>http://localhost:8080</code>. To solve this, set up a local Nginx virtual host that proxies to your dev server.</p>
<h3>Step 1 — Install Nginx (if needed)</h3>
<pre><code class="language-bash">sudo apt update
sudo apt install nginx
sudo systemctl start nginx
sudo systemctl enable nginx
</code></pre>
<h3>Step 2 — Create a virtual host config</h3>
<pre><code class="language-bash">cd /etc/nginx/sites-available
sudo nano app.localhost
</code></pre>
<p>Paste:</p>
<pre><code class="language-nginx">server {
    listen 80;
    server_name app.localhost;

    location / {
        proxy_pass         http://localhost:8080;
        proxy_set_header   Host              $host;
        proxy_set_header   X-Real-IP         $remote_addr;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
    }
}
</code></pre>
<blockquote>
<p>Replace <code>app.localhost</code> with your tenant domain (e.g. <code>tenant1.test</code>) and <code>8080</code> with your dev server port.</p>
</blockquote>
<h3>Step 3 — Enable the site</h3>
<pre><code class="language-bash">sudo ln -s /etc/nginx/sites-available/app.localhost /etc/nginx/sites-enabled/
sudo nginx -t       # verify config
sudo systemctl reload nginx
</code></pre>
<h3>Step 4 — Update /etc/hosts</h3>
<pre><code class="language-bash">sudo nano /etc/hosts
</code></pre>
<p>Add:</p>
<pre><code class="language-plaintext">127.0.0.1   app.localhost
127.0.0.1   tenant1.test
127.0.0.1   tenant2.test
</code></pre>
<blockquote>
<p>Add a line for every tenant domain you're testing locally.</p>
</blockquote>
<p>Now requests to <code>http://app.localhost</code> route through Nginx → your Laravel dev server, and asset URLs generated by <code>tenant_asset()</code> will resolve correctly.</p>
<hr />
<h2>Tips &amp; Gotchas</h2>
<p><strong>Docker hosts</strong> Inside a Docker container, <code>127.0.0.1</code> is the container itself. Use the service name from <code>docker-compose.yml</code> as <code>db_host</code> (e.g. <code>mysql</code> or <code>db</code>).</p>
<p><strong>Encrypt passwords</strong> Use Laravel's <code>encrypted</code> cast on the <code>Tenant</code> model for <code>db_password</code> so it's never stored in plaintext:</p>
<pre><code class="language-php">protected $casts = ['db_password' =&gt; 'encrypted'];
</code></pre>
<p>The <code>SwitchTenantDatabaseTask</code> will receive the already-decrypted value automatically.</p>
<p><strong>Don't forget</strong> <code>storage:link</code> Run <code>php artisan storage:link</code> so <code>public/storage</code> symlinks to <code>storage/app/public</code>. Without this, tenant file URLs return 404.</p>
<p><strong>Tenant-aware queues</strong> With <code>queues_are_tenant_aware_by_default: true</code>, dispatched jobs automatically carry the current tenant ID and restore it when executing. If a specific job should NOT be tenant-aware, implement <code>\Spatie\Multitenancy\Jobs\NotTenantAware</code>.</p>
<p><strong>Seeding per tenant</strong> To run a seeder for a specific tenant:</p>
<pre><code class="language-bash">php artisan tenants:artisan "db:seed --class=TenantSeeder" --tenant=1
</code></pre>
<p><strong>Rolling back tenant migrations</strong></p>
<pre><code class="language-bash">php artisan tenants:artisan "migrate:rollback --path=database/migrations/tenant --database=tenant" --tenant=1
</code></pre>
]]></content:encoded></item></channel></rss>