Back in November 2015, I wrote a Medium post about using Laravel’s subdomain routing feature with custom domain names, something that is important for many multi-tenant applications. There was always one thing about this solution that bugged me however, and recently I decided to take a second look at the problem to try and come up with a better approach.
The original solution
The original solution I found for using custom domains in Laravel was to add a global route pattern to RouteServiceProvider
that changed the regular expression for subdomain matching to allow for full domains:
Route::pattern('domain', '[a-z0-9.\]+');
This allowed you to use the domain routing feature as follows:
Route::domain('{domain}')->group(function() { Route::get('users/{user}', function (Request $request, string $domain, User $user) { // Your code here }); });
While this works very well, it means you need to add the $domain
argument to all your route actions, even if you don’t need them for the route in question. At the time I found it an acceptable consequence of the solution, but I’ve always wanted to try to find a better way.
The better way
The improved solution required completely forgetting that subdomain routing exists in Laravel the first place. Instead, we can use a simple middleware to look up the appropriate tenant based on the request host and add data to the request object so it can be used elsewhere in the application. The simplest implementation of this could be the following middleware class:
<?php namespace App\Http\Middleware; use Closure; use App\Tenant; class CustomDomain { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { $domain = $request->getHost(); $tenant = Tenant::where('domain', $domain)->firstOrFail(); // Append domain and tenant to the Request object // for easy retrieval in the application. $request->merge([ 'domain' => $domain, 'tenant' => $tenant ]); return $next($request); } }
Here, I’m assuming you have a Laravel model named Tenant
which has an attribute domain
which contains the full domain name you want to use for the tenant in question.
Now, in your app/Http/Kernel.php
file, you can add a middleware group or route middleware that includes this new middleware. I like to create a new group for domain routes, which can be useful if there are other middleware I need to apply specifically to custom domain routes:
... protected $middlewareGroups = [ ... 'domain' => [ \App\Http\Middleware\CustomDomain::class, ], ... ];
Now, I can assing this middleware group to any routes that should be served up under a custom domain. Personally, I like to create a new routes/tenant.php
routes file, and then in app/Providers/RouteServiceProvider.php
add a new method for mapping tenant routes, complete with namespace and route name prefixing, such as:
public function map() { $this->mapApiRoutes(); $this->mapWebRoutes(); $this->mapTenantRoutes(); } protected function mapTenantRoutes() { Route::middleware(['web', 'auth', 'domain']) ->namespace("$this->namespace\Tenant") ->name('tenant.') ->group(base_path('routes/tenant.php')); }
The final piece of the puzzle is how to access information about the tenant from your controller actions, views and other parts of your application. After all, you’re going to need to know the tenant to correctly filter requests and what not. Unlike the previous solution, this approach does not add the $domain
argument to your route action methods. Instead, you can simply read the tenant information off the request object itself. The following is an example of a controller action that does just that:
public function index(Request $request) { $tenant = $request->tenant; ... }
If you’re using Blade templates in your application, the CustomDomain
middleware is also a good place to share any data that may be useful to your views that is different based on the tenant:
<?php namespace App\Http\Middleware; use Closure; use App\Tenant; use Illuminate\Support\Facades\View; class CustomDomain { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { $domain = $request->getHost(); $tenant = Tenant::where('domain', $domain)->firstOrFail(); // Append domain and tenant to the Request object // for easy retrieval in the application. $request->merge([ 'domain' => $domain, 'tenant' => $tenant ]); // Share tentant data with your views for easier // customization across the board View::share('tenantColor', $tenant->color); View::share('tenantName', $tenant->name); return $next($request); } }
Other considerations
The following are some other things you’ll likely encounter when you use this approach to enable custom domains in your application.
Authentication
You’ll most likely want to enable users of your application to only be able to login under the tenant they belong to. To do this, you may store a tenant_id
attribute on your user model, and amend your authentication to take this into account when logging in. You can do this by overriding the credentials
method on the stock Laravel app/Http/Controllers/Auth/LoginController.php
file. The default method (which comes from the AuthenticatesUsers
trait) is as follows:
protected function credentials(Request $request) { return $request->only($this->username(), 'password'); }
Making this tenant aware is as simple as changing this to the following:
protected function credentials(Request $request) { $credentials = $request->only($this->username(), 'password'); $credentials['tenant_id'] = optional($request->tenant)->id; return $credentials; }
Now users will only be able to login under the domain associated with the tenant their user model is related to.
You will also likely want to consider how your auth routes are mapped. By default, Laravel’s authentication routes are added to routes/web.php
, but if you want to use the tenant middleware in your auth routes (for customisation or other reasons) you might find it better to create a new method in your RouteServiceProvider
specifically for auth routes, such as:
protected function mapAuthRoutes() { Route::middleware(['web', 'domain']) ->namespace("$this->namespace\Auth") ->name('auth.') ->group(base_path('routes/auth.php')); }
Subdomains
While the goal of this is to enable support for custom domains in a Laravel application, you can still of course continue to serve subdomains and use the same approach as described here. The main difference between this solution and the built-in subdomain routing feature is that with this approach you need to store the full domain for your tenant rather than just the subdomain prefix.
Root and other non-tenant domains
In some instances, you may want to serve up something under your application’s root domain or under a specific domain that is not used by a specific tenant. This could be for an administration panel or some other functionality. One approach you could take here is to set a config for the domain you want to use and add a check for this if a tenant is not found in your CustomDomain
middleware.
<?php namespace App\Http\Middleware; use Closure; use App\Tenant; use Illuminate\Support\Facades\View; class CustomDomain { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { $domain = $request->getHost(); $tenant = Tenant::where('domain', $domain)->first(); if (!$tenant) { $adminDomain = config('app.admin_domain'); if ($domain != $adminDomain) { abort(404); } } // Append domain and tenant to the Request object // for easy retrieval in the application. $request->merge([ 'domain' => $domain, 'tenant' => $tenant ]); if ($tenant) { View::share('tenantColor', $tenant->color); View::share('tenantName', $tenant->name); } return $next($request); } }
For the view data, you could add fallbacks directly in the middleware here or alternatively you can add these to your app/Providers/AppServiceProvider.php
in the boot
method.
... public function boot() { ... View::share('tenantColor', 'gray'); View::share('tenantName', 'Admin'); ... } ...
Conclusion
And that’s about it! Overall I find this approach to be a cleaner method of implementing custom domains (and indeed subdomains) in a Laravel application. If you have any suggestions on how the above approach can be improved, feel free to reach out to me on Twitter @joelennon.