Introduction

Tenancy for Laravel is package for making Laravel applications multi-tenant.

It supports both single-database and multi-database tenancy.

Main features include:

  • Automatic mode (automatically scoping Laravel components to the current tenant)
  • Multiple identification methods out of the box (domains, subdomains, path, request data)
  • Event-based architecture (lets you hook into tenant creation, tenancy initialization, and dozens of other events)

The package was originally released in February 2019. Version 2 was released in October 2019 and Version 3 was released in June 2020. This documentation is for version 4 which was released in 2023.

At the time of writing, the package has over 3,000 stars on GitHub and 1.4 million downloads on Packagist.

The main idea behind this package is automatic multi-tenancy. My motivation for building this package back in 2019 was that I had a large Laravel application that I wanted to turn into a SaaS. Looking into the existing solutions for multi-tenancy, all of them were overly complex and would require that I rewrite significant parts of my codebase just to add multi-tenancy (which seemed more like a feature that I should be able to simply add to an existing application).

With that philosophy in mind, the package was built all around:

  1. Identifying a tenant (generally done using request middleware)
  2. Making Laravel components scope everything to that tenant's context
  3. The actual application requiring very few changes to be made multi-tenant, ideally zero changes to the core logic being used on tenant pages

This ended up being a popular approach to multi-tenancy, with both Tenancy v3 and the Multi-tenant SaaS boilerplate (released alongside Tenancy v3) being very popular and Tenancy for Laravel becoming the de-facto standard library for implementing multi-tenancy in Laravel.

Important notes

While this package abstracts most of the complexity that comes with multi-tenancy away, it's important to fully understand a few key concepts before using the package in a real application.

It is not required that you read the entire documentation first, but there are a few pages you should read in full. The rest of the documentation can be read as needed. In other words, you only need to understand a few concepts to get started with the package, and then you can read pages for individual features as you need them.

To make your experience with this package as smooth as possible, we made a How To Read This Documentation page which explains the ideal order in which you should go through the pages of this documentation.

How To Read This Documentation

Before building a multi-tenant application, you need to understand the following concepts:

  • Tenants
  • Tenant identification and routing
  • Single vs multi-database tenancy

It's recommended that you first follow the Quickstart Guide. That way you'll have a sample multi-tenant application running and will understand roughly what goes into setting multi-tenancy up.

After that, you should read the following documentation pages to understand how it works:

  • todo

v4 Changelog & Upgrade Guide

Version 4 brings many new features, with the most important ones being:

  • Jetstream/Fortify support
  • Early identification and a general overhaul of routing logic
  • New tenancy bootstrappers

If you're upgrading from v3, the main changes that will affect you are:

  • Namespace changes (mostly database-related things being moved to a dedicated namespace). These are easily resolved with any modern IDE
  • New config structure. You will have to delete and republish it (and then re-apply your changes, if any)
  • Several static properties being removed and replaced with config keys
  • Changes to routing logic (if you're using universal routes)

Diff: d0de09a...master

New features

  • Tenant schema dump (tenants:dump) command 7d98ebb #807 a1a976c #985 cb7567a #1015 ea19117 #1019
  • Add Microsoft SQL Server support cc6d4fe #715
  • Add ScoutTenancyBootstrapper f83504a #936
  • Set default value for tenant URL parameters when a tenant is identified by path f2c6408 #925
  • Make tenants:run handle stdin prompts from subcommands 409190f #923
  • Add optional DeleteTenantStorage listener ab5fa7a #938
  • Add BatchTenancyBootstrapper b78320b #874
  • Add Storage::url() support 7bacc50 #909 a7ad828
  • Add tenant-specific maintenance mode 121370e #761
  • Add current() and currentOrFail() tenant methods 42dab29 #970
  • Add option for dropping tenant databases on migrate:fresh 080b271 #971
  • Add maintenance mode events 3f60c4a #979
  • Add cookie option on InitializeTenancyByRequestData 05f1b2d #980
  • Add pending tenants1 198f34f #869 7d3298c #1025
  • Add SessionTenancyBootstrapper (adds support for database session driver) 5849089
  • Add session state when impersonating tenant f42f08c #1029
  • Add a dedicated feature for tenant-specific mail credentials 0f892f1 #989
  • Add skip-failing option to tenants:migrate command 342c67f #945
  • Allow defining the tenant connection template using array syntax 087733d #1040
  • Add UrlTenancyBootstrapper (changes APP_URL in CLI context, used e.g. when sending emails in queued jobs that should have links to the tenant's domain) 617e9a7 #1044 fbdb13f #1068 719b1be #1079
  • Add BroadcastTenancyBootstrapper (makes multi-tenant broadcasting/websockets possible) d7a4982 #1027
  • Add cache prefix mode for separating tenant caches (introduces tenancy support for any cache driver, no longer only Redis) bd9bbe8 #1014

Changes

  • Respect user-defined $onFail in the Universal Routes feature 233a122 #679
  • Change how tenants:run is used with arguments a45aa8e #912 #686
  • Stronger typing, minor changes to the Tenant interface 55d0a9a 8af354c 87212e5 d463e2d a94227a f98a901 942d79c
  • Improved namespacing, all database-related logic was moved to namespaces under Stancl\Tenancy\Database 40bf28c
  • Refactor TenantDatabaseManagers d2e1ce0
  • Fix/improve Ignition solutions 55d0a9a 3542b3f
  • Reverse order of tenancy bootstrappers when reverting to central context 62d19c5 #932
  • Make impersonation tokens require stateful guards 3bf2c39 #935
  • Remove configurable static property from BelongsToTenant trait (deprecated in PHP 8.1), replace it with a config key 24146b2 e5bc8dd
  • Refactor more static properties to config keys ccaba05
  • Improve CLI command outputs using Laravel 9+ components e4f5b92 #968 5d688e6 68de360 #1030
  • Change TenantDatabaseManager-related interfaces, add public database(): Connection method fe0a322
  • Make tenants:migrate default to configured schema path a1a976c #985
  • Use a dedicated DB connection for creating/deleting tenant databases bf504f4 #946 (fixes bug #515)
  • Improve resource syncing logic 77c5ae1 #915 (fixes bug #658) 22d1b20 #993 ea3e445 #992, 758fbc8 #997 (adds polymorphic table for mapping resources to tenants)
  • Centralize config used by BelongsToTenant and HasDomains, replace all tenant_id literals dd0f03f 2a39b05 (fix #998), 82fa6cb
  • Convert publishable migrations to anonymous classes fb2369d #1001
  • Only delete tenants in MigrateFreshOverride if the tenants table exists 9520cbc #1007
  • Early identification (old version: ff46bcf, new version: 1d0ca27)
  • Manual mode improvements (use correct event type, add new listeners) 73c5655 #1013
  • Add remember bool column to ImpersonationToken 228c267 #1101

Package changes

1

Feature name subject to change

Early identification

Early identification refers to identifying tenants (and initializing tenancy) before the controller is instantiated.

Simple applications will not need this, but you might need this if you're:

  • adding Tenancy for Laravel to an existing application that has a lot of controllers that use constructor dependency injection
  • integrating with packages that use constructor dependency injection in their controllers

Luckily, in Tenancy v4 we've abstracted away all of the complexity that comes with early identification, so solving this problem is easy.

That said, you will need a perfect understanding of how this works to make sure you implement this properly and avoid reading/writing data in the wrong context.

The problem

Controller constructors are executed ahead of route-level middleware, therefore any controllers that inject service-classes objects which read from the database/cache/similar will incorrectly use the central context even if identification middleware is applied on the route.

// Imagine that this represents some cloud service for storing images
class ImageService
{
    public string $apiKey;

    public function __construct(Repository $config)
    {
        $this->apiKey = $config->get('services.images.api_key');
    };

    public function store($image): string
    {
        // Pseudocode
        $response = Http::put('https://cloud-service.com/upload', [
            'image' => $image,
            'api_key' => $this->apiKey,
        ]);

        return $response->json('url');
    }
}

class PostController extends Controller
{
    public function __construct(
        ImageService $images,
    ) {}

    public function update(Request $request, Post $post)
    {
        if ($request->has('image')) {
            $post->update([
                'image_url' => $this->images->store($image),
            ]);
        }
    }
}

// Tenancy middleware included
Route::middleware(InitializeTenancyByDomain::class)
    ->post('/posts/update', [PostController::class, 'update']);

The code above would always use the central API key for the cloud service, even if you'd be changing services.images.api_key upon tenant identification.

This is because ImageService was injected in the controller's constructor, which Laravel executed before tenancy was initialized. Laravel does this because it supports specifying middleware in controller constructors, which means that the constructor needs to be instantiated first to get the full list of middleware that should be used on the route.

class PostController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
    }
}

Solutions

There are three solutions to this problem.

1. Not using constructor injection

Laravel lets you inject dependencies in route actions. Route actions are always called in the right context (i.e. tenant context if there's tenancy middleware on the route), so simply moving the dependency injection there fixes the problem.

class PostController extends Controller
{
-    public function __construct(
-        ImageService $images,
-    ) {}

-    public function update(Request $request, Post $post)
+    public function update(Request $request, Post $post, ImageService $images)
     {
         if ($request->has('image')) {
             $post->update([
-                'image_url' => $this->images->store($image),
+                'image_url' => $images->store($image),
             ]);
         }
     }
}

2. Using Laravel's native solution

Since Laravel 9.35/9.36, the core logic that causes this problem has been improved. The Laravel's native early identification section provides a detailed explanation of what was changed and how you can apply this in your project.

In short, if your controller implements the Illuminate\Routing\Controllers\HasMiddleware interface, or is not an instance of Illuminate\Routing\Controller, it will not be instantiated ahead of route-level middleware.

A note about controllers: By default, Laravel controllers extend Illuminate\Routing\Controller. However, they don't actually need to extend anything.

The base Controller class provides a few helpful methods that you can use in your controllers, but most controllers don't tend to use these.

All of these three classes are valid controllers:

class PostController {}
class PostController extends Controller {}
class PostController extends CustomClass {}

You likely know this already, but we're explicitly mentioning it here since this detail will be important in the section about this solution.

3. Using kernel identification

This is the most robust solution to the problem. Our package will do all of the heavy lifting for you, but you will need to apply middleware differently, and will need to use different middleware.

This approach is especially useful when you cannot change the application code, either because there's way too much logic to change (e.g. too many controllers to modify to use Laravel's native solution), or because you're integrating with a third-party pacakage (this is the main use case for kernel identification).

See the Kernel identification section below for a detailed explanation of how it works.

Laravel's native early identification

Since Laravel 9.35, route-level middleware will be executed ahead of controller constructors if the controller implements the Illuminate\Routing\Controllers\HasMiddleware interface or is not an instance of Illuminate\Routing\Controller.

// Evil code that caused the controller to be instantiated
- return $this->controllerDispatcher()->getMiddleware(
-     $this->getController(), $this->getControllerMethod()
- );

+ [$controllerClass, $controllerMethod] = [
+     $this->getControllerClass(),
+     $this->getControllerMethod(),
+ ];

// Code that skips instantiating the controller
+ if (is_a($controllerClass, HasMiddleware::class, true)) {
+     return $this->staticallyProvidedControllerMiddleware(
+         $controllerClass, $controllerMethod
+     );
+ }
+
+ if (is_a($controllerClass, Controller::class, true)) {
      // Evil code moved here
+     return $this->controllerDispatcher()->getMiddleware(
+         $this->getController(), $controllerMethod
+     );
+ }

In other words: the $this->controllerDispatcher()->getMiddleware logic (what causes dependency injection to run too early) would only be executed if the middleware does NOT implement HasMiddleware and IS an instance of Illuminate\Routing\Controller.

And since Laravel 9.36, Laravel looks for a getMiddleware() method (as opposed to checking if the controller is an instance of Illuminate\Routing\Controller). This method is still part of Illuminate\Routing\Controller, so the condition evaluates as true either way, but this change is still worth mentioning for completion.

- if (is_a($controllerClass, Controller::class, true)) {
+ if (method_exists($controllerClass, 'getMiddleware')) {

In summary: since Laravel 9.35/9.36, route-level middleware will be executed correctly, before controller constructors, assuming:

  1. The controller implements HasMiddleware, or
  2. The controller is not an instance of Illuminate\Routing\Controller

Therefore: If you use pure classes as controllers, or implement the HasMiddleware interface, your controller constructors will work fine and you don't need to use kernel identification.

Kernel identification

Kernel identification refers to using your identification middleware in the Http Kernel's global $middleware stack, as opposed to route-level middleware.

Usage

Open App\Http\Kernel and add the middleware you want to use to the $middleware array. Keep in mind that you can only use one identification approach/middleware in the kernel stack.

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array<int, class-string|string>
     */
    protected $middleware = [
        // Existing middleware...
        // \App\Http\Middleware\TrustHosts::class,
        \App\Http\Middleware\TrustProxies::class,
        \Illuminate\Http\Middleware\HandleCors::class,
        \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
+       \Stancl\Tenancy\Middleware\InitializeTenancyByDomain::class,
    ];

     // ...
}

With this change, the specified middleware (in our example InitializeTenancyByDomain, but you can use any other tenancy middleware the package provides) will execute on every request, long before Laravel even figures out which controller to use.

But of course we don't want to initialize tenancy on every request, since we also have central routes. So we need some way to distinguish between central and tenant routes. The following sections will cover that.

Route context flags

Since the middleware will execute even on requests you make on yourapp.com (say this is your central domain), we need to tell the package to not initialize tenancy on those requests.

This can be achieved by adding the central middleware1 to your route:

// Tenancy will not be initialized here
Route::middleware('central')
    ->get('/central-route', CentralController::class);

The opposite of this is the tenant flag, which tells the package that it should initialize tenancy on a given route (or route group):

// Tenancy will not be initialized here
Route::middleware('central')
    ->get('/central-route', CentralController::class);

// Tenancy will be initialized here
Route::middleware('tenant')
    ->get('/tenant-route', TenantController::class);

You might be asking, if both central and tenant should be defined, what happens if you define neither?

Default route mode

Tenancy has a config key called default_route_mode. This indicates what should happen if no flag is provided on a route.

If the value is RouteMode::CENTRAL, routes will be treated as central by default and tenancy will not be initialized on them, even if the identification middleware is in the Http Kernel. You will need to apply the tenant flag on a route (or route group) for tenancy to be initialized when the route is visited.

If the value is RouteMode::TENANT, routes will be treated as tenant/tenancy will be initialized by default. If you'll want some routes to be central (= tenancy not being initialized), you'll need to apply the central middleware. For example, if you wanted routes/web.php to be central, you'd need to wrap the contents of that file in Route::group('central').

If the value is RouteMode::UNIVERSAL, routes will be usable as both central and tenant. This means tenancy will be initialized if the "tenant value" is provided in some way.

  • With domain identification, this means making a request on a tenant domain (domain not specified in the central_domains list in the tenancy config)
  • With path identification, this means making a request to /tenant123/foo instead of /foo 2
  • With request data identification, this means providing the tenant in any of the supported formats (X-Tenant header, ?tenant query string, and others)

Intuitive defaults

By default, the package uses the RouteMode::CENTRAL route mode and marks routes/tenant.php as tenant (see App\Providers\TenancyServiceProvider@mapRoutes, ->middleware('tenant') call).

This means that routes in tenant.php are treated as tenant routes, and routes in any other files are treated as central.

In other words, there's no need to add the central flag to individual routes, as RouteMode::CENTRAL is used by default. And there's no need to add the tenant flag to your routes in routes/tenant.php, as that entire file has the tenant middleware applied on it.

Integrating with packages

If you're integrating with third-party packages, you may need a different default route mode. This depends on the level of control you have over the package's routes.

Say that you're integrating a package that needs early identification (because it uses Illuminate\Routing\Controller-based controllers that inject some service in their constructor).

If the package lets you configure the middleware it uses on its routes, you can simply add tenant to that list.

If the package doesn't let you do that, you'll need to use the RouteMode::TENANT default route mode (so that routes that don't have tenant/central/universal explicitly defined are considered tenant). This will make Tenancy consider the package's routes to be tenant routes and tenancy will be initialized when they're used. You'll just have to remember to add the central flag to any central routes (e.g. routes/web.php and routes/api.php) since tenancy will consider all routes that don't have any explicit flag to be tenant.

And if you want to use the package's routes in both the central part of your app and the tenant part of your app, you can use RouteMode::UNIVERSAL. That way tenancy will be initialized whenever the tenant is specified in some way (that matches what the identification middleware you added to the kernel looks for, as explained in the Default route mode section).

1

Under the hood, this is simply an empty middleware group. central, tenant, and universal are all defined like this, and they are reserved middleware by the package. Therefore you shouldn't define them in your app to make sure you don't override them.

2

With path identification, you'll also need to use the CloneRoutesAsTenant action to create a copy of the central route prefixed with /{tenant}.

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

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

    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.

You might be thinking that we could use an optional parameter:

/{tenant?}/posts

However, due to how Laravel's optional parameters work, it's not possible to use an optional argument for general prefixes like /{tenant}/ that will be followed by more route segments and parameters.

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@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

Terminology

This page covers some of the common terminology we use across this documentation. Most of the concepts are explained on their respective pages, but this page aims to provide a list of todo

TermMeaning
Route-level middlewareMiddleware defined on a specific route, either directly or using route groups. Opposite of kernel middleware
Kernel middlewareMiddleware defined in App\Http\Kernel@$middleware. Used with early identification
Universal routeA route usable in both the central app and the tenant app. It has the identification middleware but it only triggers when the tenant is actually provided in some way.
Tenant appThe tenant part of the application (tenant is identified and tenancy is initialized upon visit)
Central appThe central part of the application (tenancy is not used here, typically part of the app includes the onboarding logic, some central admin panel for managing tenants, and similar)
Tenant identificationThe process of identifying a tenant based on the request. Handled by identification middleware
Tenancy initializationThe process of initializing tenancy (switching the application context to that of a specific tenant)
Application contextCan be central or tenant. When the application is in the central context, it works like a normal Laravel app. When it's in the tenant context, Tenancy Bootstrappers scope various Laravel components to the current tenant
Tenancy BootstrapperA class that manages scoping certain logic (either a part of Laravel or a third-party package) to the tenant context and vice versa. Implements the Stancl\Tenancy\Contracts\TenancyBootstrapper interface and is registered in tenancy.bootstrappers
Tenancy FeatureA class that adds bootstraps additional functionality (optional features). Implements Stancl\Tenancy\Contracts\Feature and is registered in tenancy.features