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
email

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.