A multi-tenant application is always a security challenge because tenants share the same codebase, same infrastructure, and sometimes even the same database. One small mistake in isolation logic can leak data across tenants. For regulated industries (finance, aviation, logistics, healthcare), tenant isolation is not optional — it is mandatory and audited.
This article explains how to design a strong Tenant-Isolated Security Boundary using SQL Server, .NET, and Angular. This includes data partitioning, row-level access controls, encryption boundaries, token scoping, network segmentation, and domain event isolation.
This is written with practical implementation guidance for senior developers, architects, and DevOps engineers.
Table Of Contents
Understanding Multi-Tenant Isolation Types
Selecting the Right Model (Shared DB, Schema per Tenant, DB per Tenant)
Designing Isolation Boundaries Across Layers
SQL Server Isolation Techniques
.NET API Layer Isolation
Angular Client-Side Isolation
Token-Based Tenant Enforcement
Encryption and Key Isolation
Preventing Cross-Tenant Leaks
Workflow, Eventing, and Cache Isolation
Architecture Diagram
Request Flow Diagram
Best Practices and Anti-Patterns
Sample Implementation (SQL + .NET + Angular)
Summary
Understanding Multi-Tenant Isolation Types
A multi-tenant system has three broad isolation models.
1. Shared Database, Shared Schema
All tenants share tables, but every table has a TenantId column.
This is cost-efficient but riskier.
2. Shared Database, Separate Schema
Each tenant has its own schema:
TenantA.Users
TenantB.Users
TenantC.Users
Good balance for 50–500 tenants.
3. Separate Database Per Tenant
Every tenant gets a separate DB instance.
Best for strict isolation, compliance, and heavy workloads.
How To Choose The Right Model
| Model | When to use | Cost | Security |
|---|
| Shared Schema | Many small tenants | Lowest | Lowest |
| Separate Schema | Mid-size tenants | Medium | Medium–High |
| Separate DB | Enterprise tenants | Highest | Very High |
Designing Isolation Boundaries Across Layers
Tenant isolation must exist at all layers:
Network layer: restrict cross-tenant access
Database layer: row filtering, schema separation
API layer: token-based tenant enforcement
UI layer: tenant-scoped navigation and data
Event layer: isolated message streams
Cache layer: per-tenant cache keys
File/Blob layer: tenant root folders
Logging and monitoring: tenant-based log correlation
Isolation in only one layer is not enough.
SQL Server Isolation Techniques
1. TenantId Column Enforcement
Every table includes a TenantId column:
CREATE TABLE Orders(
OrderId INT PRIMARY KEY,
TenantId INT NOT NULL,
Amount DECIMAL(18,2),
CreatedOn DATETIME
);
2. Row-Level Security (RLS)
Use security predicates to filter rows automatically:
CREATE FUNCTION fnTenantPredicate(@TenantId AS INT)
RETURNS TABLE
AS
RETURN SELECT 1 AS fn_result
WHERE @TenantId = CAST(SESSION_CONTEXT(N'TenantId') AS INT);
Bind policy
CREATE SECURITY POLICY TenantPolicy
ADD FILTER PREDICATE dbo.fnTenantPredicate(TenantId) ON Orders;
3. SESSION_CONTEXT for Tenant Enforcement
In .NET API call:
await db.ExecuteAsync("EXEC sp_set_session_context @key='TenantId', @value=@tId", new { tId });
SQL Server now filters rows automatically.
4. Schema-Based Isolation
Useful for strong logical boundaries:
TenantA.Orders
TenantB.Orders
TenantC.Orders
5. Database-Per-Tenant Isolation
Each tenant’s DB connection string is loaded dynamically.
.NET API Layer Isolation
The API layer is the core enforcement point.
1. Tenant Extractor Middleware
public class TenantMiddleware
{
private readonly RequestDelegate _next;
public TenantMiddleware(RequestDelegate next) => _next = next;
public async Task Invoke(HttpContext context, ITenantResolver resolver)
{
var tenantId = resolver.Resolve(context);
context.Items["TenantId"] = tenantId;
await _next(context);
}
}
2. Enforcing Tenant in Repositories
public async Task<IEnumerable<Order>> GetOrdersAsync()
{
var tenantId = _contextAccessor.HttpContext.Items["TenantId"];
return await _db.Orders.Where(x => x.TenantId == tenantId).ToListAsync();
}
3. Prevent Overriding TenantId
Do not accept TenantId from request body.
Always set it server-side.
Angular Client-Side Isolation
Angular should not trust itself to enforce security, but it should:
Hide cross-tenant navigation
Load data only for the active tenant
Prevent user from switching tenants unless authorized
Example: store tenant context in a service.
@Injectable({ providedIn: 'root' })
export class TenantContextService {
private tenantId = new BehaviorSubject<number>(0);
tenant$ = this.tenantId.asObservable();
setTenant(id: number) { this.tenantId.next(id); }
}
Add tenant header in HTTP calls:
const headers = new HttpHeaders().set('X-Tenant', tenantId);
return this.http.get('/api/orders', { headers });
Token-Based Tenant Enforcement
JWT should include:
{"sub": "user1","tenantId": 45,"role": "Admin"}
API reads this claim and validates against the request header.
Do not allow tenants to manipulate TenantId in UI.
Encryption And Key Isolation
Use separate keys per tenant:
/keys/tenantA/
master.key
/keys/tenantB/
master.key
In .NET:
var keyPath = $"keys/{tenantId}/master.key";
This ensures compromise of one tenant does not affect others.
Preventing Cross-Tenant Leaks
Techniques:
Avoid joins across tenant tables
Prevent caching of unscoped queries
Always scope cache keys using tenant id
Prevent log messages from mixing tenant data
Enforce unit and integration tests for isolation
Workflow, Eventing, And Cache Isolation
Event Streams
tenantA-orders-topic
tenantB-orders-topic
Cache Keys
cache:tenant:45:orders:123
Blob Root Folders
/tenantA/uploads/
/tenantB/uploads/
Everything must be physically partitioned.
Architecture Diagram
+------------------------+
| Angular UI |
| tenant-scoped routes |
+-----------+------------+
|
v
+------------------------+
| API Gateway |
| tenant validation |
+-----------+-------------+
|
v
+-----------Network Boundary-----------------------------+
| API Services |
| +-------------------+----------------+ |
| | Tenant Middleware| Token Checker | |
| +---------+---------+----------------+ |
| | |
| v |
| +-----------------------+ |
| | Database Router | |
| | (tenant → DB/schema) | |
| +-----------+-----------+ |
+-------------------|------------------------------------+
v
+-----------------------------+
| SQL Server |
| RLS / Schemas / DBs |
+-----------------------------+
Request Flow Diagram
[Angular Request]
|
v
[X-Tenant Header]
|
v
[API Middleware → resolve tenant]
|
v
[Set SESSION_CONTEXT(TenantId)]
|
v
[SQL Server Row-Level Security]
|
v
[Filtered Data Returned]
Sample Implementation (SQL + .NET + Angular)
SQL RLS Setup
Already covered above.
.NET Configure Middleware
app.UseMiddleware<TenantMiddleware>();
Add claim validation:
if (jwtTenant != headerTenant)
return Unauthorized("Invalid tenant context");
Angular Tenant Selector
<select (change)="onTenantChange($event)">
<option *ngFor="let t of tenants" [value]="t.id">{{t.name}}</option>
</select>