# Tenancy for Laravel Admin Panels: How Laravilt Finds, Provisions and Isolates Each Tenant

> Tenancy for Laravel comes down to two questions: which tenant is this request for, and where does its data live? How Laravilt answers both, step by step.

*2026-10-25T10:31:27Z*

Tenancy for Laravel comes down to two questions on every request: which tenant is this request for, and where does that tenant's data live? Laravilt answers both inside the panel provider, and which middleware runs depends on the mode you pick.

I've already written about choosing between [single database or one database per tenant](/en/blog/laravel-multi-tenant-admin-panel-laravilt). This post stays on what happens at runtime: how a request finds its tenant, what happens when you create one, and the places where tenant context gets lost.

**The short answer:** in single-database mode the tenant comes from the route (`/admin/{tenant}/...`), and `IdentifyTenant` checks that the signed-in user can open it. In multi-database mode the tenant comes from the subdomain, `InitializeTenancyBySubdomain` switches to that tenant's database, and creating a tenant provisions the database for you. In code, the current tenant comes from the `Laravilt` facade.

## Two modes, two ways to find the tenant

| | Single database | Multi-database |
|---|---|---|
| Tenant comes from | Route parameter | Subdomain |
| Example URL | `/admin/{tenant}/...` | `{tenant}.myapp.com/admin/...` |
| Middleware | `IdentifyTenant` | `InitializeTenancyBySubdomain` |
| Database switch | No | Yes, to the tenant's database |

All the middleware lives in `Laravilt\Panel\Middleware`. A third one, `PreventAccessFromCentralDomains`, blocks tenant-only routes when they're requested on a central domain.

## Single database: the tenant comes from the route

In this mode, [`IdentifyTenant`](/en/docs/panel/tenancy/middleware#identifytenant) does three things: it reads the tenant from the route parameter, checks that the user can access it, and sets the tenant context. There is no database switch.

You turn it on in the panel provider:

```php
use App\Models\Team;
use Laravilt\Panel\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        ->tenant(Team::class, 'team', 'slug')   // model, ownership relationship, slug attribute
        ->tenantRegistration()
        ->tenantProfile();
}
```

The "checks that the user can access it" step is where your `User` model comes in. It has to implement two contracts, and the tenant model implements two more:

| Interface | Methods |
|-----------|---------|
| `HasTenants` (user) | `getTenants(Panel $panel)`, `canAccessTenant(Model $tenant)` |
| `HasDefaultTenant` (user) | `getDefaultTenant(Panel $panel)` |
| `HasTenantName` (tenant) | `getTenantName()` |
| `HasTenantAvatar` (tenant) | `getTenantAvatarUrl()` |

The published `HasTeams` trait supplies the user-side methods, so for a [team-based app](/en/docs/panel/tenancy/teams) you rarely write them by hand. If a user lands on "no teams", the docs give four things to check: the `User` implements `HasTenants` and `HasDefaultTenant`, it uses the `HasTeams` trait, `current_team_id` is fillable, and the `team_user` pivot table exists.

## Multi-database: the tenant comes from the subdomain

Here [`InitializeTenancyBySubdomain`](/en/docs/panel/tenancy/middleware#initializetenancybysubdomain) does five things:

1. Extracts the subdomain from the host.
2. Rejects reserved subdomains.
3. Finds the tenant by domain or slug.
4. Switches to the tenant database.
5. Sets the tenant context.

Enabling it is one call on the panel:

```php
use Laravilt\Panel\Models\Tenant;
use Laravilt\Panel\Panel;

public function panel(Panel $panel): Panel
{
    return $panel->multiDatabaseTenancy(Tenant::class, 'myapp.com');
}
```

The second argument is the base domain. The [config file](/en/docs/panel/tenancy/configuration), `config/laravilt-tenancy.php`, holds the rest. Here are the two parts that matter most for runtime behaviour, trimmed from the published file:

```php
'provisioning' => [
    'auto_create_database' => true,
    'auto_migrate' => true,
    'auto_seed' => false,
    'seeder' => null,
    'queue' => false,
    'queue_name' => 'default',
],

'subdomain' => [
    'domain' => env('APP_DOMAIN', 'localhost'),
    'reserved' => ['www', 'api', 'admin', 'app', /* ... */],
],
```

### What happens when you create a tenant

The `Tenant` model lives in the central database. It uses string (non-incrementing) keys and soft deletes:

```php
use Laravilt\Panel\Models\Tenant;

$tenant = Tenant::create([
    'name' => 'Acme Corp',
    'slug' => 'acme',
    'email' => 'admin@acme.com',
    'owner_id' => $user->id,
]);
```

With the default provisioning config, creating a tenant creates and migrates its database. `php artisan tenant:create` does the same from the command line. For faster sign-ups you can queue it by setting `'queue' => true` and a `queue_name`, so the tenant database is created in the background.

Tenant subdomains are `Domain` records:

```php
use Laravilt\Panel\Models\Domain;

$domain = Domain::createSubdomain(
    tenant: $tenant,
    subdomain: 'acme',
    baseDomain: 'myapp.com',
    isPrimary: true
);

$tenant = Domain::findTenantByDomain('acme.myapp.com');
```

### Reserved subdomains

The config ships with a reserved list (`www`, `api`, `admin`, `app` and more). Add any subdomain your tenants must not claim, such as `support`, `docs` or `status`, to `reserved`.

## Reading the current Laravel tenant in code

Once the middleware has run, the current Laravel tenant is available from the `Laravilt` facade:

```php
use Laravilt\Panel\Facades\Laravilt;

$tenant = Laravilt::getTenant();

if (Laravilt::hasTenant()) {
    $name = Laravilt::getTenant()->name;
}
```

The facade also provides `setTenant()`, `getTenantId()`, `getTenants()`, `canAccessTenant()` and `isTenancyEnabled()`.

## Where tenant context gets lost: queued jobs

Queued jobs don't carry tenant context. Pass the tenant id into the job and restore the tenant when it runs:

```php
use Illuminate\Contracts\Queue\ShouldQueue;
use Laravilt\Panel\Facades\Laravilt;
use Laravilt\Panel\Models\Tenant;

class ProcessOrder implements ShouldQueue
{
    public function __construct(
        public string $tenantId,
        public int $orderId,
    ) {}

    public function handle(): void
    {
        Laravilt::setTenant(Tenant::find($this->tenantId));

        // Process in tenant context
    }
}
```

This is an easy mistake to make in a multi tenant Laravel app: the code works in the request, then a job runs without a tenant. Make restoring the tenant the first line of every job that touches tenant data.

## Different modes on different panels

Each panel configures tenancy on its own, so one app can mix modes:

```php
use App\Models\Team;
use Laravilt\Panel\Models\Tenant;

// Admin panel: multi-database
$panel->multiDatabaseTenancy(Tenant::class, 'myapp.com');

// Portal panel: single-database
$panel->tenant(Team::class);

// Marketing panel: no tenancy call at all
```

To check the state of a panel, use `$panel->hasTenancy()`, `$panel->isMultiDatabaseTenancy()` and `$panel->isSingleDatabaseTenancy()`.

## If you're coming from Filament

I compared the two frameworks in [Laravilt vs Filament](/en/blog/laravilt-vs-filament). On tenancy the short version is this: both support multi-tenancy, and the PHP API is close enough that if you've written a Filament resource, a Laravilt resource will look familiar. But Laravilt isn't a drop-in replacement. Filament plugins don't run in Laravilt, so don't assume a Filament tenancy plugin carries over.

## Honest limits

- **This is tenancy inside a Laravilt panel.** If you aren't building on Laravilt, this isn't a general-purpose Laravel tenancy package.
- **Multi-database means more to run.** Every migration runs once per tenant with `tenants:migrate`, and the database user must be allowed to create databases.
- **Moving between modes is a data migration.** The panel config changes in a few lines, but moving existing records between a shared database and per-tenant databases is real work. Choose early.
- **Tenant context is yours to restore in queued jobs.** Nothing does it for you.
- **Laravilt is young.** If you need a large set of ready-made tenancy plugins today, check what exists first.

## FAQ

### How does Laravilt know which tenant a request belongs to?
In single-database mode, `IdentifyTenant` reads the tenant from the route parameter and checks the user can access it. In multi-database mode, `InitializeTenancyBySubdomain` reads the subdomain, rejects reserved ones, finds the tenant by domain or slug and switches to its database.

### Does multi-database tenancy create the tenant's database for me?
With the default provisioning config, yes: creating a tenant creates and migrates its database. You can queue that work with `'queue' => true`.

### Can one app use both tenancy modes?
Yes. Each panel configures tenancy on its own, so an admin panel can use `multiDatabaseTenancy()` while a portal panel uses `tenant()`.

### How do I keep tenant context in queued jobs?
Queued jobs don't carry it. Pass the tenant id to the job and call `Laravilt::setTenant(Tenant::find($this->tenantId))` at the start of `handle()`.

## Next steps

The full reference for both modes is in the [Laravilt tenancy docs](/en/docs/panel/tenancy/overview).

Building a multi tenant Laravel SaaS? I design and build them, including white-label for agencies. See [product engineering](https://fadymondy.com/en/services/product-engineering?utm_source=laravilt&utm_medium=referral&utm_campaign=seo-content-oct26) or [get in touch](https://fadymondy.com/en/contact?utm_source=laravilt&utm_medium=referral&utm_campaign=seo-content-oct26).

---

Source: https://laravilt.com/en/blog/tenancy-for-laravel-laravilt
