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:
- Domain identificaton: identifying tenants by full hostnames
- Subdomain identification: identifying tenanats by subdomains of central domains
- Combined domain and subdomain identification: both of the above
- Path identification: identifying tenants by a
/{tenant}/path parameter - Request data identification: identifying tenants by headers, query parameters, and cookies
- Origin header identification: domain identification using the
Originheader, essentially a hybrid between domain and request data identification
These middleware identify the tenant from the current request and subsequently initialize tenancy.
Domain identification
Section titled “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.
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"); });});$tenant = Tenant::create();$tenant->createDomain('tenant1.example.test');The id of the current tenant is 91ea01a5-17c5-4956-a309-5ec636447015If 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
Section titled “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 -> tenant1http://tenant2.another-domain.test/bar -> NotASubdomainException
centralDomains = ['foo.test', 'bar.text']http://tenant1.foo.test/foo -> tenant1http://tenant2.bar.test/bar -> tenant2http://tenant3.another-domain.test/bar -> NotASubdomainExceptionRoute::middleware([ 'web', InitializeTenancyBySubdomain::class, PreventAccessFromUnwantedDomains::class,])->group(function () { Route::get('/foo', function () { return response('The ID of the current tenant is ' . tenant('id') . "\n"); });});'identification' => [ 'central_domains' => [ 'example.test', ],],$tenant = Tenant::create();$tenant->createDomain('tenant1');The id of the current tenant is 91ea01a5-17c5-4956-a309-5ec636447015Combined domain and subdomain identification
Section titled “Combined domain and subdomain identification”The InitializeTenancyByDomainOrSubdomain middleware combines the two approaches above:
- If the request hostname ends with any of the configured central domains, the middleware will use subdomain identification
- If the request hostname doesn’t end with a central domain, the middleware will use domain identification
Path identification
Section titled “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.
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"); });});> $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", }The id of the current tenant is 84501e25-a744-468c-ab31-2d7309ab8b2aThe name of the route parameter is configurable via the tenant_parameter_name config:
'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:
'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:
'identification' => [ 'resolvers' => [ Resolvers\PathTenantResolver::class => [ 'allowed_extra_model_columns' => [], // used with binding route fields 'allowed_extra_model_columns' => ['slug'], ], ],],Request data identification
Section titled “Request data identification”The InitializeTenancyByRequestData middleware supports identifying tenants using:
- headers (
X-Tenantby default) - query parameters (
?tenant=by default) - cookies (
tenantby default)
You can configure this identification method in the respective resolver section of the tenancy config:
'identification' => [ 'resolvers' => [ Resolvers\RequestDataTenantResolver::class => [ // Set any of these to null to disable that method of identification
'header' => 'X-Tenant', 'cookie' => 'tenant', 'query_parameter' => 'tenant',
'tenant_model_column' => null, // null = tenant key ],],Usage:
Route::middleware([ 'web', InitializeTenancyByRequestData::class])->group(function () { Route::get('/foo', function () { return response('The ID of the current tenant is ' . tenant('id') . "\n"); });});> $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", }$ curl example.test/foo?tenant=84501e25-a744-468c-ab31-2d7309ab8b2aThe id of the current tenant is 84501e25-a744-468c-ab31-2d7309ab8b2a
$ curl -H "X-Tenant: 84501e25-a744-468c-ab31-2d7309ab8b2a" example.test/fooThe id of the current tenant is 84501e25-a744-468c-ab31-2d7309ab8b2a
$ curl --cookie "tenant=84501e25-a744-468c-ab31-2d7309ab8b2a" example.test/fooThe id of the current tenant is 84501e25-a744-468c-ab31-2d7309ab8b2aOrigin header identification
Section titled “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.comyourapp.tenant2.com
Browsers will automatically add the Origin header to any request made from the frontend. For example:
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:
'https://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
Section titled “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:
- caching + cache invalidation,
- 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:
'resolvers' => [ Resolvers\DomainTenantResolver::class => [ 'cache' => false, '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' => false, 'cache_ttl' => 3600, // seconds 'cache_store' => null, // null = default ], Resolvers\RequestDataTenantResolver::class => [ // Set any of these to null to disable that method of identification 'header' => 'X-Tenant', 'cookie' => 'tenant', 'query_parameter' => 'tenant',
'tenant_model_column' => null, // null = tenant key
'cache' => false, 'cache_ttl' => 3600, // seconds 'cache_store' => null, // null = default ],],Customizing onFail logic
Section titled “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.