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:
- Identifying a tenant (generally done using request middleware)
- Making Laravel components scope everything to that tenant's context
- 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) command7d98ebb#807a1a976c#985cb7567a#1015ea19117#1019 - Add Microsoft SQL Server support
cc6d4fe#715 - Add
ScoutTenancyBootstrapperf83504a#936 - Set default value for
tenantURL parameters when a tenant is identified by pathf2c6408#925 - Make
tenants:runhandle stdin prompts from subcommands409190f#923 - Add optional
DeleteTenantStoragelistenerab5fa7a#938 - Add
BatchTenancyBootstrapperb78320b#874 - Add
Storage::url()support7bacc50#909a7ad828 - Add tenant-specific maintenance mode
121370e#761 - Add
current()andcurrentOrFail()tenant methods42dab29#970 - Add option for dropping tenant databases on
migrate:fresh080b271#971 - Add maintenance mode events
3f60c4a#979 - Add cookie option on InitializeTenancyByRequestData
05f1b2d#980 - Add pending tenants1
198f34f#8697d3298c#1025 - Add
SessionTenancyBootstrapper(adds support fordatabasesession driver)5849089 - Add session state when impersonating tenant
f42f08c#1029 - Add a dedicated feature for tenant-specific mail credentials
0f892f1#989 - Add
skip-failingoption totenants:migratecommand342c67f#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#1044fbdb13f#1068719b1be#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
$onFailin the Universal Routes feature233a122#679 - Change how
tenants:runis used with argumentsa45aa8e#912 #686 - Stronger typing, minor changes to the
Tenantinterface55d0a9a8af354c87212e5d463e2da94227af98a901942d79c - Improved namespacing, all database-related logic was moved to namespaces under
Stancl\Tenancy\Database40bf28c - Refactor TenantDatabaseManagers
d2e1ce0 - Fix/improve Ignition solutions
55d0a9a3542b3f - 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
BelongsToTenanttrait (deprecated in PHP 8.1), replace it with a config key24146b2e5bc8dd - Refactor more static properties to config keys
ccaba05 - Improve CLI command outputs using Laravel 9+ components
e4f5b92#9685d688e668de360#1030 - Change
TenantDatabaseManager-related interfaces, add publicdatabase(): Connectionmethodfe0a322 - Make
tenants:migratedefault to configured schema patha1a976c#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#993ea3e445#992,758fbc8#997 (adds polymorphic table for mapping resources to tenants) - Centralize config used by
BelongsToTenantandHasDomains, replace alltenant_idliteralsdd0f03f2a39b05(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
rememberbool column toImpersonationToken228c267#1101
Package changes
- Tests now use Pest
- Custom Dockerfile, CI no longer uses
docker-compose - We now use phpstan
- Added API docs: api.tenancyforlaravel.com
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
Controllerclass 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:
- The controller implements
HasMiddleware, or - 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_domainslist in the tenancy config) - With path identification, this means making a request to
/tenant123/fooinstead of/foo2 - With request data identification, this means providing the tenant in any of the supported formats (
X-Tenantheader,?tenantquery 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).
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.
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?}/postsHowever, 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
tenantflag (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
| Term | Meaning |
|---|---|
| Route-level middleware | Middleware defined on a specific route, either directly or using route groups. Opposite of kernel middleware |
| Kernel middleware | Middleware defined in App\Http\Kernel@$middleware. Used with early identification |
| Universal route | A 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 app | The tenant part of the application (tenant is identified and tenancy is initialized upon visit) |
| Central app | The 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 identification | The process of identifying a tenant based on the request. Handled by identification middleware |
| Tenancy initialization | The process of initializing tenancy (switching the application context to that of a specific tenant) |
| Application context | Can 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 Bootstrapper | A 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 Feature | A class that adds bootstraps additional functionality (optional features). Implements Stancl\Tenancy\Contracts\Feature and is registered in tenancy.features |