Courses

Laravel 12 Multi-Tenancy: All You Need To Know

"Wildcard" Subdomain for Every Tenant

Summary of this lesson:
- Adding unique subdomain field to tenants table
- Setting up wildcard subdomains with SESSION_DOMAIN
- Handling subdomain redirects during tenant switching
- Ensuring security through database tenant validation

Now, let's talk about subdomains. This is a typical way to structure multi-tenancy projects, and how to separate tenants so every tenant would have a "personal space". Fake, but personal space.

What do I mean by fake? Why is it fake?

Because subdomains can't act as a security filter, it wouldn't be secure enough. For example, we have a tenancy.test domain. And for every tenant/company/team, we create a subdomain like povilas.tenancy.test.

If I take just the URL and filter the data by that URL, anyone could fake my URL without even being logged in. Another way would be to log into another tenant, replace their URL, and land onto my workspace, which is a security issue.

So, in this lesson, we will create subdomains. I will show you how it works, but you need to understand correctly that it is more like the cherry on top, like part of the design of your application, but not the secure authentication of the tenant.

Let's implement these features:

  1. Register the subdomain during the registration and then redirect to the correct subdomain.
  2. When switching between the tenants, redirect them to their subdomain.

Registering the Subdomain

In the registration form, let's add the input for the subdomain.

resources/views/auth/register.blade.php:

// ...
 
<!-- Email Address -->
<div class="mt-4">
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
 
<!-- Subdomain -->
<div class="mt-4">
<x-input-label for="subdomain" :value="__('Subdomain')" />
<x-text-input id="subdomain" class="block mt-1 mr-2 w-full" type="text" name="subdomain" :value="old('subdomain')" required />
<x-input-error :messages="$errors->get('subdomain')" class="mt-2" />
</div>
 
<!-- Password -->
 
// ...
</x-guest-layout>

Next, create a subdomain column in the tenants table.

php artisan make:migration "add subdomain to tenants table"

database/migrations/xxx_add_subdomain_to_tenants_table.php:

Schema::table('tenants', function (Blueprint $table) {
$table->string('subdomain')->unique();
});

app/Models/Tenant.php:

class Tenant extends Model
{
protected $fillable = [
'name',
'subdomain',
];
 
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class);
}
}

Lastly, when a tenant is being created, and the user is registering, we must do three things:

  1. Validate the subdomain for uniqueness
  2. Create a tenant with the subdomain
  3. Finally, the user must be redirected to the tenant subdomain

app/Http/Controllers/Auth/RegisteredUserController.php:

class RegisteredUserController extends Controller
{
// ...
 
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
'subdomain' => ['required', 'alpha', 'unique:'.Tenant::class],
]);
 
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
 
$tenant = Tenant::create([
'name' => $request->name . ' Team',
'subdomain' => $request->subdomain,
]);
$tenant->users()->attach($user);
$user->update(['current_tenant_id' => $tenant->id]);
 
event(new Registered($user));
 
Auth::login($user);
 
return redirect(route('dashboard', absolute: false));
$tenantDomain = str_replace('://', '://' . $request->subdomain . '.', config('app.url'));
return redirect($tenantDomain . route('dashboard', absolute: false));
}
}

Wildcard Subdomain

After registering, we are redirected to the correct subdomain but not logged in correctly. And this is important. For authentication sessions, Laravel saves cookies on your computer based on domain: in this case, domain and subdomain are different.

So, to perform that wildcard subdomain session, we need to set the SESSION_DOMAIN in the .env to your domain.

.env:

// ...
 
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=tenancy.test
 
// ...

After registering with a new user and different subdomain, we are redirected to the correct subdomain and authenticated.


Subdomain Routing

So, we have that subdomain, but should we do something about it? Should we check that? Should we get that into a variable and then filter some records for it? As I said at the beginning of this lesson, I would advise you on something else.

Laravel has the routing feature of subdomain routing, so we could use that. But we do have the current_tenant_id saved in the database. This is our primary source of security checks, which are the domains active for the user. So, in my case, I recommend not using wild card routing with a domain for multi-tenancy.

But if the user has more than one tenant, when the user changes the tenant, they must be redirected to the tenant's subdomain.

class TenantController extends Controller
{
public function __invoke($tenantId)
{
// Check tenant
$tenant = auth()->user()->tenants()->findOrFail($tenantId);
 
// Change tenant
auth()->user()->update(['current_tenant_id' => $tenant->id]);
 
// Redirect to dashboard
return redirect()->route('dashboard');
$tenantDomain = str_replace('://', '://' . $tenant->subdomain . '.', config('app.url'));
return redirect($tenantDomain . route('dashboard', absolute: false));
}
}

But as I said multiple times in this lesson, it's just a nice touch that you visually see what team you are on, but filtering the data is actually performed by the users.current_tenant_id field from the database.

Previous: Multiple Tenants and Switching Between Them
avatar

Where do you check the subdomain assigned to the tenant? Regardless of which subdomain I enter, the dashboard shows me (checking permissions by subdomain does not work).

avatar

You can add the check based on the $user->current_tenant_id in your system. This can be done via middleware or via if conditions where you need it to.

An example is at the end of the video with the TenantController

avatar

Can you give me some code example? I don't know how to apply this to the example in question.

avatar

Example code for what exactly? There is both video and a github repository with the code shown there

avatar

I need code to check whether the subdomain entered by the user is assigned to him. Currently, I enter any subdomain and see my dashboard, e.g. test.mydomain.com, test2.mydomain.com (test2 is not my subdomain tenant).

avatar

Oh, now I understand.

For this, you need to create a custom middleware, which would check if the domain belongs to user.

You can get the subdomain with this code: $request->route('subdomain') and simply compare to auth()->user() models allowed subdomains.


There is no ready-made example of this, sorry!

avatar
You can use Markdown
avatar

what is the name plugin to fill form fastly ?

avatar

The plugin is called FakeFiller in chrome

avatar
You can use Markdown
avatar

It does not work with Laragon and Apache. What is webserver that you use?

avatar

For Laragon/Apache combination, you have to modify windows HOST file and create EACH tenant as an entry there (domain inside HOST file). It does not automatically resolve it correctly.

We are using Laravel Valet, but I assume you are on windows - so you should probably check out Herd!

avatar
You can use Markdown
avatar
You can use Markdown