This tutorial demonstrates how to build secure, scalable multi-tenant Software-as-a-Service (SaaS) applications on Azure using Farmer. Based on Microsoft’s SaaS Architecture guidance and battle-tested patterns from Azure customers.
Note on Pricing: Cost estimates in this guide are approximate as of January 2025 in USD for the East US region. Azure pricing varies by region, currency, and billing agreements. Always verify current pricing at https://azure.microsoft.com/pricing/calculator/ before making budget decisions.
Multi-tenancy allows a single application instance to serve multiple customers (tenants) while maintaining data isolation and security. This architecture is fundamental to modern SaaS applications.
Key Requirements:
Reference: Microsoft SaaS Architecture documentation (https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/overview)
Azure supports three primary isolation strategies. The choice depends on tenant count, data sensitivity, and cost constraints.
Use Azure SQL Database with schemas for logical isolation. Best for 100-1000+ tenants with moderate isolation requirements.
Advantages:
Disadvantages:
open Farmer
open Farmer.Builders
open Sql
let multiTenantDatabase = sqlServer {
name "multitenant-sql-server"
admin_username "sqladmin"
add_databases [
sqlDb {
name "shared-tenant-db"
sku (GeneralPurpose Gen5_4) // 4 vCores for multi-tenant
}
]
// Enable Azure services access for tenant isolation auditing
enable_azure_firewall
}
Implementing Row-Level Security (RLS):
After deploying with Farmer, configure RLS using T-SQL:
-- Create schema per tenant
CREATE SCHEMA TenantA;
CREATE SCHEMA TenantB;
-- Create table in each schema
CREATE TABLE TenantA.Orders (
OrderId INT PRIMARY KEY,
CustomerId INT,
OrderDate DATETIME
);
-- Enable Row Level Security
ALTER TABLE TenantA.Orders ENABLE ROW LEVEL SECURITY;
-- Create security policy
CREATE FUNCTION TenantA.fn_securitypredicate(@TenantId INT)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS fn_securitypredicate_result
WHERE @TenantId = CAST(SESSION_CONTEXT(N'TenantId') AS INT);
CREATE SECURITY POLICY TenantA.tenantSecurityPolicy
ADD FILTER PREDICATE TenantA.fn_securitypredicate(TenantId) ON TenantA.Orders
WITH (STATE = ON);
Reference: Azure SQL Row-Level Security (https://learn.microsoft.com/en-us/sql/relational-databases/security/row-level-security)
Dedicate entire database to each tenant. Best for <100 enterprise tenants with strict isolation needs.
Advantages:
Disadvantages:
open Sql
// Function to create database for new tenant
let createTenantDatabase (tenantName: string) =
sqlDb {
name (sprintf "tenant-%s-db" tenantName)
sku DtuSku.S1 // Start small, scale per tenant
}
// Multi-tenant SQL Server with tenant-specific databases
let enterpriseMultiTenantServer = sqlServer {
name "enterprise-tenant-server"
admin_username "tenantadmin"
add_databases [
createTenantDatabase "acme-corp"
createTenantDatabase "globex-inc"
createTenantDatabase "initech-llc"
]
// Elastic pool for cost optimization across tenant databases
elastic_pool_name "tenant-pool"
elastic_pool_sku PoolSku.Standard400 // 400 eDTUs shared
}
Cost Optimization with Elastic Pools:
According to Microsoft’s SaaS patterns, elastic pools reduce costs by 30-50% for database-per-tenant architectures.
| Approach | Cost (10 tenants) | Cost (50 tenants) |
|---|---|---|
| Individual S1 databases | $300/month | $1,500/month |
| Elastic Pool (400 eDTUs) | $240/month | $240/month |
| Savings | 20% | 84% |
Combine strategies: Small tenants share schema, enterprise tenants get dedicated databases.
open Sql
// Shared database for small/medium tenants
let sharedServer = sqlServer {
name "shared-tenants-server"
admin_username "sharedadmin"
add_databases [
sqlDb {
name "shared-tenants-db"
sku (GeneralPurpose Gen5_8) // 8 vCores
}
]
}
// Dedicated server for enterprise tenants
let enterpriseServer = sqlServer {
name "enterprise-tenants-server"
admin_username "enterpriseadmin"
add_databases [
createTenantDatabase "fortune500-client"
]
}
let hybridMultiTenantArchitecture = arm {
location Location.EastUS
add_resources [
sharedServer
enterpriseServer
]
}
Maintain a central tenant registry using Cosmos DB for global distribution and low latency.
open Farmer.CosmosDb
let tenantRegistry = cosmosDb {
name "tenant-registry"
account_name "saas-tenants"
// Partition by tenant ID for efficient queries
add_containers [
cosmosContainer {
name "tenants"
partition_key [ "/tenantId" ] Hash
}
cosmosContainer {
name "tenant-configurations"
partition_key [ "/tenantId" ] Hash
}
]
consistency_policy Session
throughput 400<CosmosDb.RU>
}
Tenant Registry Schema:
{
"tenantId": "acme-corp",
"tenantName": "Acme Corporation",
"tier": "enterprise",
"status": "active",
"createdDate": "2025-01-01T00:00:00Z",
"databaseConnection": "enterprise-tenants-server",
"databaseName": "tenant-acme-corp-db",
"features": ["advanced-reporting", "api-access"],
"billingPlan": "enterprise-annual",
"customDomain": "acme.app.example.com"
}
Store per-tenant secrets and configuration in Azure Key Vault with tenant-scoped access policies.
open Farmer.KeyVault
// Function to create tenant-specific Key Vault
let createTenantKeyVault (tenantId: string) =
keyVault {
name (sprintf "tenant-%s-secrets" tenantId)
sku Sku.Standard
// Tenant-specific secrets (values should come from secure parameters)
add_secret (sprintf "api-key-%s" tenantId)
add_secret (sprintf "encryption-key-%s" tenantId)
add_secret (sprintf "integration-token-%s" tenantId)
enable_soft_delete_with_purge_protection
}
let acmeVault = createTenantKeyVault "acme-corp"
let globexVault = createTenantKeyVault "globex-inc"
let initechVault = createTenantKeyVault "initech-llc"
let tenantSecrets = arm {
location Location.EastUS
add_resources [
acmeVault
globexVault
initechVault
]
}
Build tenant-aware applications using Azure App Service with tenant context middleware.
// App Service Plan scaled for multi-tenant load
let appPlan = servicePlan {
name "multitenant-plan"
sku WebApp.Sku.P1V3 // Premium for production multi-tenant
number_of_workers 3
}
// Application insights for monitoring
let mtInsights = appInsights {
name "multitenant-insights"
}
// Main application
let mtApp = webApp {
name "multitenant-webapp"
link_to_service_plan appPlan
always_on
https_only
link_to_app_insights mtInsights.Name
// Connection to tenant registry
setting "CosmosDbEndpoint" tenantRegistry.Endpoint
setting "CosmosDbKey" tenantRegistry.PrimaryKey
}
let multiTenantWebApp = arm {
location Location.EastUS
add_resources [
appPlan
tenantRegistry
mtInsights
mtApp
]
}
Tenant Context Middleware (F# example with Giraffe):
open Giraffe
open Microsoft.AspNetCore.Http
open System.Threading.Tasks
type ITenantRegistry =
abstract GetTenantAsync: string -> Task<Tenant option>
let tenantMiddleware (registry: ITenantRegistry) : HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) -> task {
// Extract tenant from subdomain: acme.app.example.com
let host = ctx.Request.Host.Host
let tenantId = host.Split('.') |> Array.tryHead |> Option.defaultValue ""
// Resolve tenant configuration
let! tenant = registry.GetTenantAsync(tenantId)
match tenant with
| None ->
ctx.Response.StatusCode <- 404
return! Task.FromResult None
| Some tenant ->
// Set tenant context for request
ctx.Items.["TenantId"] <- box tenant.TenantId
ctx.Items.["TenantConfig"] <- box tenant
return! next ctx
}
Implement defense-in-depth for multi-tenant security, following Microsoft Security Best Practices.
Always include tenant context in queries:
// ❌ BAD: No tenant filtering
let getOrder (ctx: HttpContext) (orderId: Guid) = task {
let! order = dbContext.Orders
|> Seq.filter (fun o -> o.OrderId = orderId)
|> Seq.tryHead
return order
}
// ✅ GOOD: Tenant context enforced
let getOrder (ctx: HttpContext) (orderId: Guid) = task {
let tenantId = ctx.Items.["TenantId"] :?> string
let! order = dbContext.Orders
|> Seq.filter (fun o -> o.TenantId = tenantId && o.OrderId = orderId)
|> Seq.tryHead
return order
}
let tenantAuditLog = appInsights {
name "tenant-audit-logs"
retention_days 90 // Compliance requirement
// Custom dimensions for tenant tracking
// Configure in application code
}
Log tenant context in Application Insights:
open Microsoft.ApplicationInsights
open System.Collections.Generic
let logOrderCreated (telemetry: TelemetryClient) tenantId orderId userId =
let properties = Dictionary<string, string>()
properties.["TenantId"] <- tenantId
properties.["OrderId"] <- orderId.ToString()
properties.["UserId"] <- userId
telemetry.TrackEvent("OrderCreated", properties)
Implement chargeback using Azure Cost Management and resource tagging.
// Tag all tenant resources for cost allocation
let taggedTenantResources = arm {
location Location.EastUS
add_resources [
sqlServer {
name "tenant-acme-server"
add_tags [
"TenantId", "acme-corp"
"CostCenter", "acme-billing"
"Tier", "enterprise"
]
add_databases [ /* ... */ ]
}
]
}
Cost Allocation by Tier:
| Tier | Shared Resources | Dedicated Resources | Monthly Cost |
|---|---|---|---|
| Free | Shared DB schema | None | $0 (subsidized) |
| Standard | Shared DB schema | None | $10-25/tenant |
| Premium | Shared DB schema | Dedicated Key Vault | $50-100/tenant |
| Enterprise | Dedicated database | Dedicated server, Key Vault | $500-2000/tenant |
Automate tenant onboarding using Azure Functions and Logic Apps.
let tenantProvisioningFunction = functions {
name "tenant-provisioning"
storage_account_name "provisioningstorage"
setting "SqlConnectionString" multiTenantDatabase.ConnectionString "shared-tenant-db"
setting "CosmosDbEndpoint" tenantRegistry.Endpoint
// Function triggered by new tenant sign-up
// Implements provisioning workflow
}
Provisioning Steps:
Reference: Microsoft SaaS Fulfillment APIs (https://learn.microsoft.com/en-us/azure/marketplace/partner-center-portal/pc-saas-fulfillment-apis)
Implement per-tenant monitoring dashboards using Application Insights.
let tenantMonitoring = appInsights {
name "tenant-monitoring"
// Workbook for per-tenant metrics
// Configure via Azure Portal
}
Key Metrics per Tenant:
Multi-tenant SaaS applications must address compliance requirements per tenant.
GDPR Compliance:
HIPAA Compliance:
SOC 2 Compliance:
This pattern is used by successful Azure SaaS providers including Microsoft’s own Dynamics 365 and Office 365.
Case Study: Enterprise SaaS Platform
A B2B SaaS company serving 500+ customers implemented hybrid multi-tenancy:
Architecture:
Results:
Microsoft Official Documentation:
Microsoft Learn Paths:
Community Resources:
Books:
This tutorial demonstrates proven multi-tenant patterns based on Microsoft’s guidance and successful Azure customer implementations. Always validate your specific compliance and security requirements with legal and security teams before implementing multi-tenancy.