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

LayerAzure ServicePurposeMulti-Tenant Consideration
Global EdgeAzure Front Door PremiumGlobal load balancing, SSL termination, WAF protection, content cachingSingle instance serving all tenants with tenant-aware routing
API GatewayAzure API Management (Multi-region)API abstraction, rate limiting, tenant validation, policy enforcementPer-tenant rate limits, subscription keys, usage analytics
IdentityMicrosoft Entra ID B2BAuthentication, authorization, tenant claim injectionTenant ID embedded in JWT claims
SecretsAzure Key VaultCentralized secrets, certificates, connection stringsTenant-specific secrets isolated by naming convention
ComputeAzure App Service (Premium V3)REST/GraphQL APIs, tenant-aware business logicShared app services with tenant context in all requests
Async ProcessingAzure Functions (Premium Plan)Background jobs, scheduled tasks, event handlersTenant ID in message metadata for isolation
DatabaseAzure SQL Database (Elastic Pool)Pooled tenant data with row-level securitySingle database, TenantIddiscriminator column
Object StorageAzure Blob Storage (RA-GZRS)Photos, documents, invoices, vehicle imagesTenant-prefixed container naming
CacheAzure Cache for Redis (Premium)Session state, tenant configuration, frequently accessed dataTenant-keyed cache entries
MessagingAzure Service Bus (Premium)Reliable async messaging, workflow orchestrationTenant ID in message properties
EventsAzure Event GridDomain events, webhooks, integrationsTenant-filtered event subscriptions
NotificationsAzure Notification HubsPush notifications to mobile devicesPer-tenant notification templates and tags
MonitoringApplication InsightsTelemetry, performance, errors, custom metricsTenant 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

AspectPooled DB (Recommended)Schema-per-TenantDatabase-per-Tenant
Cost? Most efficient – Shared resourcesModerate – Complex management? Highest – Separate DB instances
IsolationApplication-enforced RLSStrong – Schema isolationStrongest – Physical separation
Ops Complexity? Low – Single deploymentHigh – Multiple schemas? Very High – Multiple DBs
Scale Limit500-1000 tenants100-200 tenants50-100 tenants
Schema Updates? Single migrationComplex coordinationVery complex
Backup/RestorePoint-in-time for allPer-schema complexity? Per-tenant granular
Best ForSmall-medium tenants (7 users avg)Regulated industriesEnterprise 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:

  1. Request arrives at Azure Front Door with custom domain or tenant subdomain (e.g., joes-auto.garagehub.com)
  2. API Management extracts tenant context from:
    • JWT token claims (tid or custom tenant_id claim)
    • Request headers (X-Tenant-ID)
    • Subdomain parsing
  3. 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
  4. Request forwarded to App Service with enriched headers including X-Tenant-ID
  5. Application middleware validates tenant context and sets it in request scope
  6. Database queries automatically filtered by TenantId via:
    • 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