Multi-Tenant Car Garage Repair SaaS Platform Architecture for Microsoft Azure
Executive Summary
This document presents a production-ready, enterprise-grade architecture for a multi-tenant SaaS platform designed to serve car garage repair businesses. The solution leverages Azure-native services with a pooled database tenancy model to support 99% availability, 24-hour RTO/RPO, and offline-capable mobile applications across iOS and Android. The architecture supports customer-facing mobile apps and web-based admin portals for garage staff, optimized for multi-region deployment to ensure low latency and disaster recovery capabilities.
A) High-Level Azure Architecture
Architecture Overview
The architecture follows a modern, cloud-native approach with clear tenant boundaries, leveraging Azure’s global infrastructure for resilience and performance.
<style> :root { --accent: #464feb; --timeline-ln: linear-gradient(to bottom, transparent 0%, #b0beff 15%, #b0beff 85%, transparent 100%); --timeline-border: #ffffff; --bg-card: #f5f7fa; --bg-hover: #ebefff; --text-title: #424242; --text-accent: var(--accent); --text-sub: #424242; --radius: 12px; --border: #e0e0e0; --shadow: 0 2px 10px rgba(0, 0, 0, 0.06); --hover-shadow: 0 4px 14px rgba(39, 16, 16, 0.1); --font: "Segoe Sans", "Segoe UI", "Segoe UI Web (West European)", -apple-system, "system-ui", Roboto, "Helvetica Neue", sans-serif; --overflow-wrap: break-word; }
@media (prefers-color-scheme: dark) { :root { --accent: #7385ff; --timeline-ln: linear-gradient(to bottom, transparent 0%, transparent 3%, #6264a7 30%, #6264a7 50%, transparent 97%, transparent 100%); --timeline-border: #424242; --bg-card: #1a1a1a; --bg-hover: #2a2a2a; --text-title: #ffffff; --text-sub: #ffffff; --shadow: 0 2px 10px rgba(0, 0, 0, 0.3); --hover-shadow: 0 4px 14px rgba(0, 0, 0, 0.5); --border: #3d3d3d; } }
@media (prefers-contrast: more), (forced-colors: active) { :root { --accent: ActiveText; --timeline-ln: ActiveText; --timeline-border: Canvas; --bg-card: Canvas; --bg-hover: Canvas; --text-title: CanvasText; --text-sub: CanvasText; --shadow: 0 2px 10px Canvas; --hover-shadow: 0 4px 14px Canvas; --border: ButtonBorder; } }
.insights-container { display: grid; grid-template-columns: repeat(2,minmax(240px,1fr)); padding: 0px 16px 0px 16px; gap: 16px; margin: 0 0; font-family: var(--font); }
.insight-card:last-child:nth-child(odd){ grid-column: 1 / -1; }
.insight-card { background-color: var(--bg-card); border-radius: var(--radius); border: 1px solid var(--border); box-shadow: var(--shadow); min-width: 220px; padding: 16px 20px 16px 20px; }
.insight-card:hover { background-color: var(--bg-hover); }
.insight-card h4 { margin: 0px 0px 8px 0px; font-size: 1.1rem; color: var(--text-accent); font-weight: 600; display: flex; align-items: center; gap: 8px; }
.insight-card .icon { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; font-size: 1.1rem; color: var(--text-accent); }
.insight-card p { font-size: 0.92rem; color: var(--text-sub); line-height: 1.5; margin: 0px; overflow-wrap: var(--overflow-wrap); }
.insight-card p b, .insight-card p strong { font-weight: 600; }
.metrics-container { display:grid; grid-template-columns:repeat(2,minmax(210px,1fr)); font-family: var(--font); padding: 0px 16px 0px 16px; gap: 16px; }
.metric-card:last-child:nth-child(odd){ grid-column:1 / -1; }
.metric-card { flex: 1 1 210px; padding: 16px; background-color: var(--bg-card); border-radius: var(--radius); border: 1px solid var(--border); text-align: center; display: flex; flex-direction: column; gap: 8px; }
.metric-card:hover { background-color: var(--bg-hover); }
.metric-card h4 { margin: 0px; font-size: 1rem; color: var(--text-title); font-weight: 600; }
.metric-card .metric-card-value { margin: 0px; font-size: 1.4rem; font-weight: 600; color: var(--text-accent); }
.metric-card p { font-size: 0.85rem; color: var(--text-sub); line-height: 1.45; margin: 0; overflow-wrap: var(--overflow-wrap); }
.timeline-container { position: relative; margin: 0 0 0 0; padding: 0px 16px 0px 56px; list-style: none; font-family: var(--font); font-size: 0.9rem; color: var(--text-sub); line-height: 1.4; }
.timeline-container::before { content: ""; position: absolute; top: 0; left: calc(-40px + 56px); width: 2px; height: 100%; background: var(--timeline-ln); }
.timeline-container > li { position: relative; margin-bottom: 16px; padding: 16px 20px 16px 20px; border-radius: var(--radius); background: var(--bg-card); border: 1px solid var(--border); }
.timeline-container > li:last-child { margin-bottom: 0px; }
.timeline-container > li:hover { background-color: var(--bg-hover); }
.timeline-container > li::before { content: ""; position: absolute; top: 18px; left: -40px; width: 14px; height: 14px; background: var(--accent); border: var(--timeline-border) 2px solid; border-radius: 50%; transform: translateX(-50%); box-shadow: 0px 0px 2px 0px #00000012, 0px 4px 8px 0px #00000014; }
.timeline-container > li h4 { margin: 0 0 5px; font-size: 1rem; font-weight: 600; color: var(--accent); }
.timeline-container > li h4 em { margin: 0 0 5px; font-size: 1rem; font-weight: 600; color: var(--accent); font-style: normal; }
.timeline-container > li * { margin: 0; font-size: 0.9rem; color: var(--text-sub); line-height: 1.4; }
.timeline-container > li * b, .timeline-container > li * strong { font-weight: 600; } @media (max-width:600px){ .metrics-container, .insights-container{ grid-template-columns:1fr; } }</style><div class="metrics-container"> <div class="metric-card"><h4>Target Availability</h4><div class="metric-card-value">99%</div><p>SLA with multi-region deployment</p></div> <div class="metric-card"><h4>RTO/RPO Target</h4><div class="metric-card-value">24 hrs</div><p>Disaster recovery objective</p></div> <div class="metric-card"><h4>Avg Users/Tenant</h4><div class="metric-card-value">7</div><p>Staff per garage location</p></div> <div class="metric-card"><h4>Tenancy Model</h4><div class="metric-card-value">Pooled DB</div><p>Cost-optimized isolation</p></div>
Azure Services Selection Justification
Azure App Service (Recommended) vs AKS:
- Azure App Service is the recommended compute platform for this workload because:
- Simplified operations: Built-in auto-scaling, load balancing, and managed patching reduce operational overhead
- Cost-effective for small-to-medium scale: With average 7 users per tenant, the operational complexity of Kubernetes is unnecessary
- Multi-tenant features: Native support for deployment slots, traffic routing, and scaling rules suitable for pooled tenancy models
- Faster time-to-market: PaaS abstraction accelerates development without sacrificing control
- AKS would be preferred if: The platform scales to hundreds of tenants with complex microservices requiring fine-grained orchestration, or if there’s need for hybrid/edge deployment
Core Architecture Components
graph TB subgraph "Client Layer" MA[Mobile Apps iOS/Android] WA[Web Admin Portal] end subgraph "Global Edge - Azure Front Door" AFD[Azure Front Door Premium] WAF[Web Application Firewall] CDN[Content Delivery] end subgraph "API Gateway Layer" APIM[Azure API Management] subgraph "Multi-Region Gateways" APIM1[Primary Region Gateway] APIM2[Secondary Region Gateway] end end subgraph "Identity & Security" ENTRA[Microsoft Entra ID B2B] KV[Azure Key Vault] end subgraph "Application Tier - Region 1 Primary" AS1[Azure App Service Plan] API1[API Apps - REST/GraphQL] FUNC1[Azure Functions - Background Jobs] end subgraph "Application Tier - Region 2 Secondary" AS2[Azure App Service Plan] API2[API Apps - REST/GraphQL] FUNC2[Azure Functions - Background Jobs] end subgraph "Data Tier - Region 1" SQL1["(Azure SQL Database<br/>Elastic Pool)"] BLOB1["Blob Storage<br/>Hot/Cool Tiers"] REDIS1[Azure Cache for Redis] end subgraph "Data Tier - Region 2" SQL2["(Azure SQL Database<br/>Read Replica)"] BLOB2["Blob Storage<br/>RA-GZRS Replica"] end subgraph "Messaging & Events" SB["Azure Service Bus<br/>Premium Tier"] EG[Azure Event Grid] end subgraph "Monitoring & Operations" MON[Azure Monitor] AI[Application Insights] LA[Log Analytics] end subgraph "Notification Services" NH[Azure Notification Hubs] end MA --> AFD WA --> AFD AFD --> WAF WAF --> APIM APIM --> APIM1 APIM --> APIM2 APIM1 --> ENTRA APIM1 --> AS1 APIM2 --> AS2 AS1 --> SQL1 AS1 --> BLOB1 AS1 --> REDIS1 AS2 --> SQL2 AS2 --> BLOB2 API1 --> SB API1 --> EG API2 --> SB FUNC1 --> SB FUNC2 --> SB SQL1 -.Geo-Replication.-> SQL2 BLOB1 -.RA-GZRS.-> BLOB2 AS1 --> KV AS2 --> KV API1 --> NH API2 --> NH NH --> MA AS1 --> AI AS2 --> AI AI --> MON MON --> LA
Service Mapping to Architecture Layers
| Layer | Azure Service | Purpose | Multi-Tenant Consideration |
| Global Edge | Azure Front Door Premium | Global load balancing, SSL termination, WAF protection, content caching | Single instance serving all tenants with tenant-aware routing |
| API Gateway | Azure API Management (Multi-region) | API abstraction, rate limiting, tenant validation, policy enforcement | Per-tenant rate limits, subscription keys, usage analytics |
| Identity | Microsoft Entra ID B2B | Authentication, authorization, tenant claim injection | Tenant ID embedded in JWT claims |
| Secrets | Azure Key Vault | Centralized secrets, certificates, connection strings | Tenant-specific secrets isolated by naming convention |
| Compute | Azure App Service (Premium V3) | REST/GraphQL APIs, tenant-aware business logic | Shared app services with tenant context in all requests |
| Async Processing | Azure Functions (Premium Plan) | Background jobs, scheduled tasks, event handlers | Tenant ID in message metadata for isolation |
| Database | Azure SQL Database (Elastic Pool) | Pooled tenant data with row-level security | Single database, TenantIddiscriminator column |
| Object Storage | Azure Blob Storage (RA-GZRS) | Photos, documents, invoices, vehicle images | Tenant-prefixed container naming |
| Cache | Azure Cache for Redis (Premium) | Session state, tenant configuration, frequently accessed data | Tenant-keyed cache entries |
| Messaging | Azure Service Bus (Premium) | Reliable async messaging, workflow orchestration | Tenant ID in message properties |
| Events | Azure Event Grid | Domain events, webhooks, integrations | Tenant-filtered event subscriptions |
| Notifications | Azure Notification Hubs | Push notifications to mobile devices | Per-tenant notification templates and tags |
| Monitoring | Application Insights | Telemetry, performance, errors, custom metrics | Tenant dimension in all telemetry |
B) Multi-Tenant SaaS Design (Deep Dive)
Tenant Definition
A tenant is defined as a garage company, which may operate one or multiple physical locations. Each garage company represents an independent business entity with its own:
- Staff (owners, service advisors, mechanics)
- Customers and their vehicles
- Work orders, estimates, and invoices
- Configuration and branding preferences
Tenancy Model: Pooled Database with Discriminator Pattern
The architecture implements a pooled multi-tenant model where all tenants share the same Azure SQL Database within an Elastic Pool, using a TenantId discriminator column for logical isolation.
<style> :root { --accent: #464feb; --timeline-ln: linear-gradient(to bottom, transparent 0%, #b0beff 15%, #b0beff 85%, transparent 100%); --timeline-border: #ffffff; --bg-card: #f5f7fa; --bg-hover: #ebefff; --text-title: #424242; --text-accent: var(--accent); --text-sub: #424242; --radius: 12px; --border: #e0e0e0; --shadow: 0 2px 10px rgba(0, 0, 0, 0.06); --hover-shadow: 0 4px 14px rgba(39, 16, 16, 0.1); --font: "Segoe Sans", "Segoe UI", "Segoe UI Web (West European)", -apple-system, "system-ui", Roboto, "Helvetica Neue", sans-serif; --overflow-wrap: break-word; }
@media (prefers-color-scheme: dark) { :root { --accent: #7385ff; --timeline-ln: linear-gradient(to bottom, transparent 0%, transparent 3%, #6264a7 30%, #6264a7 50%, transparent 97%, transparent 100%); --timeline-border: #424242; --bg-card: #1a1a1a; --bg-hover: #2a2a2a; --text-title: #ffffff; --text-sub: #ffffff; --shadow: 0 2px 10px rgba(0, 0, 0, 0.3); --hover-shadow: 0 4px 14px rgba(0, 0, 0, 0.5); --border: #3d3d3d; } }
@media (prefers-contrast: more), (forced-colors: active) { :root { --accent: ActiveText; --timeline-ln: ActiveText; --timeline-border: Canvas; --bg-card: Canvas; --bg-hover: Canvas; --text-title: CanvasText; --text-sub: CanvasText; --shadow: 0 2px 10px Canvas; --hover-shadow: 0 4px 14px Canvas; --border: ButtonBorder; } }
.insights-container { display: grid; grid-template-columns: repeat(2,minmax(240px,1fr)); padding: 0px 16px 0px 16px; gap: 16px; margin: 0 0; font-family: var(--font); }
.insight-card:last-child:nth-child(odd){ grid-column: 1 / -1; }
.insight-card { background-color: var(--bg-card); border-radius: var(--radius); border: 1px solid var(--border); box-shadow: var(--shadow); min-width: 220px; padding: 16px 20px 16px 20px; }
.insight-card:hover { background-color: var(--bg-hover); }
.insight-card h4 { margin: 0px 0px 8px 0px; font-size: 1.1rem; color: var(--text-accent); font-weight: 600; display: flex; align-items: center; gap: 8px; }
.insight-card .icon { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; font-size: 1.1rem; color: var(--text-accent); }
.insight-card p { font-size: 0.92rem; color: var(--text-sub); line-height: 1.5; margin: 0px; overflow-wrap: var(--overflow-wrap); }
.insight-card p b, .insight-card p strong { font-weight: 600; }
.metrics-container { display:grid; grid-template-columns:repeat(2,minmax(210px,1fr)); font-family: var(--font); padding: 0px 16px 0px 16px; gap: 16px; }
.metric-card:last-child:nth-child(odd){ grid-column:1 / -1; }
.metric-card { flex: 1 1 210px; padding: 16px; background-color: var(--bg-card); border-radius: var(--radius); border: 1px solid var(--border); text-align: center; display: flex; flex-direction: column; gap: 8px; }
.metric-card:hover { background-color: var(--bg-hover); }
.metric-card h4 { margin: 0px; font-size: 1rem; color: var(--text-title); font-weight: 600; }
.metric-card .metric-card-value { margin: 0px; font-size: 1.4rem; font-weight: 600; color: var(--text-accent); }
.metric-card p { font-size: 0.85rem; color: var(--text-sub); line-height: 1.45; margin: 0; overflow-wrap: var(--overflow-wrap); }
.timeline-container { position: relative; margin: 0 0 0 0; padding: 0px 16px 0px 56px; list-style: none; font-family: var(--font); font-size: 0.9rem; color: var(--text-sub); line-height: 1.4; }
.timeline-container::before { content: ""; position: absolute; top: 0; left: calc(-40px + 56px); width: 2px; height: 100%; background: var(--timeline-ln); }
.timeline-container > li { position: relative; margin-bottom: 16px; padding: 16px 20px 16px 20px; border-radius: var(--radius); background: var(--bg-card); border: 1px solid var(--border); }
.timeline-container > li:last-child { margin-bottom: 0px; }
.timeline-container > li:hover { background-color: var(--bg-hover); }
.timeline-container > li::before { content: ""; position: absolute; top: 18px; left: -40px; width: 14px; height: 14px; background: var(--accent); border: var(--timeline-border) 2px solid; border-radius: 50%; transform: translateX(-50%); box-shadow: 0px 0px 2px 0px #00000012, 0px 4px 8px 0px #00000014; }
.timeline-container > li h4 { margin: 0 0 5px; font-size: 1rem; font-weight: 600; color: var(--accent); }
.timeline-container > li h4 em { margin: 0 0 5px; font-size: 1rem; font-weight: 600; color: var(--accent); font-style: normal; }
.timeline-container > li * { margin: 0; font-size: 0.9rem; color: var(--text-sub); line-height: 1.4; }
.timeline-container > li * b, .timeline-container > li * strong { font-weight: 600; } @media (max-width:600px){ .metrics-container, .insights-container{ grid-template-columns:1fr; } }</style><div class="insights-container"> <div class="insight-card"><h4>? Pooled Model Benefits</h4><p>Cost efficiency, simplified operations, easier schema updates, optimal for 7 users/tenant scale</p> </div> <div class="insight-card"><h4>? Isolation Strategy</h4><p>Row-Level Security (RLS), application-layer validation, separate Blob containers per tenant</p> </div> <div class="insight-card"><h4>? Resource Sharing</h4><p>Elastic Pool distributes DTUs/vCores across tenants, automatic load balancing</p> </div> <div class="insight-card"><h4>? Noisy Neighbor Protection</h4><p>Resource governance policies, query timeout limits, connection pooling per tenant</p> </div>
Isolation Model Comparison
| Aspect | Pooled DB (Recommended) | Schema-per-Tenant | Database-per-Tenant |
| Cost | ? Most efficient – Shared resources | Moderate – Complex management | ? Highest – Separate DB instances |
| Isolation | Application-enforced RLS | Strong – Schema isolation | Strongest – Physical separation |
| Ops Complexity | ? Low – Single deployment | High – Multiple schemas | ? Very High – Multiple DBs |
| Scale Limit | 500-1000 tenants | 100-200 tenants | 50-100 tenants |
| Schema Updates | ? Single migration | Complex coordination | Very complex |
| Backup/Restore | Point-in-time for all | Per-schema complexity | ? Per-tenant granular |
| Best For | Small-medium tenants (7 users avg) | Regulated industries | Enterprise customers |
Justification: Given the requirements of average 7 users per tenant, no strict compliance needs, and cost optimization, the pooled model provides the best balance.
Tenant Onboarding Lifecycle
<style> :root { --accent: #464feb; --timeline-ln: linear-gradient(to bottom, transparent 0%, #b0beff 15%, #b0beff 85%, transparent 100%); --timeline-border: #ffffff; --bg-card: #f5f7fa; --bg-hover: #ebefff; --text-title: #424242; --text-accent: var(--accent); --text-sub: #424242; --radius: 12px; --border: #e0e0e0; --shadow: 0 2px 10px rgba(0, 0, 0, 0.06); --hover-shadow: 0 4px 14px rgba(39, 16, 16, 0.1); --font: "Segoe Sans", "Segoe UI", "Segoe UI Web (West European)", -apple-system, "system-ui", Roboto, "Helvetica Neue", sans-serif; --overflow-wrap: break-word; }
@media (prefers-color-scheme: dark) { :root { --accent: #7385ff; --timeline-ln: linear-gradient(to bottom, transparent 0%, transparent 3%, #6264a7 30%, #6264a7 50%, transparent 97%, transparent 100%); --timeline-border: #424242; --bg-card: #1a1a1a; --bg-hover: #2a2a2a; --text-title: #ffffff; --text-sub: #ffffff; --shadow: 0 2px 10px rgba(0, 0, 0, 0.3); --hover-shadow: 0 4px 14px rgba(0, 0, 0, 0.5); --border: #3d3d3d; } }
@media (prefers-contrast: more), (forced-colors: active) { :root { --accent: ActiveText; --timeline-ln: ActiveText; --timeline-border: Canvas; --bg-card: Canvas; --bg-hover: Canvas; --text-title: CanvasText; --text-sub: CanvasText; --shadow: 0 2px 10px Canvas; --hover-shadow: 0 4px 14px Canvas; --border: ButtonBorder; } }
.insights-container { display: grid; grid-template-columns: repeat(2,minmax(240px,1fr)); padding: 0px 16px 0px 16px; gap: 16px; margin: 0 0; font-family: var(--font); }
.insight-card:last-child:nth-child(odd){ grid-column: 1 / -1; }
.insight-card { background-color: var(--bg-card); border-radius: var(--radius); border: 1px solid var(--border); box-shadow: var(--shadow); min-width: 220px; padding: 16px 20px 16px 20px; }
.insight-card:hover { background-color: var(--bg-hover); }
.insight-card h4 { margin: 0px 0px 8px 0px; font-size: 1.1rem; color: var(--text-accent); font-weight: 600; display: flex; align-items: center; gap: 8px; }
.insight-card .icon { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; font-size: 1.1rem; color: var(--text-accent); }
.insight-card p { font-size: 0.92rem; color: var(--text-sub); line-height: 1.5; margin: 0px; overflow-wrap: var(--overflow-wrap); }
.insight-card p b, .insight-card p strong { font-weight: 600; }
.metrics-container { display:grid; grid-template-columns:repeat(2,minmax(210px,1fr)); font-family: var(--font); padding: 0px 16px 0px 16px; gap: 16px; }
.metric-card:last-child:nth-child(odd){ grid-column:1 / -1; }
.metric-card { flex: 1 1 210px; padding: 16px; background-color: var(--bg-card); border-radius: var(--radius); border: 1px solid var(--border); text-align: center; display: flex; flex-direction: column; gap: 8px; }
.metric-card:hover { background-color: var(--bg-hover); }
.metric-card h4 { margin: 0px; font-size: 1rem; color: var(--text-title); font-weight: 600; }
.metric-card .metric-card-value { margin: 0px; font-size: 1.4rem; font-weight: 600; color: var(--text-accent); }
.metric-card p { font-size: 0.85rem; color: var(--text-sub); line-height: 1.45; margin: 0; overflow-wrap: var(--overflow-wrap); }
.timeline-container { position: relative; margin: 0 0 0 0; padding: 0px 16px 0px 56px; list-style: none; font-family: var(--font); font-size: 0.9rem; color: var(--text-sub); line-height: 1.4; }
.timeline-container::before { content: ""; position: absolute; top: 0; left: calc(-40px + 56px); width: 2px; height: 100%; background: var(--timeline-ln); }
.timeline-container > li { position: relative; margin-bottom: 16px; padding: 16px 20px 16px 20px; border-radius: var(--radius); background: var(--bg-card); border: 1px solid var(--border); }
.timeline-container > li:last-child { margin-bottom: 0px; }
.timeline-container > li:hover { background-color: var(--bg-hover); }
.timeline-container > li::before { content: ""; position: absolute; top: 18px; left: -40px; width: 14px; height: 14px; background: var(--accent); border: var(--timeline-border) 2px solid; border-radius: 50%; transform: translateX(-50%); box-shadow: 0px 0px 2px 0px #00000012, 0px 4px 8px 0px #00000014; }
.timeline-container > li h4 { margin: 0 0 5px; font-size: 1rem; font-weight: 600; color: var(--accent); }
.timeline-container > li h4 em { margin: 0 0 5px; font-size: 1rem; font-weight: 600; color: var(--accent); font-style: normal; }
.timeline-container > li * { margin: 0; font-size: 0.9rem; color: var(--text-sub); line-height: 1.4; }
.timeline-container > li * b, .timeline-container > li * strong { font-weight: 600; } @media (max-width:600px){ .metrics-container, .insights-container{ grid-template-columns:1fr; } }</style><ul class="timeline-container"> <li> <h4>Step 1: Tenant Provisioning Request</h4> <p>Admin creates tenant via portal ? Event published to Service Bus ? Provisioning workflow triggered</p></li> <li> <h4>Step 2: Data Initialization</h4> <p>Insert TenantId into Tenant table ? Create Blob container ? Initialize default configuration ? Generate API keys</p></li> <li> <h4>Step 3: Identity Setup</h4> <p>Create Entra ID B2B group ? Assign owner user ? Configure RBAC roles ? Issue invitation emails</p></li> <li> <h4>Step 4: Service Configuration</h4> <p>Register tenant in API Management ? Set rate limits ? Configure notification templates ? Enable monitoring</p></li> <li> <h4>Step 5: Activation & Notification</h4> <p>Mark tenant as Active ? Send welcome email ? Provide login credentials ? Schedule onboarding call</p></li>
Tenant-Aware Routing and Authorization
Every API request follows this flow:
- Request arrives at Azure Front Door with custom domain or tenant subdomain (e.g.,
joes-auto.garagehub.com) - API Management extracts tenant context from:
- JWT token claims (
tidor customtenant_idclaim) - Request headers (
X-Tenant-ID) - Subdomain parsing
- JWT token claims (
- Tenant validation policy in API Management:
- Verifies tenant exists and is active
- Checks subscription key if using API-key auth
- Enforces per-tenant rate limits
- Request forwarded to App Service with enriched headers including
X-Tenant-ID - Application middleware validates tenant context and sets it in request scope
- Database queries automatically filtered by
TenantIdvia:- Entity Framework global query filters
- SQL Row-Level Security (RLS) policies
Azure-Specific Implementation Patterns
Pattern 1: Tenant Context Propagation
// Middleware in Azure App Servicepublic class TenantContextMiddleware{ public async Task InvokeAsync(HttpContext context, TenantService tenantService) { var tenantId = context.User.FindFirst("tenant_id")?.Value ?? context.Request.Headers["X-Tenant-ID"].FirstOrDefault(); if (string.IsNullOrEmpty(tenantId)) throw new UnauthorizedAccessException("Tenant context missing"); var tenant = await tenantService.GetTenantAsync(tenantId); if (tenant == null || !tenant.IsActive) throw new UnauthorizedAccessException("Invalid or inactive tenant"); context.Items["TenantContext"] = tenant; await _next(context); }}
Pattern 2: Azure SQL Row-Level Security
-- Create security policy for tenant isolationCREATE FUNCTION dbo.fn_TenantPredicate(@TenantId UNIQUEIDENTIFIER)RETURNS TABLEWITH SCHEMABINDINGASRETURN SELECT 1 AS fn_TenantPredicate_resultWHERE @TenantId = CAST(SESSION_CONTEXT(N'TenantId') AS UNIQUEIDENTIFIER);
CREATE SECURITY POLICY TenantSecurityPolicyADD FILTER PREDICATE dbo.fn_TenantPredicate(TenantId) ON dbo.WorkOrders,ADD FILTER PREDICATE dbo.fn_TenantPredicate(TenantId) ON dbo.Customers,ADD FILTER PREDICATE dbo.fn_TenantPredicate(TenantId) ON dbo.Vehicles;
Noisy Neighbor Mitigation
Azure SQL Elastic Pool Configuration:
- Use Premium tier with sufficient eDTUs to handle burst loads
- Implement per-database min/max DTU limits to prevent single tenant monopolization
- Enable query performance insights to identify problematic queries
- Set query timeout limits (30 seconds default, 120 seconds max)
- Use Resource Governor for CPU/memory caps per session
Application-Level Protection:
- Rate limiting in API Management with per-tenant subscription keys
- Connection pool limits per tenant context
- Async job throttling using Azure Service Bus message rate limits
- Blob storage bandwidth limits per container (tenant-prefixed)
Monitoring & Alerts:
- Track DTU consumption per tenant using custom Application Insights metrics
- Alert on sustained high resource usage (>80% for 15+ minutes)
- Implement tenant usage dashboards for capacity planning
Per-Tenant Backup and Restore
Strategy:
- Database: Azure SQL point-in-time restore with tenant-specific data extraction
- Full backups retained for 35 days
- Transaction log backups every 5-10 minutes
- Tenant-specific restore: Clone database ? Extract tenant data ? Import to production
- Blob Storage: Soft delete enabled (30-day retention) + versioning
- Per-tenant container restoration from soft-deleted state
- Point-in-time restore for containers using blob versioning
- Recovery Procedure:
- Identify recovery point (timestamp)
- Restore SQL database to separate server
- Export tenant-specific rows using
WHERE TenantId = '{tenant-id}' - Restore blob container from versioned snapshots
- Import data to production with validation
- Notify tenant of restoration completion