Skip to main content

Guide

How to Structure a Multi-Tenant SaaS

Data isolation strategies, tenant-aware middleware, shared vs separate databases, and the entitlement architecture that scales multi-tenant SaaS applications.

Category Guide
Read Time 9 min read
Updated April 2026
Steps 5 steps

Who This Guide Is For

This guide is for developers building a SaaS application where multiple customers (tenants) share the same codebase and infrastructure. You are past the prototype stage and need to make structural decisions about how tenants are isolated, how data is scoped, and how entitlements control what each tenant can access. You have built web applications before but have not architected a multi-tenant system from scratch, or you have inherited one and need to understand the patterns it should follow.

Before You Start

You should have a clear understanding of your business model: how many tenants you expect in the first year, whether tenants have users (a tenant is an organisation with multiple users) or whether each user is their own tenant, and what features vary between pricing tiers. These business decisions directly determine the technical architecture. A system designed for fifty enterprise tenants with separate databases looks completely different from one designed for ten thousand individual users on a shared database.

You should also have a working application framework and authentication system. This guide focuses on the multi-tenancy layer that sits on top of those foundations, not on setting up the application itself.

Step 1: Choose a Data Isolation Strategy

The most consequential architectural decision in a multi-tenant system is how you isolate each tenant’s data. There are three common approaches, each with distinct trade-offs.

Shared database with a tenant column is the simplest and most common approach. Every table has a tenant_id column, and every query is scoped to the current tenant. This is the right choice for most SaaS applications, especially those with many tenants and relatively uniform data structures. It is easy to deploy (one database to manage), efficient with resources (no per-tenant infrastructure), and straightforward to query across tenants for platform-level analytics.

The risk with a shared database is data leakage. A single query that forgets the tenant_id filter exposes one tenant’s data to another. This is not a theoretical risk — it is the most common multi-tenancy bug and one of the most damaging. The mitigation is automatic scoping (covered in Step 2), not developer discipline.

Shared database with separate schemas (where the database engine supports it, such as PostgreSQL) gives each tenant their own schema within a single database. Tables have the same structure but exist in isolated namespaces. This provides stronger isolation than a tenant column — a query physically cannot access another tenant’s schema without explicitly switching — while still using a single database server.

Separate databases per tenant provides the strongest isolation. Each tenant has their own database, and there is no possibility of cross-tenant data access through query mistakes. This approach is appropriate for enterprise SaaS with strict compliance requirements, tenants with very different data volumes, or situations where tenants need to own their data and potentially export or migrate it independently.

The cost of separate databases is operational complexity: migrations must run against every tenant database, connection management scales linearly with tenant count, and cross-tenant analytics requires a separate data pipeline.

For the majority of SaaS applications — those serving small to mid-size businesses at scale — the shared database with tenant column is the right starting point. It minimises operational overhead and can be migrated to a schema-per-tenant or database-per-tenant model later if isolation requirements change.

Step 2: Implement Tenant-Aware Middleware

The middleware layer is your primary defence against data leakage. Every request must be resolved to a tenant before it reaches your application logic, and every database query must be automatically scoped to that tenant.

Tenant resolution determines which tenant a request belongs to. Common strategies include: subdomain-based (each tenant has a subdomain like acme.yourapp.com), header-based (a custom header identifies the tenant), path-based (the tenant identifier is part of the URL path), or session-based (the tenant is determined from the authenticated user’s organisation).

For most B2B SaaS applications, resolving the tenant from the authenticated user is the most reliable approach. The user logs in, their user record is associated with a tenant, and the middleware sets the current tenant for the request. This avoids the complexity of subdomain DNS management and works naturally with standard authentication flows.

Global query scoping is the mechanism that prevents data leakage. In Laravel, this means applying a global scope to every tenant-aware model that adds a WHERE tenant_id = ? clause to every query. The scope reads the current tenant from the middleware-set context. This ensures that even if a developer writes a query without explicitly filtering by tenant, the scope applies the filter automatically.

This scoping must also apply to creates. When a new record is created, the tenant_id should be set automatically by the model (in a boot method or observer) rather than relying on the developer to pass it explicitly. The fewer places where tenant_id is handled manually, the fewer places where it can be forgotten.

Testing the isolation is non-negotiable. Write tests that create data for Tenant A, authenticate as Tenant B, and verify that Tenant B cannot see Tenant A’s data. Do this for every model and every endpoint. These tests are the safety net that catches scoping bugs before they reach production.

Step 3: Design the Entitlement System

Multi-tenant SaaS applications almost always have multiple pricing tiers, and those tiers control which features each tenant can access. The entitlement system is the mechanism that maps a tenant’s subscription to the features they are allowed to use.

Centralise entitlement checks in a single service rather than scattering subscription checks across controllers and views. An EntitlementService (or equivalent) should expose methods like canAccess(feature), getCurrentTier(), and hasReachedLimit(resource). Every access check in the application calls this service, and the service reads from the tenant’s subscription data.

This centralisation has two benefits. First, changing pricing tiers or feature allocations means updating one service rather than hunting through the codebase for scattered checks. Second, it makes the entitlement logic testable — you can unit test every combination of tier and feature without rendering views or hitting controllers.

Entitlements should be data-driven, not code-driven. Define features and their tier mappings in a database table or configuration file, not in conditional statements scattered through the code. A data-driven approach means you can change what each tier includes without deploying code. This matters when the business wants to run promotions, grandfather existing customers on old pricing, or experiment with feature bundling.

Usage limits are a subset of entitlements that require counting. If your pricing tiers include limits (a certain number of users, projects, API calls, or storage), the entitlement system must track current usage and enforce limits. Count current usage at the point of creation, not retrospectively. When a tenant tries to create their eleventh project on a ten-project plan, reject the creation with a clear message about the limit and how to upgrade.

Grace periods and enforcement require careful thought. When a subscription lapses or downgrades, what happens to data that exceeds the new tier’s limits? The answer should never be data deletion. Read-only access to existing data with creation blocked is the standard pattern. The tenant can see their data but cannot add more until they upgrade or reduce their usage.

Step 4: Handle Tenant Lifecycle Events

A multi-tenant system must handle the full lifecycle of a tenant: provisioning, configuration, suspension, and termination.

Provisioning is what happens when a new tenant signs up. At minimum, this creates the tenant record and associates the first user as the tenant’s administrator. For database-per-tenant architectures, provisioning also creates the tenant’s database and runs migrations. Design provisioning to be idempotent — if it fails halfway through and is retried, it should not create duplicate records or leave the tenant in a half-provisioned state.

Configuration covers tenant-specific settings that affect application behaviour: branding, notification preferences, feature toggles, timezone and locale settings. Store these in a tenant settings table and load them early in the request lifecycle. Cache aggressively — tenant settings rarely change but are read on every request.

Suspension handles situations where a tenant’s subscription lapses, they violate terms of service, or they request a temporary freeze. Suspended tenants should not be able to log in (or should see a suspension notice), but their data must be preserved. Implement suspension as a status flag on the tenant record, checked in the authentication middleware.

Termination and data deletion must comply with your data retention policy and applicable regulations. When a tenant requests account deletion, either delete their data immediately or schedule it for deletion after a grace period (30 days is common). For shared-database architectures, this means deleting all rows with the tenant’s ID. For separate-database architectures, it means dropping the database. Log the deletion for audit purposes.

Step 5: Plan for Cross-Tenant Operations

Despite the isolation between tenants, there are legitimate operations that span multiple tenants, and the architecture must support them without compromising isolation.

Platform analytics — usage metrics, revenue reporting, churn analysis — require querying across all tenants. In a shared database, this is straightforward: query without the tenant scope. In a separate-database architecture, you need a data pipeline that aggregates tenant data into a central analytics database. Design this from the beginning rather than bolting it on later.

System-wide migrations and updates must apply to all tenants. In a shared database, a migration runs once. In a separate-database architecture, a migration must run against every tenant database, and you need tooling to track which tenants have been migrated and retry failures. Build this tooling before you have more than a handful of tenants.

Admin and support access is the ability for your team to view a tenant’s data for support purposes. Implement this as an explicit “impersonation” feature that logs who accessed which tenant’s data and when. Never build admin access by simply removing the tenant scope — that exposes all tenants’ data simultaneously and provides no audit trail.

Rate limiting per tenant prevents any single tenant from consuming disproportionate resources and degrading the experience for others. Apply rate limits at the tenant level (not just the user level) for API endpoints, background job dispatch, and resource-intensive operations. A single tenant running a bulk import should not slow down every other tenant’s dashboard.

Common Mistakes

  • Relying on developer discipline for tenant scoping. Every query that requires a manual tenant_id filter is a data leakage bug waiting to happen. Use automatic scoping through global scopes or middleware.
  • Building entitlements as code rather than data. Hardcoded feature checks scattered across controllers are impossible to maintain as pricing evolves. Centralise entitlements in a service with data-driven feature mappings.
  • No tests for data isolation. If you do not have tests that verify Tenant A cannot see Tenant B’s data, you do not know whether your isolation works. Write these tests first.
  • Deleting data on downgrade. When a tenant downgrades, restrict creation but preserve existing data. Deleting a customer’s data because they moved to a cheaper plan is a trust-destroying experience.
  • Ignoring the admin access problem. Support engineers need to see tenant data. Without a structured impersonation feature, they will find workarounds that bypass your isolation and leave no audit trail.

What Good Looks Like

A well-structured multi-tenant SaaS has: a clear data isolation strategy with automatic query scoping that prevents cross-tenant data leakage, a centralised entitlement service that maps subscriptions to feature access, a complete tenant lifecycle from provisioning through termination, cross-tenant operations that are audited and controlled, and rate limiting that prevents any single tenant from degrading the platform for others. The architecture should be invisible to developers working on features — they write queries and the tenant scoping happens automatically.

Next Steps

For the subscription billing that drives your entitlement system, How to Integrate Stripe Payments covers the Stripe patterns. For the authentication layer that resolves tenants from users, How to Implement OAuth Authentication covers authentication flows. For planning the SaaS product before building it, see How to Define Requirements for a SaaS Product.

Need Hands-On Help?

Our guides give you the thinking. If you want someone to do the building, we should talk.

Start a Project Browse Case Studies