How to Implement Tenant Isolation in a Multi-Tenant SaaS API
- Author
- Vishal Maurya
- Published on
- Reading time
- 4 min read
Overview
A multi-tenant SaaS application serves several organizations from one product. Shared infrastructure can reduce operating overhead, but it creates an important security requirement: a user from one tenant must never gain access to another tenant's records by changing an identifier in a request.
Hiding another organization's data in the frontend is not authorization. The backend must enforce tenant access on every relevant operation.
1. Decide How Tenant Data Is Stored
Common models include a shared database with a tenant ID on each row, separate schemas, or separate databases per tenant. Each has trade-offs in isolation, operational complexity, migration strategy, and cost.
A shared-table design is often simpler to operate, but it requires consistent tenant scoping and careful constraints. Separate databases can provide stronger physical separation at the cost of more complicated provisioning, migrations, backups, and reporting. Choose based on the product's threat model and operational needs rather than assuming one model is universally best.
2. Resolve Tenant Membership on the Server
Do not trust a tenant_id supplied by the client as proof of membership. Resolve the user's allowed organizations from the authenticated session or a server-side membership record, then verify that the requested tenant is one of them.
The same check applies to background jobs and administrative endpoints. A valid user session does not automatically grant access to every tenant.
3. Scope Database Queries by Tenant
For a shared-table model, build tenant filtering into the query path instead of fetching a record by ID and checking ownership only in the frontend.
from django.shortcuts import get_object_or_404
def get_invoice_for_tenant(*, invoice_id, tenant):
return get_object_or_404(
Invoice.objects.filter(tenant=tenant),
pk=invoice_id,
)
This example assumes tenant has already been authenticated and authorized for the current user. It is not a complete view or permission class. Apply the same rule to list, detail, update, delete, export, and nested-resource endpoints.
Centralize common scoping logic where possible, but keep the authorization boundary explicit. A generic manager that silently depends on thread-local state can make security behavior harder to reason about.
4. Add Database Constraints
Application checks are necessary, but the database can prevent inconsistent relationships too. Use foreign keys and uniqueness constraints that include tenant identity where the business rule requires uniqueness within an organization.
For example, if a project slug must be unique per tenant, a unique constraint on (tenant_id, slug) expresses that rule more accurately than a global slug constraint. Review relationships so a child record cannot accidentally reference a parent from a different tenant.
Where the database supports row-level security and it fits your architecture, evaluate it as an additional layer. It does not replace correct connection setup, migration practices, or application authorization.
5. Test for Cross-Tenant Access
Create tests with at least two tenants and users with different memberships. Verify that a user cannot list, read, update, delete, export, or trigger a job for another tenant's record by guessing its ID.
Also test unowned records, revoked membership, bulk operations, file download URLs, search endpoints, and admin functions. Security tests should cover all paths that expose data, not just the main detail route.
6. Be Careful with Caches and Background Jobs
A cache key that contains only invoice:123 may return one tenant's result to another tenant if identifiers or cache usage overlap. Include the relevant tenant and authorization context in cache keys, and avoid caching data across permission boundaries without a deliberate design.
Background tasks should receive a stable tenant identifier and verify the operation's authorization and state where appropriate. Do not assume the request that queued the job is the only path by which the task can run.
7. Keep Audit Trails Useful
For sensitive business records, log access and state changes with actor, tenant, resource, operation, and timestamp where appropriate. Avoid logging complete document contents or secrets. Define retention and access controls for audit logs themselves.
Conclusion
Tenant isolation is an end-to-end property. It depends on membership checks, tenant-scoped queries, database constraints, cache design, background processing, and tests that try to cross the boundary.
If you are building a B2B SaaS product or need to review tenant isolation in an existing API, I can help assess the data model and authorization paths. Contact me with your stack and tenancy model.