.NET  

Designing Multi Tenant SaaS Applications: Roles, Permissions and Data Isolation

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:

  • Authentication failures

  • Authorisation failures

  • API error rates

  • Tenant specific usage spikes

  • Database performance

  • Background job failures

  • Rate limit activity

  • Suspicious access patterns

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