Tenant context resolution and propagation patterns for multi-tenant applications. Covers middleware, headers, claims, and distributed tracing.
Implements tenant context resolution and propagation patterns for multi-tenant applications.
/plugin marketplace add melodic-software/claude-code-plugins/plugin install saas-patterns@melodic-softwareThis skill is limited to using the following tools:
Use this skill when:
Patterns for resolving and propagating tenant context throughout multi-tenant applications.
Tenant context must be available everywhere in the application - from web requests to background jobs to microservice calls. This skill covers strategies for establishing, propagating, and accessing tenant context reliably.
Tenant Resolution Methods:
+------------------------------------------------------------------+
| Method | Source | Use Case |
+-----------------+---------------------+--------------------------+
| Subdomain | acme.app.com | SaaS with vanity URLs |
| Custom Domain | app.acme.com | White-label enterprise |
| Header | X-Tenant-Id | API/microservices |
| JWT Claim | tenant_id claim | Authenticated requests |
| Path Segment | /tenants/{id}/... | REST API design |
| Query Parameter | ?tenant_id=... | Admin/support tools |
| Database Lookup | user → tenant | User-first resolution |
+-----------------+---------------------+--------------------------+
+------------------------------------------------------------------+
| Tenant Context Flow |
+------------------------------------------------------------------+
| |
| +---------+ +------------+ +-------------+ |
| | Request |-->| Middleware |-->| Context | |
| | (HTTP) | | Resolver | | Provider | |
| +---------+ +------------+ +-------------+ |
| | | |
| v v |
| +-------------+ +---------------+ |
| | Validation | | AsyncLocal<T> | |
| | (exists?) | | (Thread-safe) | |
| +-------------+ +---------------+ |
| | |
| +-------------------+-------------------+ |
| v v v |
| +---------+ +---------+ +----------+ |
| | Services| | EF Core | | Background| |
| | | | Filters | | Jobs | |
| +---------+ +---------+ +----------+ |
| |
+------------------------------------------------------------------+
public interface ITenantContext
{
Guid TenantId { get; }
string TenantName { get; }
string TenantSlug { get; }
TenantSettings Settings { get; }
bool IsResolved { get; }
}
public interface ITenantContextAccessor
{
ITenantContext? Current { get; set; }
}
public sealed class TenantContextAccessor : ITenantContextAccessor
{
private static readonly AsyncLocal<TenantContextHolder> _current = new();
public ITenantContext? Current
{
get => _current.Value?.Context;
set
{
var holder = _current.Value;
if (holder != null)
{
holder.Context = null;
}
if (value != null)
{
_current.Value = new TenantContextHolder { Context = value };
}
}
}
private sealed class TenantContextHolder
{
public ITenantContext? Context { get; set; }
}
}
public sealed class SubdomainTenantMiddleware(
RequestDelegate next,
ITenantContextAccessor contextAccessor,
ITenantRepository tenants,
ILogger<SubdomainTenantMiddleware> logger)
{
private static readonly string[] ExcludedSubdomains = ["www", "api", "admin"];
public async Task InvokeAsync(HttpContext context)
{
var host = context.Request.Host.Host;
var subdomain = ExtractSubdomain(host);
if (!string.IsNullOrEmpty(subdomain) && !ExcludedSubdomains.Contains(subdomain))
{
var tenant = await tenants.GetBySubdomainAsync(subdomain);
if (tenant != null)
{
contextAccessor.Current = new TenantContext(tenant);
logger.LogDebug("Resolved tenant {TenantId} from subdomain {Subdomain}",
tenant.Id, subdomain);
}
else
{
logger.LogWarning("Unknown subdomain: {Subdomain}", subdomain);
context.Response.StatusCode = 404;
return;
}
}
await next(context);
}
private static string? ExtractSubdomain(string host)
{
var parts = host.Split('.');
return parts.Length >= 3 ? parts[0] : null;
}
}
public sealed class HeaderTenantMiddleware(
RequestDelegate next,
ITenantContextAccessor contextAccessor,
ITenantRepository tenants)
{
private const string TenantHeader = "X-Tenant-Id";
public async Task InvokeAsync(HttpContext context)
{
if (context.Request.Headers.TryGetValue(TenantHeader, out var tenantIdValue))
{
if (Guid.TryParse(tenantIdValue, out var tenantId))
{
var tenant = await tenants.GetByIdAsync(tenantId);
if (tenant != null)
{
contextAccessor.Current = new TenantContext(tenant);
}
}
}
await next(context);
}
}
public sealed class ClaimsTenantMiddleware(
RequestDelegate next,
ITenantContextAccessor contextAccessor,
ITenantRepository tenants)
{
public async Task InvokeAsync(HttpContext context)
{
if (context.User.Identity?.IsAuthenticated == true)
{
var tenantClaim = context.User.FindFirst("tenant_id");
if (tenantClaim != null && Guid.TryParse(tenantClaim.Value, out var tenantId))
{
var tenant = await tenants.GetByIdAsync(tenantId);
if (tenant != null)
{
contextAccessor.Current = new TenantContext(tenant);
}
}
}
await next(context);
}
}
public sealed class AppDbContext(
DbContextOptions<AppDbContext> options,
ITenantContextAccessor tenantContext) : DbContext(options)
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Apply tenant filter to all tenant-scoped entities
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
if (typeof(ITenantScoped).IsAssignableFrom(entityType.ClrType))
{
modelBuilder.Entity(entityType.ClrType)
.AddQueryFilter<ITenantScoped>(e =>
e.TenantId == tenantContext.Current!.TenantId);
}
}
}
public override Task<int> SaveChangesAsync(CancellationToken ct = default)
{
// Auto-set TenantId on new entities
foreach (var entry in ChangeTracker.Entries<ITenantScoped>())
{
if (entry.State == EntityState.Added)
{
entry.Entity.TenantId = tenantContext.Current!.TenantId;
}
}
return base.SaveChangesAsync(ct);
}
}
public sealed class TenantJobFilter(ITenantContextAccessor contextAccessor) : IJobFilter
{
public void OnCreating(CreatingContext context)
{
// Capture tenant context when job is created
if (contextAccessor.Current != null)
{
context.SetJobParameter("TenantId", contextAccessor.Current.TenantId);
}
}
public void OnPerforming(PerformingContext context)
{
// Restore tenant context when job executes
var tenantId = context.GetJobParameter<Guid?>("TenantId");
if (tenantId.HasValue)
{
// Resolve and set tenant context
var tenant = context.ServiceProvider
.GetRequiredService<ITenantRepository>()
.GetByIdAsync(tenantId.Value)
.GetAwaiter().GetResult();
if (tenant != null)
{
contextAccessor.Current = new TenantContext(tenant);
}
}
}
}
public sealed class TenantHeaderHandler(
ITenantContextAccessor tenantContext) : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken ct)
{
if (tenantContext.Current != null)
{
request.Headers.Add("X-Tenant-Id", tenantContext.Current.TenantId.ToString());
}
return base.SendAsync(request, ct);
}
}
// Registration
services.AddHttpClient("downstream")
.AddHttpMessageHandler<TenantHeaderHandler>();
public static class TenantTracing
{
public static Activity? StartTenantActivity(
ITenantContext context,
string operationName)
{
var activity = Activity.Current?.Source.StartActivity(operationName);
if (activity != null && context.IsResolved)
{
activity.SetTag("tenant.id", context.TenantId.ToString());
activity.SetTag("tenant.name", context.TenantName);
}
return activity;
}
}
Context Propagation Best Practices:
+------------------------------------------------------------------+
| Practice | Benefit |
+-----------------------------+------------------------------------+
| AsyncLocal for thread-safe | Works across async/await |
| Early resolution (middleware)| Available everywhere downstream |
| Cached tenant data | Avoid repeated DB lookups |
| Validation in middleware | Fail fast on invalid tenant |
| Include in traces/logs | Debugging multi-tenant issues |
| Propagate to outbound calls | Consistent context in microservices|
+-----------------------------+------------------------------------+
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Static tenant | Not thread-safe | AsyncLocal |
| Late resolution | Missing context in services | Middleware |
| No validation | Invalid tenant access | Middleware validation |
| Missing propagation | Lost context in background jobs | Job filters |
| No logging | Hard to debug tenant issues | Include tenant in logs |
tenancy-models - Overall architecture contextdatabase-isolation - Database-level multi-tenancyaudit-logging - Tenant-aware audit trailsFor current patterns:
perplexity: "multi-tenant context propagation .NET 2024" "AsyncLocal tenant context"
microsoft-learn: "multi-tenant middleware ASP.NET Core" "EF Core query filters"
Use when working with Payload CMS projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.
Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.