Imagine you are building a SaaS application.
One company signs up.
Then another company signs up.
Then another.
All of them use the same application, but their data must remain completely separate.
Company A should never see Company B’s:
- Users
- Orders
- Customers
- Products
- Invoices
- Reports
- Settings
This is where multi-tenancy becomes important.
Multi-tenancy is not simply adding a tenant_id column to a table.
It is an architectural decision that affects your database, authentication, authorization, queries, storage, cache, queues, APIs, and even deployment strategy.
In this article, we’ll look at how multi-tenancy works and how we can design it properly in a Laravel application.
1. What Is Multi-Tenancy?
A tenant is an isolated customer or organization using the same application.
For example:
SaaS Application
│
├── Company A
│ ├── Users
│ ├── Products
│ ├── Orders
│ └── Reports
│
├── Company B
│ ├── Users
│ ├── Products
│ ├── Orders
│ └── Reports
│
└── Company C
├── Users
├── Products
├── Orders
└── Reports
The application itself is shared.
The data is logically isolated.
For example:
users
--------------------------------
id | tenant_id | name
--------------------------------
1 | 10 | Jalis
2 | 10 | Rahim
3 | 20 | Karim
4 | 20 | Hasan
Users 1 and 2 belong to Tenant 10.
Users 3 and 4 belong to Tenant 20.
When Jalis logs in, he should only be able to access data belonging to Tenant 10.
2. Multi-Tenancy Is Different From Multi-Branch
This is an important distinction.
A tenant is usually an independent organization or customer.
A branch is usually a subdivision inside a tenant.
For example:
Tenant: ABC Corporation
│
├── Dhaka Branch
├── Chittagong Branch
└── Sylhet Branch
So the architecture could be:
Tenant
│
├── Branch
│ ├── Users
│ ├── Orders
│ └── Inventory
│
└── Branch
├── Users
├── Orders
└── Inventory
This distinction becomes extremely important in ERP, POS, HRM, inventory, and SaaS systems.
A user may have access to:
Tenant → All branches
or:
Tenant → Only one branch
Those are different authorization rules.
3. Why Do We Need Multi-Tenancy?
Without tenant isolation, you might have an application like:
users
products
orders
customers
invoices
But how do you know which company owns each record?
You need a relationship such as:
tenant_id
For example:
products
--------------------------------
id | tenant_id | name
--------------------------------
1 | 10 | Laptop
2 | 10 | Mouse
3 | 20 | Keyboard
4 | 20 | Monitor
Now the application can determine ownership.
A query like:
Product::where('tenant_id', $tenantId)->get();returns only the products belonging to that tenant.
But manually adding this condition everywhere can become dangerous.
That’s where Laravel’s global scopes and tenancy architecture become useful.
4. Multi-Tenancy Architecture Options
There are three common database strategies.
1. Shared Database + Shared Tables
2. Shared Database + Separate Schemas
3. Separate Database Per Tenant
Each has advantages and disadvantages.
5. Strategy 1 — Shared Database, Shared Tables
This is one of the most common approaches for SaaS applications.
All tenants use the same database and same tables.
Each tenant-owned table contains:
tenant_id
For example:
orders
--------------------------------
id
tenant_id
customer_id
total
status
created_at
The data might look like:
Tenant 10
Order 1
Order 2
Order 3
Tenant 20
Order 4
Order 5
Tenant 30
Order 6
Order 7
The application separates the data logically.
Advantages
- Simple infrastructure
- Lower hosting cost
- Easy database management
- Easy migrations
- Easy reporting across tenants when needed
- Good for many small and medium tenants
Disadvantages
The biggest risk is data leakage.
If one query accidentally misses:
where('tenant_id', ...)one tenant could potentially access another tenant’s data.
That’s why tenant isolation must be designed carefully.
6. Strategy 2 — Separate Schema Per Tenant
In this architecture, tenants may have separate schemas inside the same database system.
Conceptually:
Database
│
├── tenant_10
│ ├── users
│ ├── orders
│ └── products
│
├── tenant_20
│ ├── users
│ ├── orders
│ └── products
│
└── tenant_30
├── users
├── orders
└── products
The application switches the active schema depending on the tenant.
This can provide stronger separation than simply using tenant_id.
However, managing migrations and a large number of schemas can become more complicated.
7. Strategy 3 — Separate Database Per Tenant
Here each tenant gets its own database.
Application
│
├── Tenant A → Database A
├── Tenant B → Database B
└── Tenant C → Database C
For example:
tenant_10_db
tenant_20_db
tenant_30_db
Advantages
Strong data isolation.
One tenant’s database is physically separate from another tenant’s database.
This can be useful when:
- Tenants are large
- Data isolation requirements are high
- Customers require dedicated databases
- Backup/restore needs to be tenant-specific
- Compliance requirements are strict
Disadvantages
Infrastructure becomes more complicated.
You now need to manage:
Multiple Databases
Multiple Connections
Migrations
Backups
Monitoring
Provisioning
Credentials
If you have thousands of tenants, database management becomes a serious architectural concern.
8. Which Strategy Should You Choose?
There is no universal answer.
A simple decision can be:
Small / Medium SaaS
↓
Shared Database + tenant_id
Strong Isolation Requirements
↓
Separate Schema
Large Enterprise / Dedicated Infrastructure
↓
Separate Database
But the decision should also consider:
- Number of tenants
- Data volume
- Compliance
- Cost
- Backup strategy
- Performance
- Operational complexity
- Customer requirements
Don’t choose separate databases simply because it sounds more scalable.
Sometimes a well-designed shared database architecture is the better solution.
9. Designing the Tenant Table
A basic tenants table might look like:
tenants
-------------------------
id
name
slug
domain
status
created_at
updated_at
For example:
id: 10
name: ABC Corporation
slug: abc-corporation
domain: abc.example.com
status: active
The slug or domain can help identify the tenant.
10. How Do We Identify the Current Tenant?
The application needs to answer:
“Which tenant is currently making this request?”
There are several ways.
Subdomain
abc.example.com
xyz.example.com
The subdomain identifies the tenant.
Custom Domain
app.abc.com
app.xyz.com
Authenticated User
The logged-in user belongs to a tenant:
users
-------------------------
id
tenant_id
name
Then:
$user->tenant_id
can identify the current tenant.
Session
The application can also store the selected tenant in the session.
The correct method depends on the product architecture.
11. Tenant Context
One useful pattern is to establish the current tenant early in the request lifecycle.
For example:
HTTP Request
↓
Identify Tenant
↓
Set Tenant Context
↓
Authentication
↓
Authorization
↓
Controller
↓
Business Logic
The rest of the application can then access the current tenant context.
Conceptually:
$tenant = app(CurrentTenant::class)->get();
The exact implementation can vary.
The important idea is:
Tenant identification should happen centrally, not independently inside every controller.
12. Global Scopes Can Help
Suppose we have:
products
-------------------------
id
tenant_id
name
price
Instead of writing:
Product::where('tenant_id', $tenantId)->get();everywhere, we can use a global scope.
Conceptually:
protected static function booted(): void
{
static::addGlobalScope('tenant', function ($query) {
$query->where(
'tenant_id',
app(CurrentTenant::class)->id()
);
});
}
Now:
Product::all();
automatically becomes tenant-aware.
This can significantly reduce accidental data leakage.
However, global scopes must be used carefully.
13. Don’t Trust the Frontend
This is a very important security principle.
Never assume that because the frontend sends:
{
"tenant_id": 10
}the user should be allowed to use Tenant 10.
A malicious user could modify the request:
{
"tenant_id": 20
}Tenant ownership should be determined from trusted server-side context.
For example:
Authenticated User
↓
User's Tenant
↓
Current Tenant
↓
Query Data
Not:
Frontend tenant_id
↓
Trust it
14. Tenant-Aware Models
In a shared database architecture, you can create a common pattern for tenant-owned models.
For example:
TenantModel
↓
Product
Order
Customer
Invoice
Payment
The base model can handle common tenant behavior.
Conceptually:
abstract class TenantModel extends Model
{
protected static function booted(): void
{
static::creating(function ($model) {
$model->tenant_id = app(CurrentTenant::class)->id();
});
}
}
Then:
class Product extends TenantModel
{
}
When a product is created, the tenant can be automatically assigned.
Again, the exact architecture depends on the application.
15. Automatically Assign tenant_id
One common mistake is manually doing this everywhere:
Product::create([
'name' => $request->name,
'price' => $request->price,
'tenant_id' => auth()->user()->tenant_id,
]);
Then another developer forgets it somewhere:
Product::create([
'name' => $request->name,
'price' => $request->price,
]);
Now you may have a record without tenant ownership.
A centralized tenant assignment strategy can reduce this risk.
16. Relationships Must Also Be Tenant-Safe
Imagine:
Tenant A
Customer #1
Tenant B
Customer #2
Now an Order belongs to Customer.
A dangerous implementation might allow:
Tenant A
↓
Order
↓
Customer #2
if the application only checks the customer ID.
The application should verify ownership.
Conceptually:
Order Tenant
=
Customer Tenant
Relationships in multi-tenant applications need to respect tenant boundaries.
17. Unique Constraints Need Tenant Awareness
Suppose each tenant can have a unique product SKU.
You might think:
UNIQUE(sku)
But that means the entire database must have unique SKUs.
Maybe Tenant A and Tenant B should both be allowed to use:
SKU-100
In that case, the constraint should be:
UNIQUE(tenant_id, sku)
Now:
Tenant A → SKU-100
Tenant B → SKU-100
is allowed.
But:
Tenant A → SKU-100
Tenant A → SKU-100
is rejected.
This is a small database decision with a big architectural impact.
18. Soft Deletes and Tenancy
Suppose a product is soft deleted.
You need to consider both:
tenant_id
deleted_at
Queries should respect:
Current Tenant
+
Not Deleted
Laravel’s SoftDeletes handles the second part.
Your tenancy architecture needs to handle the first.
19. File Storage Must Also Be Tenant-Aware
Tenant isolation isn’t only about database records.
Imagine users upload:
profile.jpg
invoice.pdf
product.jpg
Don’t put every tenant’s files into the same unstructured directory.
A better structure might be:
storage/
tenants/
10/
products/
invoices/
users/
20/
products/
invoices/
users/
Now storage is logically separated.
This is especially important when generating public URLs or signed download URLs.
20. Cache Can Leak Tenant Data Too
This is an often-forgotten problem.
Imagine:
Cache::remember('products', 3600, function () {
return Product::all();
});This looks fine.
But in a multi-tenant application, Tenant A may populate the cache.
Then Tenant B could receive the same cached data.
Instead, cache keys should include tenant context:
products:tenant:10
products:tenant:20
The same principle applies to:
- Redis
- Application cache
- Query cache
- Dashboard statistics
- Permission cache
Every cached tenant-specific value needs tenant-aware keys.
21. Queues Need Tenant Context Too
Imagine a user uploads a large CSV file.
Laravel creates a queued job:
ImportProductsJob
The request already knows the tenant.
But the queue worker may process the job later.
So the job must know:
Which tenant created this job?
A common approach is to store:
tenant_id
with the job’s relevant data.
Conceptually:
User Request
↓
Tenant 10
↓
Create Job
↓
Queue
↓
Worker
↓
Restore Tenant Context
↓
Process Data
This becomes extremely important in background processing.
22. Reports Must Be Tenant-Aware
Imagine a dashboard showing:
Total Sales
Total Customers
Total Orders
Revenue
A query like:
Order::sum('total');is dangerous in a shared database.
It may calculate the total for every tenant.
The query needs to respect the current tenant.
Tenant isolation must therefore exist not only in CRUD operations but also in:
- Reports
- Analytics
- Dashboards
- Exports
- Search
- Notifications
23. APIs Need Tenant Isolation
Suppose your API has:
GET /api/orders
GET /api/customers
GET /api/products
Authentication alone is not enough.
The API also needs to determine:
Who is the user?
Which tenant does the user belong to?
What can the user access inside that tenant?
For example:
API Request
↓
Authentication
↓
Tenant Identification
↓
Permission Check
↓
Tenant-Scoped Query
↓
Response
This pattern should be consistent throughout the API.
24. Super Admin Is a Special Case
A SaaS application often has a platform-level administrator.
For example:
Platform
│
├── Tenant A
├── Tenant B
└── Tenant C
The platform admin may need to see all tenants.
This creates an important distinction:
Tenant User
↓
Only current tenant
Platform Admin
↓
Can manage tenants
Be careful when bypassing tenant scopes.
A method such as:
withoutGlobalScopes()
can be powerful, but it should not be used casually.
Whenever tenant isolation is bypassed, there should be a clear reason and strong authorization.
25. Testing Multi-Tenancy
Multi-tenancy requires more than normal feature testing.
For example:
Create Tenant A
Create Tenant B
Create Product for Tenant A
Create Product for Tenant B
Then test:
Tenant A user → Can see A product
Tenant A user → Cannot see B product
Tenant B user → Can see B product
Tenant B user → Cannot see A product
Also test:
Create
Read
Update
Delete
Search
Export
Reports
API
Files
Queues
Cache
A very important test is:
Can Tenant A ever access Tenant B’s data?
That question should be tested repeatedly.
26. Common Multi-Tenancy Mistakes
Mistake 1 — Adding tenant_id everywhere manually
This increases the chance of forgetting it.
Mistake 2 — Trusting tenant_id from the request
The client can modify it.
Mistake 3 — Only protecting the frontend
Users can call APIs directly.
Mistake 4 — Forgetting relationships
An Order may accidentally reference another tenant’s Customer.
Mistake 5 — Forgetting cache
Tenant A’s cached data can appear for Tenant B.
Mistake 6 — Forgetting queues
Background jobs may lose tenant context.
Mistake 7 — Forgetting file storage
Tenant files can become mixed.
Mistake 8 — Forgetting unique constraints
Global uniqueness may be incorrect for tenant-specific data.
Mistake 9 — No automated isolation tests
A future developer can accidentally introduce a data leak.
27. A Practical Laravel Architecture
A shared-database SaaS application could look like:
Laravel Application
│
├── Authentication
│
├── Tenant Context
│ └── CurrentTenant
│
├── Authorization
│ └── Roles & Permissions
│
├── Tenant-Aware Models
│ ├── Product
│ ├── Order
│ ├── Customer
│ └── Invoice
│
├── Tenant-Aware Services
│
├── Tenant-Aware Jobs
│
├── Tenant-Aware Cache
│
├── Tenant-Aware Storage
│
└── Tenant-Aware Reports
The key principle is:
Tenant awareness should be part of the architecture, not an afterthought.
28. A Simple Request Lifecycle
A multi-tenant Laravel request can conceptually work like this:
HTTP Request
↓
Identify Tenant
↓
Set CurrentTenant
↓
Authenticate User
↓
Check Tenant Membership
↓
Check Permission
↓
Tenant-Scoped Query
↓
Business Logic
↓
Response
This centralized flow makes the application easier to reason about.
29. Multi-Tenancy Is an Architectural Decision
The biggest lesson is that multi-tenancy affects much more than the database.
You need to think about:
Database
Authentication
Authorization
Models
Relationships
Unique Constraints
Storage
Cache
Queues
APIs
Reports
Notifications
Testing
Deployment
Backups
If you only add:
tenant_id
to your tables, you have not fully designed a multi-tenant system.
You’ve only started one.
Final Thoughts
Multi-tenancy is one of those architectural decisions that is much easier to design correctly at the beginning than to retrofit later.
Before building a SaaS application, ask:
How will we identify the current tenant?
Where will tenant ownership be stored?
How will queries be automatically scoped?
How will we prevent cross-tenant relationships?
How will files be separated?
How will cache keys be separated?
How will queued jobs know their tenant?
How will reports and APIs respect tenant boundaries?
How will we test tenant isolation?
And most importantly:
What happens if Tenant A tries to access Tenant B’s data?
If the architecture can answer that question consistently, you’re on the right path.
A good multi-tenant system isn’t just one where multiple customers can use the same application.
It is a system where multiple customers can safely share the same application while their data, permissions, configuration, and business operations remain properly isolated.
Design tenant isolation first. Scale the application second.
Комментарии (0)
Пока нет комментариев — будьте первым.