Skip to content

Universal routes

Universal routes refer to routes that are usable by both the central app and the tenant app.

In other words: tenancy is initialized when a tenant is specified, and it’s not initialized when a tenant is not specified.

Usage

Since Tenancy v4, making a route universal is as easy as adding the universal middleware on top of identification middleware:

Route::middleware([
InitializeTenancyByDomain::class,
'universal',
])->group(function () {
Route::get('/posts', [PostController::class, 'index']);
});

If your central domain were your-saas.com and a tenant’s domain were your-saas.theirdomain.com:

  • Tenancy would not be initialized on your-saas.com/posts
  • Tenancy would be initialized on your-saas.theirdomain.com/posts

How tenants are specified

Above we said that tenancy is initialized if the tenant is specified. What exactly does this mean?

It refers to providing the tenant in some format that the used identification middleware expects.

With the domain example above it’s obvious, requests made on a domain that’s included in the central_domains config will be central requests, and requests made on other domains will be tenant requests.

But how does this work with other identification middleware?

With request data identification, tenancy will be initialized if the tenant is provided in any of the supported formats:

InitializeTenancyByRequestData.php
protected function getPayload(Request $request): string|null
{
if (static::$header && $request->hasHeader(static::$header)) {
$payload = $request->header(static::$header);
} elseif (static::$queryParameter && $request->has(static::$queryParameter)) {
$payload = $request->get(static::$queryParameter);
} elseif (static::$cookie && $request->hasCookie(static::$cookie)) {
$payload = $request->cookie(static::$cookie);
} else {
$payload = null;
}
if (is_string($payload) || is_null($payload)) {
return $payload;
}
throw new TenantCouldNotBeIdentifiedByRequestDataException($payload);
}

With path identification, it’s a bit more complex.

Path identification

When you’re using path identification, you need to include the {tenant} parameter in your routes.

However, the parameter is part of the route path, and the path is how Laravel distinguishes between different routes.

Therefore, we cannot have a route that works as both /posts and /{tenant}/posts.

Instead, We will need two different routes.

So since we’ll be using two different routes, we’ll need to register each route with path tenant identification that we’ll want to use universally in two ways:

  • /posts (central route, no identification middleware)
  • /{tenant}/posts (tenant route, with identification middleware)

To make this easier for you, we have a dedicated feature for this: cloning routes as tenant routes.

Imagine that you’re integrating with a third-party package that has standard routes without any {tenant} parameters:

Route::group([
'middleware' => config('posts_package.middleware'),
], function () {
Route::get('/posts', [PostController::class, 'index'])->name('package.posts.index');
Route::get('/posts/{post}', [PostController::class, 'show'])->name('package.posts.show');
});

In this example, we can change the middleware the package applies on its routes.

So if we apply the following middleware:

[
InitializeTenancyByPath::class,
'universal',
]

We’ll just need to create clones of these routes prefixed with /{tenant}/ to be able to use them in the tenant application.

For that, we’ll use the CloneRoutesAsTenant action:

TenancyServiceProvider.php
public function boot()
{
// ...
$cloneRoutes = app(CloneRoutesAsTenant::class);
$cloneRoutes->handle();
}

This action will create a copy of all routes that use path identification, have the universal middleware flag, and don’t have the {tenant} parameter, with the following changes:

  • Prefix the route path with /{tenant}/
  • Prefix the route name with tenant.
  • Apply the tenant flag (to make tenancy initialization work if we’d be using early identification)

The example routes above would be cloned like this:

// Original routes — central
Route::get('/posts', [PostController::class, 'index'])->name('package.posts.index');
Route::get('/posts/{post}', [PostController::class, 'show'])->name('package.posts.show');
// Cloned routes — tenant
Route::get('/{tenant}/posts', [PostController::class, 'index'])->name('tenant.package.posts.index');
Route::get('/{tenant}/posts/{post}', [PostController::class, 'show'])->name('tenant.package.posts.show');

The original routes will be accessible in the central app (even if they have the identification middleware) because they have the universal flag. They will not be accessible in the tenant app since there’s no {tenant} parameter that could be specified.

The cloned routes will only be accessible in the tenant app (since they have the {tenant} parameter) and tenancy will be initialized on them.

The route names are prefixed with tenant. to avoid collisions while still letting you use named routes.

Default route mode universal

todo