| name | tenant-provisioning |
| description | Automated tenant onboarding infrastructure patterns. Covers provisioning workflows, resource creation, seed data, and tenant readiness verification. |
| allowed-tools | Read, Glob, Grep, Task, mcp__perplexity__search, mcp__perplexity__reason, mcp__microsoft-learn__microsoft_docs_search, mcp__microsoft-learn__microsoft_docs_fetch |
Tenant Provisioning Skill
Patterns for automating the creation and setup of new tenants in multi-tenant SaaS applications.
When to Use This Skill
Use this skill when:
- Tenant Provisioning tasks - Working on automated tenant onboarding infrastructure patterns. covers provisioning workflows, resource creation, seed data, and tenant readiness verification
- Planning or design - Need guidance on Tenant Provisioning approaches
- Best practices - Want to follow established patterns and standards
Overview
Tenant provisioning is the process of creating all resources needed for a new tenant to use the application. Speed and reliability of provisioning directly impact customer experience and operational efficiency.
Provisioning Architecture
+------------------------------------------------------------------+
| Tenant Provisioning Flow |
+------------------------------------------------------------------+
| |
| +----------+ +-------------+ +-------------+ +---------+ |
| | Signup |-->| Validation |-->| Provisioner |-->| Ready | |
| | Request | | & Approval | | Workflow | | State | |
| +----------+ +-------------+ +-------------+ +---------+ |
| | |
| +---------------------+---------------------+ |
| | | | |
| v v v |
| +-----------+ +-----------+ +---------+|
| | Database | | Storage | | Config ||
| | Schema | | Container | | Entries ||
| +-----------+ +-----------+ +---------+|
| |
+------------------------------------------------------------------+
Provisioning Models
Synchronous (Simple)
Use When:
- Pool model (shared resources)
- Fast provisioning (<5 seconds)
- Low complexity setup
Flow:
Signup -> Validate -> Create Tenant Record -> Apply Defaults -> Ready
Asynchronous (Complex)
Use When:
- Silo model (dedicated resources)
- Long-running provisioning (>30 seconds)
- Multiple external dependencies
Flow:
Signup -> Validate -> Queue Job -> [Background Process] -> Ready
↓
Return "Provisioning..."
↓
Poll/Webhook for completion
Provisioning Steps
Step Catalog
Common Provisioning Steps:
+------------------------------------------------------------------+
| Step | Model | Duration | Rollback |
+-------------------------+------------+-------------+------------+
| Create tenant record | All | <1s | Delete |
| Create admin user | All | <1s | Delete |
| Apply default settings | All | <1s | N/A |
| Seed sample data | All | 1-5s | Delete |
| Create database schema | Bridge | 5-30s | Drop |
| Create database | Silo | 30s-2min | Drop |
| Create storage account | Silo | 30s-1min | Delete |
| Configure DNS | Silo | 1-5min | Remove |
| Provision SSL cert | Silo | 1-5min | Revoke |
+-------------------------+------------+-------------+------------+
Provisioning Workflow
public sealed class TenantProvisioningWorkflow(
IDbContext db,
ITenantSettingsService settings,
ISeedDataService seedData,
IStorageProvisioner storage,
ILogger<TenantProvisioningWorkflow> logger)
{
public async Task<ProvisioningResult> ProvisionAsync(
TenantProvisioningRequest request,
CancellationToken ct)
{
var steps = new List<ProvisioningStepResult>();
Guid? tenantId = null;
try
{
// Step 1: Create tenant record
tenantId = await CreateTenantRecordAsync(request, ct);
steps.Add(ProvisioningStepResult.Success("create_tenant"));
// Step 2: Create admin user
await CreateAdminUserAsync(tenantId.Value, request.AdminEmail, ct);
steps.Add(ProvisioningStepResult.Success("create_admin"));
// Step 3: Apply default settings
await settings.ApplyDefaultsAsync(tenantId.Value, request.Plan, ct);
steps.Add(ProvisioningStepResult.Success("apply_settings"));
// Step 4: Seed sample data (if requested)
if (request.IncludeSampleData)
{
await seedData.SeedAsync(tenantId.Value, ct);
steps.Add(ProvisioningStepResult.Success("seed_data"));
}
// Step 5: Provision storage (if silo model)
if (request.IsolationLevel == IsolationLevel.Silo)
{
await storage.ProvisionAsync(tenantId.Value, ct);
steps.Add(ProvisioningStepResult.Success("provision_storage"));
}
// Mark tenant as ready
await MarkTenantReadyAsync(tenantId.Value, ct);
return ProvisioningResult.Success(tenantId.Value, steps);
}
catch (Exception ex)
{
logger.LogError(ex, "Provisioning failed for {Email}", request.AdminEmail);
// Rollback completed steps
if (tenantId.HasValue)
{
await RollbackAsync(tenantId.Value, steps, ct);
}
return ProvisioningResult.Failed(ex.Message, steps);
}
}
private async Task RollbackAsync(
Guid tenantId,
List<ProvisioningStepResult> completedSteps,
CancellationToken ct)
{
// Rollback in reverse order
foreach (var step in completedSteps.AsEnumerable().Reverse())
{
try
{
await RollbackStepAsync(tenantId, step.StepId, ct);
}
catch (Exception ex)
{
logger.LogError(ex, "Rollback failed for step {Step}", step.StepId);
// Continue rollback despite failures
}
}
}
}
Pool Model Provisioning
Fast Provisioning
public sealed class PoolTenantProvisioner(IDbContext db) : ITenantProvisioner
{
public async Task<Guid> ProvisionAsync(
TenantProvisioningRequest request,
CancellationToken ct)
{
// All in single transaction - fast and atomic
await using var transaction = await db.Database.BeginTransactionAsync(ct);
try
{
// Create tenant
var tenant = new Tenant
{
Id = Guid.NewGuid(),
Name = request.CompanyName,
Subdomain = request.Subdomain,
Plan = request.Plan,
Status = TenantStatus.Active,
CreatedAt = DateTimeOffset.UtcNow
};
db.Tenants.Add(tenant);
// Create admin user
var adminUser = new User
{
Id = Guid.NewGuid(),
TenantId = tenant.Id,
Email = request.AdminEmail,
Role = UserRole.Admin,
CreatedAt = DateTimeOffset.UtcNow
};
db.Users.Add(adminUser);
// Apply default settings
var defaultSettings = GetDefaultSettings(request.Plan);
foreach (var setting in defaultSettings)
{
db.TenantSettings.Add(new TenantSetting
{
TenantId = tenant.Id,
Key = setting.Key,
Value = setting.Value
});
}
await db.SaveChangesAsync(ct);
await transaction.CommitAsync(ct);
return tenant.Id;
}
catch
{
await transaction.RollbackAsync(ct);
throw;
}
}
}
Silo Model Provisioning
Async Provisioning with Saga
public sealed class SiloProvisioningSaga
{
private readonly List<IProvisioningStep> _steps;
public SiloProvisioningSaga(IServiceProvider services)
{
_steps =
[
services.GetRequiredService<CreateDatabaseStep>(),
services.GetRequiredService<ApplyMigrationsStep>(),
services.GetRequiredService<CreateStorageContainerStep>(),
services.GetRequiredService<SeedDataStep>(),
services.GetRequiredService<ConfigureDnsStep>(),
services.GetRequiredService<ProvisionSslStep>()
];
}
public async Task<SagaResult> ExecuteAsync(
TenantProvisioningRequest request,
CancellationToken ct)
{
var context = new ProvisioningContext(request);
var completedSteps = new Stack<IProvisioningStep>();
try
{
foreach (var step in _steps)
{
await step.ExecuteAsync(context, ct);
completedSteps.Push(step);
// Report progress
await ReportProgressAsync(context, step, ct);
}
return SagaResult.Success(context.TenantId);
}
catch (Exception ex)
{
// Compensate in reverse order
while (completedSteps.TryPop(out var step))
{
try
{
await step.CompensateAsync(context, ct);
}
catch (Exception compensateEx)
{
// Log but continue compensation
context.Errors.Add(compensateEx);
}
}
return SagaResult.Failed(ex, context.Errors);
}
}
}
// Example step
public sealed class CreateDatabaseStep : IProvisioningStep
{
public string StepId => "create_database";
public async Task ExecuteAsync(ProvisioningContext context, CancellationToken ct)
{
var dbName = $"tenant_{context.TenantId}";
await _dbProvisioner.CreateDatabaseAsync(dbName, ct);
context.Data["database_name"] = dbName;
}
public async Task CompensateAsync(ProvisioningContext context, CancellationToken ct)
{
if (context.Data.TryGetValue("database_name", out var dbName))
{
await _dbProvisioner.DropDatabaseAsync((string)dbName, ct);
}
}
}
Progress Tracking
Provisioning Status
public sealed record ProvisioningStatus
{
public required Guid TenantId { get; init; }
public required ProvisioningState State { get; init; }
public required List<StepStatus> Steps { get; init; }
public decimal ProgressPercent => Steps.Count > 0
? (decimal)Steps.Count(s => s.IsCompleted) / Steps.Count * 100
: 0;
public string? CurrentStep { get; init; }
public string? ErrorMessage { get; init; }
public DateTimeOffset StartedAt { get; init; }
public DateTimeOffset? CompletedAt { get; init; }
}
public enum ProvisioningState
{
Pending,
InProgress,
Completed,
Failed,
RolledBack
}
public sealed record StepStatus
{
public required string StepId { get; init; }
public required string DisplayName { get; init; }
public required bool IsCompleted { get; init; }
public required bool IsFailed { get; init; }
public string? ErrorMessage { get; init; }
}
Progress API
[ApiController]
[Route("api/provisioning")]
public class ProvisioningController : ControllerBase
{
[HttpGet("{tenantId}/status")]
public async Task<ActionResult<ProvisioningStatus>> GetStatus(Guid tenantId)
{
var status = await _provisioningService.GetStatusAsync(tenantId);
return Ok(status);
}
[HttpGet("{tenantId}/status/stream")]
public async Task StreamStatus(Guid tenantId, CancellationToken ct)
{
Response.ContentType = "text/event-stream";
await foreach (var update in _provisioningService.StreamUpdatesAsync(tenantId, ct))
{
await Response.WriteAsync($"data: {JsonSerializer.Serialize(update)}\n\n", ct);
await Response.Body.FlushAsync(ct);
}
}
}
Seed Data
Sample Data Seeding
public sealed class SeedDataService(IDbContext db) : ISeedDataService
{
public async Task SeedAsync(Guid tenantId, CancellationToken ct)
{
// Seed sample project
var project = new Project
{
Id = Guid.NewGuid(),
TenantId = tenantId,
Name = "My First Project",
Description = "Welcome! This is a sample project to help you get started.",
CreatedAt = DateTimeOffset.UtcNow
};
db.Projects.Add(project);
// Seed sample tasks
var tasks = new[]
{
new ProjectTask
{
Id = Guid.NewGuid(),
ProjectId = project.Id,
TenantId = tenantId,
Title = "Explore the dashboard",
Description = "Take a tour of the main features",
Status = TaskStatus.Todo
},
new ProjectTask
{
Id = Guid.NewGuid(),
ProjectId = project.Id,
TenantId = tenantId,
Title = "Invite your team",
Description = "Add team members to collaborate",
Status = TaskStatus.Todo
},
new ProjectTask
{
Id = Guid.NewGuid(),
ProjectId = project.Id,
TenantId = tenantId,
Title = "Connect your tools",
Description = "Set up integrations with your existing tools",
Status = TaskStatus.Todo
}
};
db.Tasks.AddRange(tasks);
await db.SaveChangesAsync(ct);
}
}
Validation
Pre-Provisioning Validation
public sealed class ProvisioningValidator
{
public async Task<ValidationResult> ValidateAsync(
TenantProvisioningRequest request,
CancellationToken ct)
{
var errors = new List<string>();
// Validate subdomain
if (!IsValidSubdomain(request.Subdomain))
errors.Add("Invalid subdomain format");
if (await IsSubdomainTakenAsync(request.Subdomain, ct))
errors.Add("Subdomain already in use");
// Validate email
if (!IsValidEmail(request.AdminEmail))
errors.Add("Invalid email format");
if (await IsEmailRegisteredAsync(request.AdminEmail, ct))
errors.Add("Email already registered");
// Validate plan
if (!IsValidPlan(request.Plan))
errors.Add("Invalid plan selected");
return errors.Count > 0
? ValidationResult.Failed(errors)
: ValidationResult.Success();
}
}
Best Practices
Provisioning Best Practices:
+------------------------------------------------------------------+
| Practice | Benefit |
+-----------------------------+------------------------------------+
| Idempotent steps | Safe retries |
| Saga with compensation | Clean rollback on failure |
| Progress reporting | User confidence |
| Async for slow operations | Don't block signup |
| Validate before provision | Fail fast |
| Seed sample data | Faster time to value |
| Health check before ready | Ensure usability |
+-----------------------------+------------------------------------+
Anti-Patterns
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Sync silo provisioning | Blocks signup for minutes | Async with progress UI |
| No rollback on failure | Orphaned resources | Saga pattern |
| No progress feedback | User thinks it's broken | Real-time status updates |
| Hard-coded seed data | Stale samples | Configurable templates |
| Skip validation | Errors mid-provision | Validate upfront |
References
Load for detailed implementation:
references/provisioning-workflows.md- Workflow patternsreferences/saga-patterns.md- Compensation handling
Related Skills
tenancy-models- Pool/Silo/Bridge model selectiontenant-lifecycle- State management after provisioningdatabase-isolation- Database provisioning patterns
MCP Research
For current provisioning patterns:
perplexity: "SaaS tenant provisioning 2024" "saga pattern compensation"
microsoft-learn: "Azure resource provisioning" "Durable Functions orchestration"