Skip to content

Routing

This page goes over the different ways to handle route registration in multi-tenant applications, as well as common obstacles.

Basics

After following the Getting started guide, you can register your central routes in routes/web.php — like in a normal, single-tenant application — and your tenant routes in routes/tenant.php.

routes/web.php
Route::get('/', CentralHomeController::class);
Route::get('/purely-central-route', ...);
routes/tenant.php
Route::middleware([
'web',
InitializeTenancyByDomain::class,
PreventAccessFromUnwantedDomains::class,
])->group(function () {
Route::get('/', TenantHomeController::class);
Route::get('/purely-tenant-route', ...);
});

Conflicting routes

This was briefly touched upon on the Getting started page.

Conflicting routes refers to the fact that you may want to have two different routes on the same path — one in the central part of the application and one in the tenant part of the application.

The only way to solve this with Laravel’s routing is to one of these routes to a specific domain. That way, the route won’t be used when the domain condition isn’t met.

Specifically, this means registering your central routes only on central domains:

routes/web.php
Route::domain('my-central-domain.com')->group(function () {
// your central routes
});

That way, you will be able to have two different / routes in routes/web.php and routes/tenant.php, since one of them will be defined only on a specific domain.

Central routes

Continuing the previous section, to register central routes, you should bind their definition to your central domain.

However, this shouldn’t be hardcoded as in the previous example. The domains should be read from your central domains config:

foreach (config('tenancy.identification.central_domains') as $domain) {
Route::domain($domain)->group(function () {
// your central routes
});
}

This configuration key is important, and you should make sure its value is accurate (it defaults to a domain extracted from your APP_URL). It’s used by the tenancy middleware to handle the counterpart of this: preventing access to tenant routes from central domains.

Tenant routes aren’t registered using domain(), since tenant domains are dynamic — they’re read from the database and can change any time. Central domains are static, so they’re the better candidate for domain(). In short: we have two conflicting routes, so at least one of them needs to be bound to a specific domain. It won’t work for the tenant domains since they’re dynamic, so we do this with the central routes.

One more tip here is to only have one central domain. The reason for that is that you cannot use route names if you have multiple domains. Route names are unique, but each route definition bound to a domain is a separate route. Therefore, the last registered route with the name would be the only one to get actually registered with that name.

Route registration order

It’s important that your tenant routes are registered after central routes. The reason for this is that routes registered earlier take precedence over routes registered later.

You can observe a similar effect when registering routes like this:

Route::get('/products/edit', ...);
Route::get('/products/{product}', ...);

The edit route needs to be registered first. If the routes were registered in the opposite order, Laravel’s routing would try to fit edit into {product} instead of realizing it’s a separate route. You’re probably familiar with this if you’ve built a CRUD app with Laravel before.

In the context of multi-tenancy, it’s similar. If our tenant routes — which include a / route and only restrict access from central domains using middleware — were registered first, Laravel wouldn’t even look for the central / route since it already found a route that matches /.

If we register the central route first (the proper approach), Laravel will:

  • notice the route first, and use it on central domains after verifying that the request domain matches the route domain
  • ignore the route on tenant requests, since the domain() condition doesn’t match

Alternative way of registering routes

In Getting started we mentioned that there are two different ways to register your routes. One involves wrapping your central routes in a domain() scope.

The other solution — discussed here — lets you keep a clean structure in web.php, but requires a bit more configuration and has some minor drawbacks, which is why it’s not the solution we suggest in the Getting started guide. That said, you may find this approach to be a lot more convenient, especially as your application grows.

In essence, this solution registers routes via the using parameter of withRouting() method in your bootstrap/app.php file:

bootstrap/app.php
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
using: function () {
$centralDomains = config('tenancy.identification.central_domains');
foreach ($centralDomains as $domain) {
Route::middleware('web')
->domain($domain)
->group(base_path('routes/web.php'));
}
Route::middleware('tenant')->group(base_path('routes/tenant.php'));
},
)

You can see that we are registering both the central and tenant routes in this closure. This means all of your route registration happens in one place.

Since we’re registering the tenant routes here, let’s disable route registration in the TenancyServiceProvider:

app/Providers/TenancyServiceProvider.php
public function boot()
{
$this->bootEvents();
// $this->mapRoutes();

The only drawback here is that the healthcheck route doesn’t get registered, so if you’d like to use it, you’d need to manually register it as part of the using closure.

Common patterns

In large applications, you may want to separate your tenant routes into web routes and API routes:

bootstrap/app.php
Route::middleware('tenant')->group(base_path('routes/tenant.php'));
Route::middleware(['tenant', 'web'])->group(base_path('routes/tenant/web.php'));
Route::middleware(['tenant', 'api'])->group(base_path('routes/tenant/api.php'));

You may also want to automatically apply all the necessary middleware, so that you can keep your tenant route files clean:

bootstrap/app.php
Route::middleware([
'tenant',
InitializeTenancyByDomain::class,
PreventAccessFromUnwantedDomains::class
])->group(function () {
Route::middleware('web')->group(base_path('routes/tenant/web.php'));
Route::middleware('api')->group(base_path('routes/tenant/api.php'));
});

Remember that you don’t have to use the routes/tenant.php file — that’s just the default file registered by TenancyServiceProvider.

You can use the tenancy middleware in any route file — except files like web.php if they’re scoped to the central domain.