Skip to content

Tenant identification

Tenant identification refers to the process of identifying tenants in HTTP requests.

This is generally done using middleware. Out of the box, our package supports:

These middleware identify the tenant from the current request and subsequently initialize tenancy.

Domain identification

The InitializeTenancyByDomain middleware identifies the tenant using the request hostname, i.e. the full domain including any amount of subdomains:

https://tenant1.test/foo -> tenant1.test
https://tenant1.yourapp.com/bar -> tenant1.yourapp.com
https://tenant1.app.example.test/baz -> tenant1.app.example.test

Domain-based identification middleware should always be used with the PreventAccessFromUnwantedDomains middleware.

routes/tenant.php
Route::middleware([
'web',
InitializeTenancyByDomain::class,
PreventAccessFromUnwantedDomains::class,
])->group(function () {
Route::get('/foo', function () {
return response('The ID of the current tenant is ' . tenant('id') . "\n");
});
});
php artisan tinker
$tenant = Tenant::create();
$tenant->createDomain('tenant1.example.test');
http://tenant1.example.test/foo
The id of the current tenant is 91ea01a5-17c5-4956-a309-5ec636447015

If you’re using universal routes and the request is made on a central domain, the request will be handled without tenancy initialization — it will run in the central context.

Subdomain identification

The InitializeTenancyBySubdomain middleware works the same as the domain identification middleware, except it checks for subdomains instead of full hostnames. These subdomains must be subdomains of configured central domains (tenancy.identification.central_domains config):

centralDomains = ['example.test']
http://tenant1.example.test/foo -> tenant1
http://tenant2.another-domain.test/bar -> NotASubdomainException
centralDomains = ['foo.test', 'bar.text']
http://tenant1.foo.test/foo -> tenant1
http://tenant2.bar.test/bar -> tenant2
http://tenant3.another-domain.test/bar -> NotASubdomainException
routes/tenant.php
Route::middleware([
'web',
InitializeTenancyBySubdomain::class,
PreventAccessFromUnwantedDomains::class,
])->group(function () {
Route::get('/foo', function () {
return response('The ID of the current tenant is ' . tenant('id') . "\n");
});
});
config/tenancy.php
'identification' => [
'central_domains' => [
'example.test',
],
],
php artisan tinker
$tenant = Tenant::create();
$tenant->createDomain('tenant1');
http://tenant1.example.test/foo
The id of the current tenant is 91ea01a5-17c5-4956-a309-5ec636447015

Combined domain and subdomain identification

The InitializeTenancyByDomainOrSubdomain middleware combines the two approaches above:

  1. If the request hostname ends with any of the configured central domains, the middleware will use subdomain identification
  2. If the request hostname doesn’t end with a central domain, the middleware will use domain identification

Path identification

The InitializeTenancyByPath middleware uses the {tenant} parameter in routes to identify the current tenant and initialize tenancy. Unlike other bootstrappers, this is the only one that requires that you register routes differently — you need to include the {tenant} parameter as part of the route path.

The parameter is not passed to the controller, it is dropped automatically by the package after the tenant is identified.

routes/tenant.php
Route::middleware([
'web',
InitializeTenancyByPath::class
])->prefix('{tenant}')->group(function () {
Route::get('/foo', function () {
return response('The ID of the current tenant is ' . tenant('id') . "\n");
});
});
php artisan tinker
> $tenant = Tenant::create();
= App\Models\Tenant {#5259
data: null,
id: "84501e25-a744-468c-ab31-2d7309ab8b2a",
updated_at: "2024-04-14 23:41:22",
created_at: "2024-04-14 23:41:22",
tenancy_db_name: "tenant84501e25-a744-468c-ab31-2d7309ab8b2a",
}
http://example.test/84501e25-a744-468c-ab31-2d7309ab8b2a/foo
The id of the current tenant is 84501e25-a744-468c-ab31-2d7309ab8b2a

The name of the route parameter is configurable via the tenant_parameter_name config:

config/tenancy.php
'identification' => [
'resolvers' => [
Resolvers\PathTenantResolver::class => [
'tenant_parameter_name' => 'tenant',
'tenant_parameter_name' => 'team',
],
],
],

If you’d like to use different values than tenant keys for the route parameter, you can change the tenant_model_column config:

config/tenancy.php
'identification' => [
'resolvers' => [
Resolvers\PathTenantResolver::class => [
'tenant_model_column' => null, // null = tenant key
'tenant_model_column' => 'slug',
],
],
],

This can be useful if you want clean URLs for your tenant routes, but want to keep the security of using random strings for tenant keys.

You can also specify the column using the binding field syntax:

Route::get('/{tenant:slug}/foo', ...);

When using the binding field syntax, you need to whitelist the columns you use in these route definitions:

config/tenancy.php
'identification' => [
'resolvers' => [
Resolvers\PathTenantResolver::class => [
'allowed_extra_model_columns' => [], // used with binding route fields
'allowed_extra_model_columns' => ['slug'],
],
],
],

Request data identification

The InitializeTenancyByRequestData middleware supports identifying tenants using:

  • headers (X-Tenant by default)
  • query parameters (?tenant= by default)
  • cookies (tenant by default)

All of these approaches use the tenant key, the column is not configurable. You may however configure the names of all these:

app/Providers/TenancyServiceProvider.php
public function boot()
{
InitializeTenancyByRequestData::$header = 'X-Team';
InitializeTenancyByRequestData::$cookie = 'team';
InitializeTenancyByRequestData::$queryParameter = 'team';
// ...
}

Usage:

routes/tenant.php
Route::middleware([
'web',
InitializeTenancyByRequestData::class
])->group(function () {
Route::get('/foo', function () {
return response('The ID of the current tenant is ' . tenant('id') . "\n");
});
});
php artisan tinker
> $tenant = Tenant::create();
= App\Models\Tenant {#5259
data: null,
id: "84501e25-a744-468c-ab31-2d7309ab8b2a",
updated_at: "2024-04-14 23:41:22",
created_at: "2024-04-14 23:41:22",
tenancy_db_name: "tenant84501e25-a744-468c-ab31-2d7309ab8b2a",
}
Terminal window
$ curl example.test/foo?tenant=84501e25-a744-468c-ab31-2d7309ab8b2a
The id of the current tenant is 84501e25-a744-468c-ab31-2d7309ab8b2a
$ curl -H "X-Tenant: 84501e25-a744-468c-ab31-2d7309ab8b2a" example.test/foo
The id of the current tenant is 84501e25-a744-468c-ab31-2d7309ab8b2a
$ curl --cookie "tenant=84501e25-a744-468c-ab31-2d7309ab8b2a" example.test/foo
The id of the current tenant is 84501e25-a744-468c-ab31-2d7309ab8b2a

Origin header identification

The InitializeTenancyByOriginHeader middleware works like the InitializeTenancyByDomain middleware, except that it reads the domain from the Origin header instead of the request hostname.

The use case for this is when you have an API deployed on say api.yourapp.com and SPA frontends served from client domains:

  • tenant1.com
  • yourapp.tenant2.com

Browsers will automatically add the Origin header to any request made from the frontend. For example:

tenant1.com
const users = await fetch('https://api.yourapp.com/users').then(res => res.json());

Notice that the tenant is not being specified anywhere. However, if you dd($request->header('Origin')) on the backend, you get:

'tenant1.com'

The middleware uses this browser feature to make tenant identification from static SPA frontends extremely easy. You don’t have to obtain the tenant id anywhere, the package will know what tenant the request is meant for based on the domain it’s coming from.

To use this middleware, simply make sure you use the HasDomains trait on your Tenant model and assign each tenant a domain matching the site where their frontend is deployed.

Tenant resolvers

Under the hood, all of these middleware use tenant resolvers. Tenant resolvers are classes that receive some primitive input from the identification middleware and handle the rest of tenant identification.

The benefits of moving parts of the logic into resolvers are:

  1. caching + cache invalidation,
  2. code reuse: multiple identification middleware may use the same resolver. The package ships with 7 identification middleware but only 3 resolvers.

The caching logic specifically ensures that, in production, a connection to the central database doesn’t have to be established to fetch the tenant — since establishing connections can add a bit of latency in some setups. Instead, the tenant model is read from cache.

As for invalidation: when a tenant is updated, the cache for that tenant is pruned in all resolvers.

The cache logic can be configured in the tenancy.identification.resolvers config:

config/tenancy.php
'resolvers' => [
Resolvers\DomainTenantResolver::class => [
'cache' => true,
'cache_ttl' => 3600, // seconds
'cache_store' => null, // null = default
],
Resolvers\PathTenantResolver::class => [
'tenant_parameter_name' => 'tenant',
'tenant_model_column' => null, // null = tenant key
'allowed_extra_model_columns' => [], // used with binding route fields
'cache' => true,
'cache_ttl' => 3600, // seconds
'cache_store' => null, // null = default
],
Resolvers\RequestDataTenantResolver::class => [
'cache' => true,
'cache_ttl' => 3600, // seconds
'cache_store' => null, // null = default
],
],

Customizing onFail logic

If the middleware doesn’t manage to identify a tenant, it will abort the request. Out of the box, this means throwing a TenantCouldNotBeIdentifiedException exception. This behavior can be customized by setting the $onFail static property:

InitializeTenancyByDomain::$onFail = fn () => abort(404);
InitializeTenancyByDomain::$onFail = fn () => redirect('central.home');

Each identification middleware can have a different onFail handler. In other words, you need to configure this property for each identification middleware you use separately.

Alternatively, you can configure your exception handler to render all InitializeTenancyByDomain exceptions as e.g. 404s.

If you want your requests to be handled centrally when a tenant cannot be identified, see the Universal routes page of the documentation.