Laravel Multi-Tenancy: Data Models & Tenant Context
This article explains how to choose a data model for Laravel multi-tenancy and how to keep tenant context throughout your application. You have three main architectural choices—single database with a tenant column, separate databases per tenant, or schema-based separation—and the right one depends on your data isolation, scalability, and cost requirements. We’ll cover how each model works, how to maintain tenant context in Laravel, and the trade-offs you need to weigh.
Background: Why Multi-Tenancy Matters
Multi-tenancy means a single application instance serves multiple customers (tenants), each with their own data. In Laravel, this is common for SaaS products where each client needs their own workspace, users, and records. The core challenge is ensuring data isolation—tenant A must never see tenant B’s data—while keeping the codebase manageable. Laravel doesn’t have built-in multi-tenancy, but its service container, middleware, and Eloquent scopes make it straightforward to implement. The choice of data model affects everything from database backups to query performance and even your pricing model.
The Three Main Data Models
There are three widely used approaches, each with distinct characteristics:
- Single database, shared tables with a tenant_id column – The simplest model. Every table that holds tenant-specific data has a column like
tenant_id. Queries filter by that column. This is easy to set up and maintain, and it’s the most cost-effective because you only need one database server. - Separate database per tenant – Each tenant gets its own database. This gives the strongest isolation and makes it easy to restore or migrate a single tenant. However, it requires more database connections and can be expensive as you scale. You also need to manage migrations across all tenant databases.
- Schema-based separation – A single database, but each tenant has its own schema (e.g.,
tenant1,tenant2). This is a middle ground: you get decent isolation and can share the database server, but you still need to manage multiple schemas. This is less common in Laravel because it requires more manual schema management.
Most Laravel SaaS apps start with the shared-table approach because it’s the easiest to develop and debug. As you grow, you might migrate to separate databases for high-value customers who need stronger compliance or performance guarantees.
How to Keep Tenant Context in Laravel
Tenant context means every query and action knows which tenant is active. The standard way is to resolve the tenant from the request—usually from a subdomain, a custom header, or the authenticated user’s relation. Then you store that tenant in the application container so it’s available globally.
A common pattern is to create a Tenant model and a middleware that sets the current tenant:
- Create a
Tenantmodel with aslugoruuidfield. - In a middleware, parse the subdomain (e.g.,
acme.myapp.com) and find the tenant. - Bind the tenant into the container:
app()->instance('currentTenant', $tenant); - In your models, add a global scope that filters by
tenant_id. - When creating records, automatically set
tenant_idfrom the current tenant, often in a model event or a trait.
For example, using a global scope in a base model:
class TenantScopedModel extends Model
{
protected static function booted()
{
static::addGlobalScope('tenant', function (Builder $builder) {
if (app()->bound('currentTenant')) {
$builder->where('tenant_id', app('currentTenant')->id);
}
});
}
}
This ensures that every query automatically includes the tenant filter, so you don’t have to manually add where('tenant_id', ...) everywhere. When creating a record, you can override the creating event to set the tenant ID.
Choosing the Right Model for Your SaaS
There’s no one-size-fits-all answer. Here are the factors to consider:
- Data sensitivity – If your tenants are competitors or handle regulated data (like health records), you might need separate databases to guarantee isolation.
- Scale – A shared table can handle thousands of tenants, but as your dataset grows, queries might slow down without proper indexing. Separate databases scale better horizontally.
- Cost – Separate databases mean more server resources and potentially higher cloud bills. A shared table is the cheapest.
- Development speed – Shared tables are the fastest to implement and test. Separate databases add complexity because you need to run migrations for each tenant database.
- Backup and restore – With separate databases, you can back up a single tenant easily. With shared tables, you need to backup the whole database and restore selectively, which is harder.
A practical approach is to start with shared tables and structure your code so you can switch to separate databases later if needed. Use an interface for tenant resolution and a service class for tenant management, so the data model is encapsulated.
Common Problems and How to Avoid Them
One of the most common mistakes is forgetting to add the tenant scope to a new model. This can lead to data leaks. To mitigate this, use a trait or a base model that all tenant-scoped models extend. Also, be careful with relationships: if you eager load a relation, you must ensure the related model also has the tenant scope, otherwise you might pull data from other tenants.
Another issue is caching. If you cache data that includes tenant-specific content, you need to include the tenant ID in the cache key. For example, Cache::put('user_list_' . $tenant->id, ...). Otherwise, one tenant might see another’s cached data.
When using separate databases, you need to handle migrations carefully. You can use a queue to run migrations on all tenant databases, but this adds complexity. Also, you need to manage database connections dynamically, often by setting the connection based on the tenant.
Comparison: Shared Tables vs. Separate Databases
Let’s compare the two most common approaches side by side:
- Isolation – Separate databases offer perfect isolation; shared tables rely on application logic to filter correctly.
- Performance – Shared tables can become slow if you have millions of rows per tenant and don’t index properly. Separate databases give you natural partitioning.
- Maintenance – Shared tables are easier to maintain; you only have one schema to update. Separate databases require you to update every tenant database.
- Cost – Shared tables are cheaper; you pay for one database server. Separate databases require more resources, especially if each tenant has its own connection.
- Onboarding – With shared tables, creating a new tenant is just inserting a row. With separate databases, you need to create the database, run migrations, and set up the connection.
Schema-based separation is a hybrid: it gives you some isolation and can be cheaper than separate databases, but it’s rarely used in Laravel because it’s not natively supported and adds complexity.
Safety and Security Considerations
Data isolation is a security feature. If you use shared tables, you must ensure that every query is scoped. A single missing scope can expose all tenants’ data. To reduce risk, use a global scope on a base model and avoid raw queries unless you explicitly add the tenant condition. Also, be cautious with mass assignment: make sure tenant_id is not fillable from user input.
For separate databases, you need to ensure that database credentials are stored securely and that you never leak a connection to another tenant. Use Laravel’s configuration to set the connection dynamically, but never expose the credentials in logs.
Another safety point is backup. With shared tables, backing up the entire database is simple. With separate databases, you need to script backups for each tenant database. Also, consider that restoring a single tenant from a shared table backup requires filtering by tenant_id, which can be tricky if you have foreign keys.
Frequently Asked Questions
Can I switch from shared tables to separate databases later?
Yes, but it’s not trivial. You’ll need to write a migration script that creates a new database for each tenant and copies the data. The process is time-consuming and error-prone, so it’s best to decide early. However, if you keep your code abstracted (e.g., using a repository pattern), the switch is more manageable.
How do I handle tenant-specific configuration like themes or settings?
Store tenant settings in a settings table with a tenant_id column. Use a cache key that includes the tenant ID. For example, Cache::remember('settings_' . $tenant->id, ...). This way, each tenant gets its own settings without interfering with others.
What is the best way to identify the tenant in a request?
The most common method is using a subdomain, like acme.myapp.com. You can also use a custom header for API requests, or the authenticated user’s relationship to a tenant. For a single-tenant-per-user model, you can store the tenant ID in the session after login.
Does Laravel have built-in multi-tenancy support?
No, Laravel does not have built-in multi-tenancy. You implement it yourself or use third-party packages like stancl/tenancy or romegasoftware/laravel-multi-tenancy. These packages provide scaffolding for tenant resolution, database management, and scoping.
How do I run migrations for separate tenant databases?
You can use a command that iterates over all tenants and runs php artisan migrate --database=tenant with the appropriate connection. Some packages handle this automatically. Alternatively, you can use a queue to run migrations in parallel, but be careful about locking issues.
What are the performance implications of using global scopes?
Global scopes add a WHERE clause to every query, which can slightly slow down queries if the column isn’t indexed. Make sure to add an index on tenant_id in all tables that use it. Also, avoid over-scoping: don’t add a global scope to tables that are truly global, like a countries table.
Related
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.