Skip to Content
Course content

211: Tenant Identification Middleware

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you're building a SaaS application, you'll eventually hit the "multi-tenancy" problem. You have one codebase and one infrastructure, but your data is partitioned by customer. If "Bookstore A" logs in, they should never see "Bookstore B's" inventory. While you could pass a tenant_id into every single controller method, that's a recipe for a security disaster—you'll forget it once, and suddenly you've leaked private data.

The professional way to handle this is through middleware. We want to identify the tenant as early as possible in the request lifecycle and "set" that tenant globally for the duration of the request. For this example, we're going to build a system that identifies tenants based on their subdomain (e.g., books-r-us.myapp.com).

Extracting the identity from the URL

First, we need a middleware class. I'm going to assume we're using a standard PSR-15 style middleware approach. The goal here is to grab the Host header from the request, strip away our main domain, and see what's left.

public function handle(Request $request, Closure $next)
{
    $host = $request->getHost(); // e.g., 'books-r-us.myapp.com'
    $mainDomain = 'myapp.com';
    
    // We split the host and remove the main domain part
    $parts = explode('.', $host);
    
    if (count($parts) > 2) {
        $tenantSlug = $parts[0];
    } else {
        $tenantSlug = null;
    }

    // Now we find the tenant in the database
    $tenant = Tenant::where('slug', $tenantSlug)->first();

    if (!$tenant) {
        abort(404, 'Tenant not found.');
    }

    return $next($request);
}

Binding the Tenant to the Container

Finding the tenant is one thing, but how does the rest of your app—your repositories, your models, your services—know which tenant was found? If you just leave it in the middleware, it's useless. I prefer binding the tenant instance into the service container as a singleton. This makes the tenant globally accessible but decoupled.

I'll add this line right before the return $next($request):

// Bind the actual Tenant model instance so we can resolve it anywhere
app()->instance(Tenant::class, $tenant);

Now, in any controller or service, you can just type-hint Tenant $tenant in the constructor, and PHP's container will inject the specific bookstore we identified in the middleware. It's clean, and it's testable.

The "Main Site" oversight

Here is where I usually mess up the first time I implement this. I ran the code above, navigated to my landing page at myapp.com, and—surprise—I got a 404. Why? Because my middleware is running on every request, including the marketing page where there is no subdomain.

I tried to fix this by adding a complex regex, but that's overkill. The simplest fix is to check if the $tenantSlug is null before trying to query the database. If it's null, we just let the request pass through. The landing page doesn't need a tenant context.

// The correction:
if (!$tenantSlug) {
    return $next($request); 
}

$tenant = Tenant::where('slug', $tenantSlug)->first();

if (!$tenant) {
    abort(404, 'Tenant not found.');
}

Applying the middleware strategically

You don't want this running on your API health checks or your login page (unless the login page is tenant-specific). I recommend creating a route group. By wrapping your "app" routes in this middleware, you ensure that by the time a request hits your OrderController, the tenant has already been validated and bound to the container.

Route::middleware([IdentifyTenant::class])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    Route::get('/inventory', [InventoryController::class, 'index']);
});

This approach turns the tenant identity into a "given." Your business logic can now simply assume the tenant exists, which makes your controllers significantly leaner.




📋 Practical Task

Implement a Header-Based Tenant Fallback

In some environments, subdomains aren't possible (like during local development or when using a mobile app via a proxy). Your task is to modify the IdentifyTenant middleware to support a fallback mechanism.

The Requirements:

  • First, attempt to identify the tenant via the subdomain as we did in the lesson.
  • If no subdomain is present, check the request headers for a key named X-Tenant-ID.
  • If the header exists, use its value as the $tenantSlug.
  • If neither a subdomain nor the header is present, allow the request to proceed to the next middleware (supporting the main site/landing page).
  • If a slug was found (via either method) but no matching tenant exists in the database, throw the 404 error.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.