Building an internal platform taught me a lot about users, permissions, workflows and shared infrastructure. As I started thinking more about complete software products, one architectural problem became especially important: how should a SaaS application safely support multiple organisations on the same platform?
This is the core challenge of multi tenant SaaS architecture.
A multi tenant application may have hundreds or thousands of organisations using the same product, while each organisation expects its users, customers, records, workflows and configuration to remain isolated from everyone else.
In this article, I will design a practical multi tenant SaaS architecture around tenants, users, roles, permissions, ownership and data isolation.
Core SaaS Architecture
SaaS Platform
↓
Tenant Organisation
↓
Users + Roles + Permissions
↓
Application Services
↓
Tenant Aware Data Access
↓
Shared Infrastructure
↓
Strict Data Isolation
What Is a Tenant?
In a SaaS application, a tenant usually represents one customer organisation.
For example:
SaaS Platform
↓
Company A
Company B
Company C
Company D
All four organisations use the same application, but Company A should never be able to access Company B's records.
Start with the Tenant Model
I prefer making the tenant an explicit part of the data model.
A simple tenant record could look like:
{
"tenant_id": "tenant_1001",
"name": "Example Services Ltd",
"status": "ACTIVE",
"plan": "PRO"
}
Every business record that belongs to that organisation should be linked back to the tenant.
Tenant Aware Data Models
Imagine the SaaS application manages service jobs.
A job record might contain:
{
"job_id": "job_4821",
"tenant_id": "tenant_1001",
"title": "Boiler inspection",
"status": "SCHEDULED",
"assigned_to": "user_204"
}
The tenant_id is not just metadata.
It becomes one of the main security boundaries in the application.
Never Trust Tenant IDs from the Client
One design mistake I would avoid is allowing the client application to decide which tenant it belongs to.
For example, I would not trust a request such as:
{
"tenant_id": "tenant_9999",
"job_id": "job_4821"
}
The backend should determine the tenant from the authenticated user or token.
Security principle: the client can request a resource, but the backend must determine whether that resource belongs to the authenticated user's tenant.
Authentication and Tenant Context
After authentication, the application can create a trusted user context.
{
"user_id": "user_204",
"tenant_id": "tenant_1001",
"role": "MANAGER"
}
That context can then travel through the request lifecycle.
User Login
↓
Authentication
↓
User Identity Resolved
↓
Tenant Context Resolved
↓
Role and Permissions Loaded
↓
Request Processing
Designing Roles
Not every user inside a tenant should have the same capabilities.
A service operations SaaS platform might use roles such as:
Owner
Full organisation access and configuration.
Manager
Manage operational work and assigned teams.
Team Member
Access the work assigned to them and permitted team workflows.
Roles Are Not the Same as Permissions
I prefer separating roles from individual permissions.
A role is a convenient grouping.
Permissions describe what the user can actually do.
ROLE_PERMISSIONS = {
"OWNER": {
"job:create",
"job:view",
"job:update",
"job:assign",
"job:delete",
"user:manage",
"settings:manage"
},
"MANAGER": {
"job:create",
"job:view",
"job:update",
"job:assign"
},
"TEAM_MEMBER": {
"job:view",
"job:update_assigned"
}
}
Permission Checking
Permission checks should be performed server side.
def has_permission(
role,
permission
):
permissions = ROLE_PERMISSIONS.get(
role,
set()
)
return permission in permissions
The UI may hide unavailable actions for convenience, but backend permission enforcement remains essential.
Permissions Can Depend on Ownership
Role based access control alone is not always enough.
A Team Member may be allowed to update a job only if the job is assigned to them.
def can_update_job(
user,
job
):
if user["role"] == "OWNER":
return True
if user["role"] == "MANAGER":
return True
if (
user["role"] == "TEAM_MEMBER"
and job["assigned_to"] == user["user_id"]
):
return True
return False
This combines role based permissions with resource ownership.
Tenant Isolation Must Happen in Every Query
Consider a request to retrieve a job.
A dangerous query would be:
SELECT *
FROM jobs
WHERE job_id = ?;
A safer tenant aware query is:
SELECT *
FROM jobs
WHERE job_id = ?
AND tenant_id = ?;
This ensures that even if a valid job identifier from another organisation is supplied, the query does not return that record.
Centralise Tenant Filtering
Relying on developers to remember the tenant condition in every query is risky.
I prefer centralising tenant aware access as much as the application architecture allows.
def get_job(
database,
job_id,
tenant_id
):
return database.query(
"""
SELECT *
FROM jobs
WHERE job_id = ?
AND tenant_id = ?
""",
job_id,
tenant_id
)
Every repository or data access method can require tenant context explicitly.
Shared Database Multi Tenancy
One common architecture is a shared database where each tenant aware table contains a tenant identifier.
SaaS Application
↓
Shared Database
↓
Tenant 1001 Records
Tenant 1002 Records
Tenant 1003 Records
↓
tenant_id Enforces Logical Separation
This can be operationally efficient, but application level isolation must be designed carefully.
Database Per Tenant
Another architecture provides each tenant with a separate database.
SaaS Application
↓
Tenant Router
↓
Tenant A → Database A
Tenant B → Database B
Tenant C → Database C
This can provide stronger physical isolation, but it also increases operational complexity.
Choosing an Isolation Strategy
There is no single architecture that is best for every SaaS product.
Shared database
Lower operational complexity, but stronger logical isolation controls are required.
Separate schemas
Greater logical separation with additional database management overhead.
Database per tenant
Stronger physical isolation, but significantly higher operational complexity.
The right choice depends on scale, compliance requirements, customer expectations and operational cost.
Tenant Isolation Applies Beyond the Database
Multi tenancy is not only a database concern.
Tenant isolation may also apply to:
Cache keys
Object storage
Search indexes
Message queues
Background jobs
Logs
Analytics
AI context
Tenant Aware Cache Keys
Imagine caching job information.
This cache key is risky:
job:4821
A safer pattern is:
tenant:1001:job:4821
Tenant awareness should exist throughout the complete data path.
Tenant Aware Background Jobs
Background processing can also accidentally cross tenant boundaries if context is lost.
A queue message should carry trusted tenant context:
{
"event": "job.completed",
"tenant_id": "tenant_1001",
"job_id": "job_4821"
}
The worker processing the event should use that tenant context when reading or updating data.
API Authorisation Flow
A typical request can pass through several layers.
API Request
↓
Authenticate User
↓
Resolve Tenant
↓
Load Role + Permissions
↓
Validate Requested Action
↓
Query Data Using tenant_id
↓
Return Tenant Scoped Response
Example API Endpoint
A simplified FastAPI style endpoint could look like:
@app.get("/jobs/{job_id}")
def get_job(
job_id: str,
current_user = Depends(
get_current_user
)
):
job = job_repository.get_job(
job_id=job_id,
tenant_id=current_user.tenant_id
)
if not job:
raise HTTPException(
status_code=404,
detail="Job not found"
)
return job
Notice that the client does not provide the tenant identifier.
It comes from the authenticated user context.
Avoid Revealing Cross Tenant Resources
If a user requests a record that exists in another tenant, I would generally avoid returning information that confirms its existence.
For example:
404 Not Found
can be safer than revealing:
This record belongs to another customer.
Audit Important Actions
SaaS applications should record important administrative actions.
{
"tenant_id": "tenant_1001",
"user_id": "user_204",
"action": "job.assign",
"resource_id": "job_4821",
"timestamp": "2025-12-10T14:26:00Z"
}
Audit data can help answer:
Who performed the action?
Which tenant did it belong to?
What changed?
When did the change happen?
Tenant Aware Logging
Application logs should also include tenant context where appropriate.
logger.info(
"Job updated",
extra={
"tenant_id": tenant_id,
"job_id": job_id,
"user_id": user_id
}
)
This improves operational troubleshooting while still requiring appropriate protection for sensitive information.
Tenant Aware AI Features
Multi tenant boundaries become even more important when AI is introduced into a SaaS product.
If AI is used to summarise customer requests or suggest workflow actions, the model context should only contain information that belongs to the current tenant and that the current user is allowed to access.
User Request
↓
Authentication
↓
Tenant + Permission Check
↓
Retrieve Allowed Tenant Data
↓
Build AI Context
↓
AI Response
Important: AI should never become a shortcut around the application's normal tenant and permission boundaries.
Protecting File Storage
If tenants upload documents or images, storage paths can also include tenant context.
tenants/
tenant_1001/
jobs/
job_4821/
report.pdf
Access to the object should still be controlled through the application or approved storage access mechanism.
Rate Limits Can Be Tenant Aware
Shared SaaS infrastructure also needs protection from excessive use by one tenant.
Instead of only limiting by IP address, the application can consider tenant level usage.
rate_limit_key = (
f"tenant:{tenant_id}:api"
)
This can help prevent one customer's workload from unfairly affecting others.
Subscription Plans Should Not Become Security Roles
Another distinction I find useful is separating subscription plans from permissions.
For example:
tenant.plan = "PRO"
user.role = "MANAGER"
The plan determines which product capabilities are available to the organisation.
The role determines what the individual user can do inside that organisation.
Feature Entitlements
A simple entitlement model might look like:
PLAN_FEATURES = {
"STARTER": {
"jobs",
"customers"
},
"PRO": {
"jobs",
"customers",
"automation",
"analytics"
}
}
Product entitlement and security permission are related concepts, but they should not be treated as the same thing.
Testing Tenant Isolation
Multi tenancy should be tested explicitly.
One important test is:
def test_tenant_cannot_access_other_tenant_job():
tenant_a_user = create_user(
tenant_id="tenant_a"
)
tenant_b_job = create_job(
tenant_id="tenant_b"
)
response = client.get(
f"/jobs/{tenant_b_job.id}",
headers=auth_headers(
tenant_a_user
)
)
assert response.status_code == 404
I would treat cross tenant access tests as important security tests, not optional functional tests.
Product Architecture Is About More Than Infrastructure
This was an important shift in my own thinking.
In platform engineering, I spent a lot of time thinking about:
CI/CD, Kubernetes, Terraform, GitOps, policies and developer workflows.
SaaS product architecture introduces another layer:
Who is the customer, who are the users, what are they allowed to do, who owns each record, and how do we guarantee that one customer's data never crosses into another customer's experience?
These are product engineering decisions as much as technical decisions.
A Layered Multi Tenant Architecture
Web / Mobile Client
↓
API Gateway
↓
Authentication
↓
Tenant Resolution
↓
Authorisation Layer
↓
Application Services
↓
Tenant Aware Repository Layer
↓
Database + Cache + Storage + Queues
↓
Logging + Audit + Monitoring
Defence in Depth
I would not rely on one single tenant check.
A stronger design applies tenant isolation at several layers:
Authentication
Identify the user.
Tenant resolution
Determine the organisation.
Authorisation
Validate the requested action.
Data access
Scope all queries to the tenant.
Storage and cache
Namespace tenant data.
Audit
Record important tenant scoped activity.
What I Would Monitor
A multi tenant SaaS platform should also provide operational visibility.
I would monitor areas such as:
What I Learned from Designing Multi Tenant SaaS
Multi tenancy changed the way I thought about application architecture.
When building internal automation, the main question was often:
Can this workflow execute correctly?
In a SaaS product, another set of questions becomes equally important:
Is this the correct tenant?
Is this the correct user?
Is this user allowed to perform this action?
Does this record belong to their organisation?
Can another tenant ever access it accidentally?
That moves engineering thinking from infrastructure automation towards complete product behaviour and customer trust.
Final Architecture
Customer Organisation
↓
Users
↓
Authentication
↓
Tenant Resolution
↓
Roles + Permissions
↓
SaaS Application Services
↓
Tenant Aware Data Access
↓
Database + Cache + Storage + Events
↓
Audit + Observability
↓
Strict Tenant Isolation
Conclusion
In this article, I explored the foundations of designing a multi tenant SaaS application.
We covered:
Tenant modelling
Tenant aware data models
Authentication and tenant context
Roles
Permissions
Resource ownership
Tenant scoped queries
Shared database isolation
Database per tenant architecture
Tenant aware caching
Background job isolation
Tenant aware AI
File storage isolation
Rate limiting
Subscription entitlements
Audit logging
Isolation testing
Defence in depth