Laravel 5 — Simple Subdomain for Multi-Tenant Application

Photo by rawpixel on Unsplash

Introduction

When building a multi-tenant web application, one of the common pattern is to use subdomain as a gateway to separate between different tenants. (For example, https://company-one.example.com would show the page/data for Company One whereas https://company-two.example.com would show for Company Two.)

In Laravel 5, to define a route to subdomain and to access it, you’d need to provide the id / keyword for every routes.

As shown from Laravel 5.7 docs:
Route::domain('{account}.myapp.com')->group(function () {
Route::get('user/{id}', function ($account, $id) {
//
});
});
And to generate an URL:
route('routeName', ['account' => 'account-one']);

The id / keyword would then be accessible as if it is a route parameter.

That is very useful if you would like to enable user to navigate between different subdomains. But if you don’t need that, it would be too troublesome to supply the id / keyword every time when you define a Route and URL.

To simplify that, we could leverage the power of Middleware.


1. Get Tenant from Subdomain

Setup

Create a tenants table with data of the tenant and a slug field. The slug would be used to match the subdomain.

Middleware

With the setup done, we can start to work on the subdomain part. All we need is a Middleware file and few lines of codes.

First, generate a new Middleware:

php artisan make:middleware CheckTenant

Modify the generated Middleware:

Code breakdown

list($subdomain) = explode('.', $request->getHost(), 2);

Extract the tenant slug from the request URL. Here we use a simple explode method, you may modify it based on how you define the stored slug pattern.

$tenant = Tenant::whereSlug($subdomain)->first() ?: abort(404);

Try to get the tenant data from database with the slug. If it does not exists, abort the request with 404 not found error.

$request->session()->put('tenant', $tenant);

If the tenant exists, store the tenant data into session to be accessed later. Throughout your application, the tenant data is then accessible with a session(‘tenant’) call using Laravel session helper.

(* Note that unless you modify the config in Laravel, by default the sessions won’t be shared between different subdomain.)

Register Middleware

Next, in app/Http/Kernel.php, register the Middleware to ‘web’ group. The middleware now applies to all routes:

protected $middlewareGroups = [
'web' => [
...,
\App\Http\Middleware\CheckTenant::class,
],
...
];

Result

Try to access the web application with existing and non-existing tenant. You should only able to access the tenant with existing record.

By using this method, you don’t have to supply the id / keyword when you define the route / generate URL. When you create a link anywhere in the application using route() helper, it will contain the full subdomain URL.

(If you are developing using localhost, note that you can access it with http://{TENANT-SLUG}.localhost:{PORT} subdomain.)


2. (Additional) Tenant’s User

With the simple change above, now we are able to determine which tenant the user is trying to access on every request. We also stored it in session so we can now get the tenant data anywhere with a session(‘tenant’) call.

In most cases, each tenant have own set of Users. Therefore, the next thing we could add is to check if the user can access to the requested tenant.

Setup

Start with the default auth template provided by Laravel.

php artisan make:auth

After that,

  1. Add a tenant_id foreign key to the users table so that users could be granted access to tenant.
  2. Define one-to-many relationships within the Model files. (One tenant has many users)

Middleware

Modify the Middleware by adding some additional logic:

Don’t forget to update the Middleware name in app/Http/Kernel.php as well.

Code breakdown

if ($request->user() == null) {
return $next($request);
}

If the user is not logged in yet, we could just pass this request for the Laravel’s auth logic to handle it.

$has_access = $request->user()->tenant == $tenant;
if (!$has_access) {
Auth::logout();
return redirect('/login')->with('no_access', true);
} else {
$request->session()->put('has_access', true);
}

If the user is logged in, we check if the user has access to the requested tenant. If no access, the user is logged out, and redirected to login page with a flash message to inform them.

If everything’s good, we assign a session ‘has_access’ as true, then let it pass.

So far, we are able to check for the tenant and whether the user can access it. But there is still one flaw: with every single request, we need to make at least one database call for the checking. To solve this, we could check the value of ‘has_access’ session at the beginning of the middleware:

$has_access = $request->session()->get('has_access');
if ($has_access) {
return $next($request);
}

If ‘has_access’ is true, it means the user has been granted access previously, so we just let it pass without needing to check again.

Login View

At last, add the following to login view page. As set in Line 48, this flash message would be shown if an user try to access other tenant’s page.

@if (session('no_access'))
<div class="alert alert-danger">
You have no access to this Tenant.
</div>
@endif

Result

Try to access http://{SUBDOMAIN}/login. You can only sign in with user that are assigned to the respective tenant. After you’ve signed in, you should also unable to access another tenant’s data by manually changing the subdomain.


To Study Further

Multi-tenancy is a huge topic by itself and usually requires lots of setup with careful planning, starting from the infrastructure level.

The example shown in this article is useful for multi-tenant Laravel application with simple structure. If your app requires more complex structure, here are some very useful resources I find:

  1. Multi-Tenancy Talk from Laracon US 2017 by Tom Schlick
  2. Tutorials on setting up multi-tenant app with Laravel by Ashok Gelal
  3. Overviews on Domain/Subdomain/Path based multi-tenancy by Ollie Read.

Thanks for reading through! Feel free to leave any comment or contact me on weihien90@gmail.com