Introduction
Multi-tenancy is a concept where a single instance of an application serves multiple customers. Each customer has their own data and access rules that prevent them from viewing or modifying each other’s data. This is a common pattern in SaaS applications. Users often belong to groups of users (often called teams or organizations). Records are owned by the group, and users can be members of multiple groups. This is suitable for applications where users need to collaborate on data. Multi-tenancy is a very sensitive topic. It’s important to understand the security implications of multi-tenancy and how to properly implement it. If implemented partially or incorrectly, data belonging to one tenant may be exposed to another tenant. Filament provides a set of tools to help you implement multi-tenancy in your application, but it is up to you to understand how to use them.Simple one-to-many tenancy
The term “multi-tenancy” is broad and may mean different things in different contexts. Filament’s tenancy system implies that the user belongs to many tenants (organizations, teams, companies, etc.) and may switch between them. If your case is simpler and you don’t need a many-to-many relationship, then you don’t need to set up the tenancy in Filament. You could use observers and global scopes instead. Let’s say you have a database columnusers.team_id, you can scope all records to have the same team_id as the user using a global scope:
team_id on the record when it’s created, you can create an observer:
Setting up tenancy
To set up tenancy, you’ll need to specify the “tenant” (like team or organization) model in the configuration:HasTenants interface on the App\Models\User model:
teams() relationship. The getTenants() method returns the teams that the user belongs to. Filament uses this to list the tenants that the user has access to.
For security, you also need to implement the canAccessTenant() method of the HasTenants interface to prevent users from accessing the data of other tenants by guessing their tenant ID and putting it into the URL.
You’ll also want users to be able to register new teams.
Adding a tenant registration page
A registration page will allow users to create a new tenant. When visiting your app after logging in, users will be redirected to this page if they don’t already have a tenant. To set up a registration page, you’ll need to create a new page class that extendsFilament\Pages\Tenancy\RegisterTenant. This is a full-page Livewire component. You can put this anywhere you want, such as app/Filament/Pages/Tenancy/RegisterTeam.php:
form() method, and create the team inside the handleRegistration() method.
Now, we need to tell Filament to use this page. We can do this in the configuration:
Customizing the tenant registration page
You can override any method you want on the base registration page class to make it act as you want. Even the$view property can be overridden to use a custom view of your choice.
Adding a tenant profile page
A profile page will allow users to edit information about the tenant. To set up a profile page, you’ll need to create a new page class that extendsFilament\Pages\Tenancy\EditTenantProfile. This is a full-page Livewire component. You can put this anywhere you want, such as app/Filament/Pages/Tenancy/EditTeamProfile.php:
form() method. They will get saved directly to the tenant model.
Now, we need to tell Filament to use this page. We can do this in the configuration:
Customizing the tenant profile page
You can override any method you want on the base profile page class to make it act as you want. Even the$view property can be overridden to use a custom view of your choice.
Accessing the current tenant
Anywhere in the app, you can access the tenant model for the current request usingFilament::getTenant():
Billing
Using Laravel Spark
Filament provides a billing integration with Laravel Spark. Your users can start subscriptions and manage their billing information. To install the integration, first install Spark and configure it for your tenant model. Now, you can install the Filament billing provider for Spark using Composer:tenantBillingProvider():
Requiring a subscription
To require a subscription to use any part of the app, you can use therequiresTenantSubscription() configuration method:
Requiring a subscription for specific resources and pages
Sometimes, you may wish to only require a subscription for certain resources and custom pages in your app. You can do this by returningtrue from the isTenantSubscriptionRequired() method on the resource or page class:
requiresTenantSubscription() configuration method, then you can return false from this method to allow access to the resource or page as an exception.
Writing a custom billing integration
Billing integrations are quite simple to write. You just need a class that implements theFilament\Billing\Providers\Contracts\Provider interface. This interface has two methods.
getRouteAction() is used to get the route action that should be run when the user visits the billing page. This could be a callback function, or the name of a controller, or a Livewire component - anything that works when using Route::get() in Laravel normally. For example, you could put in a simple redirect to your own billing page using a callback function.
getSubscribedMiddleware() returns the name of a middleware that should be used to check if the tenant has an active subscription. This middleware should redirect the user to the billing page if they don’t have an active subscription.
Here’s an example billing provider that uses a callback function for the route action and a middleware for the subscribed middleware:
Customizing the billing route slug
You can customize the URL slug used for the billing route using thetenantBillingRouteSlug() method in the configuration:
Customizing the tenant menu
The tenant-switching menu is featured in the admin layout. It’s fully customizable. Each menu item is represented by an action, and can be customized in the same way. To register new items, you can pass the actions to thetenantMenuItems() method of the configuration:
Allowing the tenants to be searched
You can use thesearchableTenantMenu() method in the configuration to allow the tenants to be searched:
searchableTenantMenu(false).
Customizing the registration link
To customize the registration link in the tenant menu, register a new item with theregister array key, and pass a function that customizes the action object:
Customizing the profile link
To customize the user profile link at the start of the tenant menu, register a new item with theprofile array key, and pass a function that customizes the action object:
Customizing the billing link
To customize the billing link in the tenant menu, register a new item with theprofile array key, and pass a function that customizes the action object:
Conditionally hiding tenant menu items
You can also conditionally hide a tenant menu item by using thevisible() or hidden() methods, passing in a condition to check. Passing a function will defer condition evaluation until the menu is actually being rendered:
Sending a POST HTTP request from a tenant menu item
You can send a POST HTTP request from a tenant menu item by passing a URL to the url() method, and also using postToUrl():
Disabling the tenant switcher
By default, users can switch between tenants using the tenant menu. If you want to keep the tenant menu visible but prevent users from switching tenants, you can use thetenantSwitcher() method in the configuration:
Hiding the tenant menu
You can hide the tenant menu by using thetenantMenu(false)
Setting up avatars
Out of the box, Filament uses ui-avatars.com to generate avatars based on a user’s name. However, if your user model has anavatar_url attribute, that will be used instead. To customize how Filament gets a user’s avatar URL, you can implement the HasAvatar contract:
getFilamentAvatarUrl() method is used to retrieve the avatar of the current user. If null is returned from this method, Filament will fall back to ui-avatars.com.
You can easily swap out ui-avatars.com for a different service, by creating a new avatar provider. You can learn how to do this here.
Configuring the tenant relationships
When creating and listing records associated with a Tenant, Filament needs access to two Eloquent relationships for each resource - an “ownership” relationship that is defined on the resource model class, and a relationship on the tenant model class. By default, Filament will attempt to guess the names of these relationships based on standard Laravel conventions. For example, if the tenant model isApp\Models\Team, it will look for a team() relationship on the resource model class. And if the resource model class is App\Models\Post, it will look for a posts() relationship on the tenant model class.
Customizing the ownership relationship name
You can customize the name of the ownership relationship used across all resources at once, using theownershipRelationship argument on the tenant() configuration method. In this example, resource model classes have an owner relationship defined:
$tenantOwnershipRelationshipName static property on the resource class, which can then be used to customize the ownership relationship name that is just used for that resource. In this example, the Post model class has an owner relationship defined:
Customizing the resource relationship name
You can set the$tenantRelationshipName static property on the resource class, which can then be used to customize the relationship name that is used to fetch that resource. In this example, the tenant model class has an blogPosts relationship defined:
Configuring the slug attribute
When using a tenant like a team, you might want to add a slug field to the URL rather than the team’s ID. You can do that with theslugAttribute argument on the tenant() configuration method:
Configuring the name attribute
By default, Filament will use thename attribute of the tenant to display its name in the app. To change this, you can implement the HasName contract:
getFilamentName() method is used to retrieve the name of the current user.
Setting the current tenant label
Inside the tenant switcher, you may wish to add a small label like “Active team” above the name of the current team. You can do this by implementing theHasCurrentTenantLabel method on the tenant model:
Setting the default tenant
When signing in, Filament will redirect the user to the first tenant returned from thegetTenants() method.
Sometimes, you might wish to change this. For example, you might store which team was last active, and redirect the user to that team instead.
To customize this, you can implement the HasDefaultTenant contract on the user:
Applying middleware to tenant-aware routes
You can apply extra middleware to all tenant-aware routes by passing an array of middleware classes to thetenantMiddleware() method in the panel configuration file:
true as the second argument to the tenantMiddleware() method:
Adding a tenant route prefix
By default the URL structure will put the tenant ID or slug immediately after the panel path. If you wish to prefix it with another URL segment, use thetenantRoutePrefix() method:
/admin/1 for tenant 1. Now, it is /admin/team/1.
Using a domain to identify the tenant
When using a tenant, you might want to use domain or subdomain routing liketeam1.example.com/posts instead of a route prefix like /team1/posts . You can do that with the tenantDomain() method, alongside the tenant() configuration method. The tenant argument corresponds to the slug attribute of the tenant model:
domain attribute should contain a valid domain host, like example.com or subdomain.example.com.
When using a parameter for the entire domain (
tenantDomain('{tenant:domain}')), Filament will register a global route parameter pattern for all tenant parameters in the application to be [a-z0-9.\-]+. This is because Laravel does not allow the . character in route parameters by default. This might conflict with other panels using tenancy, or other parts of your application that use a tenant route parameter.Disabling tenancy for a resource
By default, all resources within a panel with tenancy will be scoped to the current tenant. If you have resources that are shared between tenants, you can disable tenancy for them by setting the$isScopedToTenant static property to false on the resource class:
Disabling tenancy for all resources
If you wish to opt-in to tenancy for each resource instead of opting-out, you can callResource::scopeToTenant(false) inside a service provider’s boot() method or a middleware:
$isScopedToTenant static property to true on a resource class:
Tenancy security
It’s important to understand the security implications of multi-tenancy and how to properly implement it. If implemented partially or incorrectly, data belonging to one tenant may be exposed to another tenant. Filament provides a set of tools to help you implement multi-tenancy in your application, but it is up to you to understand how to use them. Filament does not provide any guarantees about the security of your application. It is your responsibility to ensure that your application is secure. Below is a list of features that Filament provides to help you implement multi-tenancy in your application:-
Automatic global scoping of Eloquent model queries for tenant-aware resources that belong to the panel with tenancy enabled. The query used to fetch records for a resource is automatically scoped to the current tenant. This query is used to render the resource’s list table, and is also used to resolve records from the current URL when editing or viewing a record. This means that if a user attempts to view a record that does not belong to the current tenant, they will receive a 404 error.
- A tenant-aware resource has to exist in the panel with tenancy enabled for the resource’s model to have the global scope applied. If you want to scope the queries for a model that does not have a corresponding resource, you must use middleware to apply additional global scopes for that model.
- The global scopes are applied after the tenant has been identified from the request. This happens during the middleware stack of panel requests. If you make a query before the tenant has been identified, such as from early middleware in the stack or in a service provider, the query will not be scoped to the current tenant. To guarantee that middleware runs after the current tenant is identified, you should register it as tenant middleware.
- As per the point above, queries made outside the panel with tenancy enabled do not have access to the current tenant, so are not scoped. If in doubt, please check if your queries are properly scoped or not before deploying your application.
- If you need to disable the tenancy global scope for a specific query, you can use the
withoutGlobalScope(filament()->getTenancyScopeName())method on the query. - If any of your queries disable all global scopes, the tenancy global scope will be disabled as well. You should be careful when using this method, as it can lead to data leakage. If you need to disable all global scopes except the tenancy global scope, you can use the
withoutGlobalScopes()method passing an array of the global scopes you want to disable.
-
Automatic association of newly created Eloquent models with the current tenant. When a new record is created for a tenant-aware resource, the tenant is automatically associated with the record. This means that the record will belong to the current tenant, as the foreign key column is automatically set to the tenant’s ID. This is done by Filament registering an event listener for the
creatingandcreatedevents on the resource’s Eloquent model.- A tenant-aware resource has to exist in the panel with tenancy enabled for the resource’s model to have the automatic association to happen. If you want automatic association for a model that does not have a corresponding resource, you must register a listener for the
creatingevent for that model, and associate thefilament()->getTenant()with it. - The events run after the tenant has been identified from the request. This happens during the middleware stack of panel requests. If you create a model before the tenant has been identified, such as from early middleware in the stack or in a service provider, it will not be associated with the current tenant. To guarantee that middleware runs after the current tenant is identified, you should register it as tenant middleware.
- As per the point above, models created outside the panel with tenancy enabled do not have access to the current tenant, so are not associated. If in doubt, please check if your models are properly associated or not before deploying your application.
- If you need to disable the automatic association for a particular model, you can mute the events temporarily while you create it. If any of your code currently does this or removes event listeners permanently, you should check this is not affecting the tenancy feature.
- A tenant-aware resource has to exist in the panel with tenancy enabled for the resource’s model to have the automatic association to happen. If you want automatic association for a model that does not have a corresponding resource, you must register a listener for the
unique and exists validation
Laravel’s unique and exists validation rules do not use Eloquent models to query the database by default, so it will not use any global scopes defined on the model, including for multi-tenancy. As such, even if there is a soft-deleted record with the same value in a different tenant, the validation will fail.
If you would like two tenants to have complete data separation, you should use the scopedUnique() or scopedExists() methods instead, which replace Laravel’s unique and exists implementations with ones that uses the model to query the database, applying any global scopes defined on the model, including for multi-tenancy:
unique() and exists().
Using tenant-aware middleware to apply additional global scopes
Since only models with resources that exist in the panel are automatically scoped to the current tenant, it might be useful to apply additional tenant scoping to other Eloquent models while they are being used in your panel. This would allow you to forget about scoping your queries to the current tenant, and instead have the scoping applied automatically. To do this, you can create a new middleware class likeApplyTenantScopes:
handle() method, you can apply any global scopes that you wish: