Version 4
Early identification
Early identification solves an issue where controller constructors may run before the tenancy middleware, resulting in incorrect dependencies getting injected.
The main idea behind early identification is using the kernel middleware stack instead of the route middleware stack. However, this will apply to all of your routes, making it difficult to use multiple identification middleware, central routes, and universal routes.
Version 4 solves this by introducing a very detailed implementation of early identification. It works like this:
- You add the tenant identification middleware to the kernel middleware stack:
- You choose whether you want routes to be central, tenant, or universal by default:
- Now when you make a request to any route, tenancy will be initialized.
- To disable tenancy for certain routes, you can use the
'central'
middleware. - Alternatively, you can set the default route mode to central and then you’d be using the
'tenant'
middleware to mark routes as tenant-aware.
Early identification is typically not needed when developing a multi-tenant application from scratch, but it becomes very useful when adding multi-tenancy to an existing codebase or especially integrating with third-party packages.
PostgreSQL RLS
Our implementation of PostgreSLQ RLS is a new take on single-database tenancy:
- all of your data is in a single database,
- the usage inside your Laravel app is closer to multi-db than single-db.
Essentially, each tenant gets a new DB user with some PostgreSQL policies created for scoping all SQL queries and statements such that the user can only modify their own data.
Our implementation is highly configurable and extensible, but the main setup is our TableRLSManager
:
- it scans your database schema and looks for relationships to the
tenants
table, - it will make sure any tables that are even indirectly related to a tenant can only be accessed by that tenant or in the central context. This ensures other tenants can’t access the tenant’s data.
This means that you need essentially zero code changes — just like when using multi-db — but you get to have all your data in one database.
Traditionally, the benefits of single-db were:
- easier devops (only one DB to maintain, faster deployments, …),
- easier queries spanning multiple tenants.
While the benefits of multi-db were:
- easier implementation (very few code changes needed),
- more robust data separation.
PostgreSQL RLS comes with essentialy the best of both worlds. Keep in mind that it’s still an experimental feature, however.
Path identification improvements
v4 improves path identification mainly by introducing route cloning. Route cloning is a feature that lets you clone specified routes to create their tenant counterpart. This is helpful when integrating with third-party packages, where you have no control over how the routes are registered, and you can at most define some middleware in the package’s config.
Route cloning lets you have both the central and tenant versions of the route available. For instance:
Additionally, you can now choose to use a different column of the Tenant model in your route parameters:
Setting tenant_model_column
to e.g. slug
will use $tenant->slug
for {tenant}
route parameters, instead of the tenant key.
You can also use this syntax:
However, you need to whitelist all columns you use in this way by adding them to allowed_extra_model_columns
(see above).
Resource syncing rework
Syncable models get deleted when their respective SyncMaster is deleted
All Syncable
(tenant) models now get deleted when their respective SyncMaster
(central) model is deleted:
Directorycentral_db
Directoryusers
- 1 global_id: foo
- 2 global_id: bar
- 3 global_id: baz
Directorytenant1_db
Directoryusers
- 1 global_id: bar
- 2 global_id: foo
- 3 global_id: baz
Directorytenant2_db
Directoryusers
- 1 global_id: foo
- 2 global_id: baz
In this example, deleting central users 2
and 3
would delete users 1
and 3
in tenant1
, and user 2
in tenant2
.
To disable this logic, remove the following listener from your TenancyServiceProvider
:
Soft deletes, force deletes, and restores are now synced (only top-down)
Continuing the above, when soft deletes are used, SyncMasterDeleted
is fired and DeleteResourcesInTenants
is executed — same as with normal deletes — except that delete()
, which is called in DeleteResourcesInTenants
, only soft deletes the models.
When a SyncMaster is force deleted, the same SyncMasterDeleted
event is fired, but with the forceDelete
property set to true, which leads to the tenant counterpart models getting force deleted:
This covers deletes and force deletes when soft deletes are used. The only remaining action is restores, for which we have a separate event and listener:
Attributes used for creation can now be defined separately from attributes used for updates
This solves an issue where either the tenant or central context may have a significantly more complex version of the synced model, resulting in missing fields when a resource is created as a result of syncing (e.g. being attached to a tenant).
Lazier syncing
Syncing events now only fire when there are changes to the synced columns. Changes to other columns will not trigger syncing.
Polymorphic relations
In version 4, the mappings between synced resources and tenants can be polymorphic. This means that if you have multiple tables you sync between the central and tenant databases, you don’t need to create a new table for each mapping.
todo
Schema dump
You can now squash tenant migrations:
This produces a database/schema/tenant-schema.dump
file, which is by default passed to the tenants:migrate
command as --schema-path
. In other words, the tenants:migrate
command looks for this file out of the box, with no extra configuration needed on your end.
You can also use the --prune
argument to delete all of your tenant migrations, while keeping your central migrations untouched. Keep in mind that you should only use this if all of your tenants are fully migrated:
Microsoft SQL Server support
There are now tenant database managers for Microsoft SQL Server: a regular version and a permission-controlled version:
Laravel Scout integration
There’s now a first-party integration bootstrapper for Laravel scout:
It automatically sets the scout.prefix
config to the tenant key of the current tenant, with no configuration needed.
Improved URL generation
The InitializeTenancyByPath
middleware now automatically sets URL::defaults()
for the 'tenant'
parameter, making route generation easy — you don’t need to specify the 'tenant'
when generating a route to a tenant route:
v4 also comes with the UrlGeneratorBootstrapper
which replaces Laravel’s URL generator to produce tenant-aware URLs when you’re in the tenant context.
This is especially used in combination with route cloning, where you may have a route called foo
that’s central and a clone called tenant.foo
that’s
tenant-aware. Based on what context you’re in, route('foo')
can produce a link to either foo
or tenant.foo
.
Improved tenants:run
The tenants:run
command now correctly handles stdin from subcommands.
Listeners for creating and deleting tenant storage
There are now first-party listeners for managing tenant storage folders upon tenant creation and deletion:
CreateTenantStorage
for creating the tenant’s storage folder upon tenant creationDeleteTenantStorage
for deleting the tenant’s storage folder upon tenant deletion
All you need to do to enable them is uncomment these lines in your TenancyServiceProvider
:
Added JobBatchBootstrapper
This bootstrapper adds support for running queued tenant jobs in batches by properly resetting the database connection used by the DatabaseBatchRepository
. It is enabled by default:
Storage::url() support
This feature adds support for symlinking local tenant disks into public/
. It can be used either using events:
or by calling the tenants:link
command:
Use --force
if some tenants already have links.
To remove these links, use tenants:link --remove
:
Tenant-specific maintenance mode improvements
Version 4 adds dedicated commands for putting tenants into maintenance mode and taking them out of maintenance mode:
The command accepts the same arguments as php artisan down
, with the exception of --render
:
Added Tenant::current() methods
Two new methods have been added to the base Tenant
model:
Dropping tenant databases on migrate:fresh
Set the tenancy.database.drop_tenant_databases_on_migrate_fresh
config key to true
to drop tenant databases on php artisan migrate:fresh
.
Added cookie support to InitializeTenancyByRequestData
The InitializeTenancyByRequestData
middleware can now identify tenants using cookies.
Added pending tenants
Pending tenants is a feature that lets you maintain a pool of ready-to-use, pre-made tenants for any tenant that might sign up.
This can simplify your tenant onboarding logic, by not requiring a queued onboarding flow like we use in the v3 SaaS boilerplate. Instead, everything can happen completely synchronously since pending tenants have pre-created databases.
Added DatabaseSessionBootstrapper
This bootstrapper makes the database
session driver compatible with the package.
Impersonation improvements
v4 makes three improvements to the impersonation logic:
- There’s now session state indicating whether impersonating is taking place. This can be used for adding something like a banner to the UI to make it clear that the user is logged in via impersonation.
- The package now enforces that stateful guards must be used with impersonation tokens.
- Impersonation sessions can now have the
remember
parameter set
Added a dedicated feature for tenant-specific mail credentials
v4 adds MailConfigBootstrapper
which maps tenant properties to mail config and resets mail-related
singletons in the service container, letting you easily have reliable tenant-specific mail configuration.
It’s configured similarly to the TenantConfig
feature.
Added skip-failing option to tenants:migrate
You can now use --skip-failing
when running php artisan tenants:migrate
to ignore tenants that haven’t been fully created yet (i.e. their database doesn’t exist).
Support for defining the tenant connection template using array syntax
Previously, you had to create a connection in config/database.php
and reference it in your Tenancy config:
Now, you can define the template:
- Fully (same contents as what you’d put into a connection config):
- Partially (the rest will be taken from the central connection):
- By name (same as in v3):
Added RootUrlBootstrapper
This bootstrapper lets you change the app.url
in CLI context. This is useful e.g. for sending email. When routes are being generated in web context, they’re based on the request hostname. When they’re being generated in a CLI process (like a queue worker), there’s no request hostname, so Laravel defaults to the configured app.url
. This results in URLs generated in emails pointing to the central domain, even when tenancy is initialized.
To solve this, you can use the RootUrlBootstrapper
:
See the overrideUrlInTenantContext
method in your TenancyServiceProvider
for a more generic implementation.
Tenant-specific broadcasting
Version 4 adds two approaches for tenant-specific broadcasting:
- Tenant-specific broadcasting keys
- Prefixed channel names
For details, see the Broadcasting page of the documentation.
Added cache prefixing bootstrapper
This is a more generic implementation of our CacheTenancyBootstrapper
from v3 (which has now been renamed to CacheTagsBootstrapper
). The v3 bootstrapper only worked with cache drivers that supported caching, like Redis, and worked by applying cache tags.
The bootstrapper in v4 works by prefixing the configured cache stores. Prefixing is a better approach since it supports more drivers and results in a cleaner structure inside e.g. Redis, making it easy to view the entire cache of any given tenant.
Added single-domain tenants
In many applications, tenants will only have a single domain, making the default HasMany
relation excessive. For this, we’ve introduced a new interface: SingleDomainTenant
. Tenant models implementing this interface are expected to have a custom domain
column storing the tenant’s domain.
Added origin header identification
Version 4 adds a new identification middleware: InitializeTenancyByOriginHeader
. This can be an especially useful approach for SPAs, since you can specify the tenant by controlling what domain you make the request from, rather than having to pass the tenant to each request manually.
And since you can make requests to relative paths, you often won’t need to specify the tenant at all.
This feature leverages multiple aspects of how browsers work, letting you set up SPA client sites like this:
- Host the frontend of the client site on
tenant1.com
/tenant1.yourapp.com
- Make calls to
/api/foo
- The tenant will be identified using
tenant1.com
/tenant1.yourapp.com
, since the browser automatically adds theOrigin
header.
On the database level, your tenants will be structured as if you were using domain identification. On the middleware level, it will work similar to request data identification (it will read the Origin
header but match it against the tenant’s domain instead of the tenant key), and on the frontend, you won’t have to specify the tenant manually at all.