Software as a Service (SaaS) applications often serve many organizations from a single application.
For example, imagine a project-management platform used by:
Company A
Company B
Company C
Company D
All of these organizations use the same application, but their users, projects, billing information, and business data must remain isolated.
This architecture is known as multi-tenancy.
Designing a multi-tenant SaaS application requires more than adding an organizationId field to a database. You need to think carefully about tenant isolation, authentication, authorization, database architecture, scalability, billing, configuration, and security.
In this guide, we’ll explore how to design a multi-tenant SaaS application, particularly using Node.js, TypeScript, MongoDB, and Express.js.
What Is Multi-Tenancy?
A multi-tenant application allows multiple customers, organizations, or tenants to use the same software while keeping their data logically separated.
A simple architecture looks like this:
SaaS Application
|
┌───────────────┼───────────────┐
↓ ↓ ↓
Tenant A Tenant B Tenant C
| | |
Users Users Users
Projects Projects Projects
Data Data Data
The application infrastructure may be shared, but each tenant should only be able to access its own data.
For example:
Tenant A
├── Users
├── Projects
├── Orders
└── Documents
Tenant B
├── Users
├── Projects
├── Orders
└── Documents
Tenant A must never be able to access Tenant B’s records.
Why Build a Multi-Tenant SaaS Application?
Multi-tenancy is attractive because a single application infrastructure can serve many customers.
Instead of deploying:
Customer A → Application A
Customer B → Application B
Customer C → Application C
you can have:
One Application
|
┌────────────┼────────────┐
↓ ↓ ↓
Tenant A Tenant B Tenant C
This can simplify:
- Deployment
- Maintenance
- Monitoring
- Infrastructure management
- Feature releases
- Resource utilization
However, it also introduces additional architectural complexity.
Core Requirements of a Multi-Tenant SaaS System
A production-ready multi-tenant application should usually address:
Tenant Management
Authentication
Authorization
Data Isolation
User Management
Roles & Permissions
Billing
Tenant Configuration
File Storage
Logging
Monitoring
Scalability
Security
The most important requirement is tenant isolation.
What Is a Tenant?
A tenant usually represents a customer organization.
For example:
{
"_id": "tenant_123",
"name": "Acme Corporation",
"slug": "acme",
"status": "active",
"plan": "professional"
}
A tenant can have many users:
Acme Corporation
|
├── Admin
├── Manager
├── Employee
└── Employee
Another organization has its own tenant:
Globex Corporation
|
├── Admin
├── Manager
└── Employee
Tenant Data Model
A simple MongoDB tenant model might look like:
import { Schema, model } from "mongoose";
const tenantSchema = new Schema(
{
name: {
type: String,
required: true,
trim: true
},
slug: {
type: String,
required: true,
unique: true
},
plan: {
type: String,
enum: [
"free",
"starter",
"professional",
"enterprise"
],
default: "free"
},
status: {
type: String,
enum: [
"active",
"suspended",
"cancelled"
],
default: "active"
}
},
{
timestamps: true
}
);
export const Tenant = model(
"Tenant",
tenantSchema
);
Users and Tenants
A user usually belongs to one or more tenants.
For a simple SaaS application:
const userSchema = new Schema({
tenantId: {
type: Schema.Types.ObjectId,
ref: "Tenant",
required: true
},
name: String,
email: {
type: String,
required: true
},
role: {
type: String,
enum: [
"admin",
"manager",
"member"
]
}
});
Now every user belongs to a tenant.
For example:
User A
tenantId → Tenant A
User B
tenantId → Tenant B
The Most Important Rule: Tenant Isolation
Suppose your application has:
Tenant A → User A
Tenant B → User B
User A requests:
GET /api/projects/projectB
The application must not simply query:
Project.findById(projectId);
Instead, the query should include the tenant:
Project.findOne({
_id: projectId,
tenantId: currentTenantId
});
This is one of the most important principles of multi-tenant architecture.
Instead of:
findById(id)
use:
findOne({
_id: id,
tenantId
})
for tenant-owned resources.
Add tenantId to Tenant-Owned Collections
Suppose you have a project collection.
A multi-tenant project model could be:
const projectSchema = new Schema(
{
tenantId: {
type: Schema.Types.ObjectId,
ref: "Tenant",
required: true,
index: true
},
name: {
type: String,
required: true
},
description: String,
createdBy: {
type: Schema.Types.ObjectId,
ref: "User"
}
},
{
timestamps: true
}
);
The important field is:
tenantId
Every project now belongs to a specific tenant.
Tenant Context in Authentication
The application needs to determine which tenant the current request belongs to.
A request could contain:
Authorization: Bearer JWT_TOKEN
The JWT could contain:
{
"userId": "USER_ID",
"tenantId": "TENANT_ID",
"role": "admin"
}
After authentication:
req.user = {
userId,
tenantId,
role
};
Now every service can use:
req.user.tenantId
to scope database operations.
Tenant Middleware
You can create middleware that determines the tenant context.
For example:
export const tenantMiddleware = (
req: Request,
res: Response,
next: NextFunction
) => {
if (!req.user?.tenantId) {
return res.status(403).json({
message: "Tenant context missing"
});
}
req.tenantId = req.user.tenantId;
next();
};
Your request pipeline becomes:
Request
↓
Authentication
↓
Tenant Resolution
↓
Authorization
↓
Controller
↓
Service
↓
Database
Authentication vs Tenant Authorization
These two concepts are different.
Authentication
Answers:
Who is this user?
Example:
userId = 123
Authorization
Answers:
What can this user access?
Example:
tenantId = ABC
role = manager
permissions = project.read
A user being authenticated does not automatically mean they can access every tenant or resource.
Multi-Tenant Roles and Permissions
A SaaS application will often require role-based access control.
For example:
Tenant Admin
↓
Manage users
Manage billing
Manage settings
Manager
↓
Manage projects
View reports
Member
↓
View projects
Create tasks
A simple permission model:
interface IUserRole {
tenantId: string;
userId: string;
role: string;
permissions: string[];
}
This becomes especially important when a user can belong to multiple organizations.
Supporting Users in Multiple Tenants
Some SaaS applications allow the same user to belong to multiple organizations.
For example:
John
|
├── Acme → Admin
|
├── Globex → Member
|
└── Startup Inc → Manager
In this case, storing a single tenantId directly on the user may not be enough.
Instead, create a membership collection:
const membershipSchema = new Schema({
userId: {
type: Schema.Types.ObjectId,
ref: "User",
required: true
},
tenantId: {
type: Schema.Types.ObjectId,
ref: "Tenant",
required: true
},
role: {
type: String,
required: true
}
});
Then:
User
|
├── Membership → Tenant A
├── Membership → Tenant B
└── Membership → Tenant C
This provides more flexibility.
Choosing a Multi-Tenant Database Strategy
There are three common approaches.
1. Shared Database, Shared Collections
All tenants use the same database and collections.
Example:
MongoDB
|
├── users
├── projects
├── orders
└── documents
Each document contains:
{
"tenantId": "TENANT_A"
}
Another document:
{
"tenantId": "TENANT_B"
}
Advantages
- Simple infrastructure
- Lower cost
- Easy to scale initially
- Easy to deploy
Challenges
- Application must enforce tenant isolation
- Queries must always include tenant context
- Indexes must be designed carefully
This is often a practical starting point for SaaS applications.
2. Shared Database, Separate Collections
Each tenant has separate collections.
For example:
Tenant A
projects_A
users_A
Tenant B
projects_B
users_B
This provides more separation but introduces additional collection-management complexity.
For a large number of tenants, this approach can become difficult to manage.
3. Separate Database Per Tenant
Each tenant gets its own database.
MongoDB Cluster
|
├── tenant_acme
├── tenant_globex
└── tenant_startup
Advantages
- Stronger data isolation
- Easier tenant-specific backup/restore
- Tenant-specific database configuration can be possible
Challenges
- More operational complexity
- More database connections to manage
- More complicated migrations
- Higher infrastructure overhead
This approach can make sense for customers with strict isolation or compliance requirements.
Which Database Strategy Should You Choose?
There is no universal architecture that fits every SaaS application.
Your choice depends on:
Number of tenants
Tenant size
Compliance requirements
Data isolation requirements
Infrastructure budget
Backup requirements
Operational complexity
Expected growth
For many early-stage SaaS applications, a shared database with tenantId is a straightforward architecture.
For customers requiring stronger isolation, separate databases may be considered.
Database Indexing in Multi-Tenant Applications
Indexes become particularly important.
Suppose you frequently query:
Project.find({
tenantId,
status
});
A compound index can help:
projectSchema.index({
tenantId: 1,
status: 1
});
For tenant-specific lookups:
projectSchema.index({
tenantId: 1,
_id: 1
});
For tenant-specific unique values, consider compound unique indexes.
For example:
userSchema.index(
{
tenantId: 1,
email: 1
},
{
unique: true
}
);
This allows the same email to exist in different tenants if your business rules permit it, while keeping it unique within each tenant.
Avoid Global Queries
One of the most dangerous mistakes is running queries without tenant filters.
Dangerous:
const projects = await Project.find({
status: "active"
});
If this endpoint is tenant-specific, it could return projects belonging to multiple tenants.
Safer:
const projects = await Project.find({
tenantId,
status: "active"
});
Tenant context should be treated as a mandatory part of the query.
Centralize Tenant Filtering
A large application may have hundreds of queries.
Manually remembering tenantId everywhere increases the risk of mistakes.
For example:
Project.findOne({
_id,
tenantId
});
Order.find({
tenantId
});
Document.find({
tenantId
});
You can create service or repository functions that always require tenant context.
For example:
async function findProject(
tenantId: string,
projectId: string
) {
return Project.findOne({
_id: projectId,
tenantId
});
}
Then:
await findProject(
req.tenantId,
projectId
);
This makes tenant isolation explicit.
Tenant-Aware Repository Pattern
For larger applications, a repository pattern can make tenant filtering more consistent.
Example:
class ProjectRepository {
async findById(
tenantId: string,
projectId: string
) {
return Project.findOne({
_id: projectId,
tenantId
});
}
async findAll(
tenantId: string
) {
return Project.find({
tenantId
});
}
async create(
tenantId: string,
data: any
) {
return Project.create({
...data,
tenantId
});
}
}
Now the repository API requires tenant context.
Tenant-Aware File Storage
File storage also needs tenant isolation.
Don’t store everything like:
uploads/
file1.pdf
file2.pdf
Instead:
uploads/
tenant-a/
documents/
images/
tenant-b/
documents/
images/
For object storage:
tenant-a/documents/file.pdf
tenant-b/documents/file.pdf
When generating a file URL, verify the tenant before allowing access.
Tenant Configuration
Different tenants may have different settings.
For example:
{
"tenantId": "TENANT_A",
"settings": {
"timezone": "Asia/Kolkata",
"language": "en",
"dateFormat": "DD/MM/YYYY",
"emailNotifications": true
}
}
You could maintain a dedicated configuration collection:
tenants
tenantSettings
users
projects
This keeps tenant-specific configuration separate from application-wide configuration.
Tenant-Specific Feature Flags
SaaS applications often release features gradually.
For example:
Tenant A → New Dashboard → Enabled
Tenant B → New Dashboard → Disabled
A feature flag model might contain:
{
tenantId,
feature: "advanced_reports",
enabled: true
}
This allows controlled feature rollouts.
Multi-Tenant Billing
Billing should also be associated with the tenant rather than individual users.
For example:
Tenant
↓
Subscription
↓
Plan
↓
Usage
↓
Invoice
Example:
{
"tenantId": "TENANT_A",
"plan": "professional",
"status": "active",
"billingCycle": "monthly"
}
Usage-based SaaS applications may also track:
API requests
Storage
Users
Projects
Messages
Compute usage
Tenant Lifecycle
A tenant usually has a lifecycle.
For example:
Created
↓
Trial
↓
Active
↓
Suspended
↓
Cancelled
Your application should define what happens at each stage.
For example:
Trial expired
↓
Restrict premium features
↓
Allow billing update
↓
Suspend tenant
Avoid scattering tenant-status checks throughout the application.
Centralize business rules where possible.
Tenant Deletion
Deleting a tenant is more complicated than deleting one document.
A tenant might have:
Tenant
├── Users
├── Projects
├── Orders
├── Documents
├── Payments
├── Logs
└── Settings
You need a clear deletion strategy.
Options include:
Soft Delete
{
deletedAt: Date
}
Hard Delete
Permanently remove tenant data.
Scheduled Deletion
Tenant cancelled
↓
30-day retention
↓
Permanent deletion
The correct approach depends on business and legal requirements.
Audit Logging
Multi-tenant applications benefit from tenant-aware audit logs.
For example:
{
"tenantId": "TENANT_A",
"userId": "USER_123",
"action": "PROJECT_CREATED",
"resourceId": "PROJECT_456",
"timestamp": "2026-09-18T10:00:00Z"
}
This allows administrators to answer questions such as:
Who created this project?
Who changed the settings?
Who deleted this document?
When did the change happen?
Logging Must Include Tenant Context
Application logs should ideally contain tenant information where relevant.
Instead of:
User updated project
use structured information such as:
{
"tenantId": "TENANT_A",
"userId": "USER_123",
"action": "PROJECT_UPDATED",
"projectId": "PROJECT_456"
}
This makes debugging and monitoring much easier.
API Architecture
A clean multi-tenant Node.js architecture might look like:
src/
│
├── modules/
│ ├── auth/
│ ├── tenant/
│ ├── user/
│ ├── project/
│ ├── billing/
│ └── file/
│
├── middleware/
│ ├── auth.ts
│ ├── tenant.ts
│ └── permission.ts
│
├── models/
│
├── repositories/
│
├── services/
│
├── controllers/
│
├── routes/
│
└── utils/
Request flow:
Client
↓
Route
↓
Authentication
↓
Tenant Context
↓
Permission Check
↓
Controller
↓
Service
↓
Repository
↓
MongoDB
Example Multi-Tenant API
Consider:
GET /api/projects
Authentication identifies:
User: USER_123
Tenant: TENANT_A
Role: manager
The service executes:
const projects = await Project.find({
tenantId: "TENANT_A"
});
The response only contains Tenant A’s projects.
The client should not be trusted to provide:
{
"tenantId": "TENANT_B"
}
and gain access to another tenant.
The server should derive tenant context from trusted authentication and authorization data.
Security Considerations
Tenant isolation should be treated as a security boundary.
Important practices include:
- Never trust a client-provided
tenantId. - Validate tenant membership.
- Check authorization on every protected resource.
- Include tenant filters in database queries.
- Use compound indexes for tenant-scoped queries.
- Protect tenant-specific files.
- Keep audit logs tenant-aware.
- Test cross-tenant access explicitly.
- Avoid exposing sequential resource identifiers where they make enumeration easier.
- Apply rate limits appropriately.
- Protect administrative APIs separately.
Testing Tenant Isolation
Multi-tenant applications require dedicated isolation tests.
For example:
Tenant A creates Project A
Tenant B creates Project B
Then test:
Tenant A → Project A → Allowed
Tenant B → Project B → Allowed
Tenant A → Project B → Denied
Tenant B → Project A → Denied
Also test:
GET
POST
PUT
PATCH
DELETE
Tenant isolation should be tested for every operation.
Common Multi-Tenant Mistakes
Mistake 1: Trusting tenantId From the Client
Bad:
const { tenantId } = req.body;
The client can modify it.
Instead, derive the tenant from authenticated context.
Mistake 2: Forgetting tenantId in a Query
Bad:
Project.findById(projectId);
Better:
Project.findOne({
_id: projectId,
tenantId
});
Mistake 3: Tenant Filtering Only in Controllers
If tenant isolation is implemented only in controllers, another service or background job might accidentally execute an unscoped query.
Tenant-aware data access should be consistently enforced throughout the application.
Mistake 4: Ignoring Background Jobs
Suppose you have:
Cron Job
↓
Process Orders
The job must also understand tenant context.
For example:
Order.find({
tenantId,
status: "pending"
});
Background workers are part of your application’s security boundary too.
Mistake 5: Forgetting Tenant Isolation in Caches
Suppose you cache:
project:123
That may be insufficient if resource IDs can overlap or if cache keys don’t encode tenant context.
Prefer:
tenant:A:project:123
Tenant context should be included in cache keys where required.
Scaling a Multi-Tenant SaaS Application
As the number of tenants increases, you may need to scale different parts of the system.
For example:
Load Balancer
|
┌───────────┼───────────┐
↓ ↓ ↓
Node.js Node.js Node.js
| | |
└───────────┼───────────┘
↓
MongoDB
|
Object Storage
Background processing can be separated:
API
↓
Queue
↓
Workers
This can help with:
- Email processing
- Report generation
- File processing
- Notifications
- Billing tasks
Handling Large Tenants
Not all tenants consume the same resources.
You may have:
Tenant A → 10 users
Tenant B → 100 users
Tenant C → 100,000 users
A multi-tenant architecture should consider resource usage.
Possible controls include:
Per-tenant API limits
Storage quotas
Maximum users
Maximum projects
Request quotas
Background-job limits
This helps prevent one tenant from consuming disproportionate shared resources.
Multi-Tenant SaaS Architecture Example
A practical architecture might look like:
Client
|
↓
API Gateway
|
↓
Authentication
|
↓
Tenant Resolver
|
↓
Authorization
|
↓
Node.js API
|
┌───────────┼───────────┐
↓ ↓ ↓
Services Repository Queue
| | |
↓ ↓ ↓
Business MongoDB Workers
Logic |
↓
Object Storage
Tenant context flows through the entire request.
tenantId
↓
Authentication
↓
Service
↓
Repository
↓
Database
Multi-Tenant Design Checklist
Before launching a SaaS application, review:
Tenant Management
- Tenant creation
- Tenant status
- Tenant configuration
- Tenant lifecycle
- Tenant deletion strategy
Authentication
- User authentication
- Tenant membership
- Session/token management
- Multi-tenant user support if required
Authorization
- Roles
- Permissions
- Resource-level authorization
- Tenant-level authorization
Database
tenantIdstrategy- Tenant-aware queries
- Compound indexes
- Tenant-scoped unique constraints
- Backup strategy
Storage
- Tenant-specific file paths
- Private files
- Access authorization
- Storage quotas
Security
- Cross-tenant access tests
- API rate limits
- Audit logging
- Secure cache keys
- Background-job isolation
Scalability
- Horizontal API scaling
- Queue-based processing
- Database monitoring
- Tenant usage monitoring
- Resource quotas
Final Thoughts
Designing a multi-tenant SaaS application requires you to think beyond simply sharing one database between multiple customers.
The central principle is tenant isolation.
A practical Node.js and MongoDB architecture can use:
Tenant
↓
Authentication
↓
Tenant Context
↓
Authorization
↓
Tenant-Aware Services
↓
Tenant-Aware Repository
↓
MongoDB
For many applications, a shared database with tenant-scoped documents is a practical starting point. As requirements around scale, compliance, isolation, and operations change, other strategies such as separate collections or databases can be considered.
The most important part is to make tenant boundaries explicit throughout the entire system—not just in the database.
Every layer should understand the tenant context:
API
↓
Authentication
↓
Authorization
↓
Business Logic
↓
Database
↓
Storage
↓
Cache
↓
Background Jobs
When tenant isolation is designed into the architecture from the beginning, it becomes much easier to build a SaaS platform that can safely support multiple organizations while remaining maintainable and scalable.




