Skip to content

Multi-Tenancy

GrydAuth provides comprehensive multi-tenancy support for SaaS applications, including tenant isolation, federation, and cross-tenant access patterns.

Multi-Tenancy Models

GrydAuth supports multiple tenancy models:

ModelDescriptionUse Case
Database per TenantSeparate database for each tenantMaximum isolation
Schema per TenantSeparate schema in shared databaseGood isolation, easier management
Shared DatabaseAll tenants in one database with TenantId columnCost-effective, simple

Quick Setup

1. Enable Multi-Tenancy

Configure in appsettings.json:

json
{
  "MultiTenancy": {
    "IsEnabled": true
  }
}

2. Configure GrydAuth

csharp
// Program.cs
builder.Services.AddGrydAuth(builder.Configuration);

// Middleware pipeline (order matters!)
app.UseAuthentication();
app.UseGrydAuth();  // Handles tenant resolution from JWT
app.UseAuthorization();

3. Apply Tenant Filter to Entities

csharp
using Gryd.Domain.Abstractions;

public class Product : BaseEntity<Guid>, IHasTenant
{
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    
    // Multi-tenancy
    public Guid TenantId { get; set; }
}

Tenant Resolution Strategies

GrydAuth primarily uses Claim-Based Resolution from JWT tokens. The tenant_id claim is automatically extracted from the authenticated token.

Header-Based Resolution (X-App-Id)

For application-specific routing, GrydAuth uses the X-App-Id header:

http
POST /api/auth/login
X-App-Id: my-frontend-app
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "Password123!"
}

Claim-Based Resolution (Primary)

Tenant ID is extracted from the tenant_id claim in the JWT token automatically by UseGrydAuth() middleware.

json
{
  "sub": "user-id",
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "permissions": ["read:products"]
}

Smart Federation

GrydAuth implements Smart Federation for multi-tenant authentication:

  1. Login - Returns Global Token (if user has multiple tenants)
  2. Switch Tenant - Exchange Global Token for Tenant Token
  3. Auto-Switch - If user has only one tenant, automatically issues Tenant Token
http
POST /api/auth/switch-tenant
Authorization: Bearer {global-or-tenant-token}
Content-Type: application/json

{
  "tenantId": "550e8400-e29b-41d4-a716-446655440002"
}

Tenant Management

Creating Tenants

http
POST /api/tenants
Authorization: Bearer {super-admin-token}
Content-Type: application/json

{
  "name": "Acme Corporation",
  "identifier": "acme",
  "domain": "acme.yourapp.com",
  "settings": {
    "timezone": "America/New_York",
    "language": "en-US",
    "features": ["advanced-reports", "api-access"]
  }
}

Tenant Entity

csharp
public class Tenant : AggregateRoot<Guid>
{
    public string Name { get; private set; } = string.Empty;
    public string Identifier { get; private set; } = string.Empty;
    public string? Domain { get; private set; }
    public bool IsActive { get; private set; } = true;
    public DateTime CreatedAt { get; private set; }
    
    // Settings stored as JSON
    public TenantSettings Settings { get; private set; } = new();
    
    // Navigation
    public ICollection<UserTenant> UserTenants { get; private set; } = new List<UserTenant>();
}

User-Tenant Relationship

Assigning Users to Tenants

http
POST /api/tenants/{tenantId}/users
Authorization: Bearer {admin-token}
Content-Type: application/json

{
  "userId": "550e8400-e29b-41d4-a716-446655440001",
  "roles": ["Manager", "User"]
}

User Belonging to Multiple Tenants

csharp
public class UserTenant : BaseEntity<Guid>
{
    public Guid UserId { get; set; }
    public User User { get; set; } = null!;
    
    public Guid TenantId { get; set; }
    public Tenant Tenant { get; set; } = null!;
    
    // Roles within this tenant
    public ICollection<Role> Roles { get; set; } = new List<Role>();
    
    public bool IsDefault { get; set; }
    public DateTime JoinedAt { get; set; }
}

Tenant Isolation

Automatic Query Filtering

GrydAuth automatically filters queries by tenant:

csharp
public class ProductRepository : RepositoryBase<Product>
{
    protected override IQueryable<Product> ApplyFilters(IQueryable<Product> query)
    {
        // Automatic tenant filter is applied by GrydAuth
        // You don't need to add TenantId filter manually
        return base.ApplyFilters(query);
    }
}

Configuring Tenant Filter

csharp
public class AppDbContext : GrydAuthDbContext
{
    private readonly ITenantContext _tenantContext;
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        
        // Apply tenant filter to all entities implementing IHasTenant
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            if (typeof(IHasTenant).IsAssignableFrom(entityType.ClrType))
            {
                modelBuilder.Entity(entityType.ClrType)
                    .AddQueryFilter<IHasTenant>(e => e.TenantId == _tenantContext.TenantId);
            }
        }
    }
}

Bypassing Tenant Filter

For administrative operations:

csharp
public class TenantService
{
    private readonly AppDbContext _context;
    
    public async Task<List<Product>> GetAllProductsAcrossTenantsAsync()
    {
        // Bypass tenant filter for cross-tenant queries
        return await _context.Products
            .IgnoreQueryFilters()
            .ToListAsync();
    }
}

Tenant Federation

Smart Federation

Users can belong to multiple tenants with different roles:

csharp
// User logs in and selects tenant
var loginResult = await _authService.LoginAsync(new LoginRequest
{
    Email = "user@example.com",
    Password = "password",
    TenantId = selectedTenantId // User selects which tenant to access
});

Switching Tenants

http
POST /api/auth/switch-tenant
Authorization: Bearer {current-token}
Content-Type: application/json

{
  "tenantId": "550e8400-e29b-41d4-a716-446655440002"
}

Response:

json
{
  "isSuccess": true,
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "dGhpcyBpcyBhIHJlZnJlc2g...",
    "expiresAt": "2026-01-29T14:00:00Z",
    "isGlobal": false,
    "tokenType": "Tenant",
    "currentTenant": {
      "id": "550e8400-e29b-41d4-a716-446655440002",
      "name": "Another Corp",
      "isDefault": false
    }
  }
}

Global Tokens

GrydAuth uses Global Tokens for multi-tenant authentication:

  • Global Token: 2 minutes TTL, scope tenant-selector-only
  • Tenant Token: 60 minutes TTL, full access within tenant

When a user with multiple tenants logs in without specifying a preferredTenantId:

  1. A Global Token is issued
  2. User must call POST /api/auth/switch-tenant to select a tenant
  3. A full Tenant Token is returned

Tenant Context

Accessing Current Tenant

csharp
public class ProductService
{
    private readonly ITenantContext _tenantContext;
    private readonly ICurrentUserService _currentUser;
    
    public async Task<Product> CreateAsync(CreateProductRequest request)
    {
        // Get current tenant ID
        var tenantId = _tenantContext.TenantId;
        
        // Or from current user
        var userTenantId = _currentUser.TenantId;
        
        var product = new Product
        {
            Name = request.Name,
            TenantId = tenantId!.Value
        };
        
        return product;
    }
}

ITenantContext Properties

PropertyTypeDescription
TenantIdGuid?Current tenant ID
TenantTenant?Full tenant entity (lazy loaded)
IsResolvedboolWhether tenant was successfully resolved
ResolutionMethodstringHow tenant was resolved (Header, Subdomain, etc.)

Tenant-Specific Configuration

Per-Tenant Settings

csharp
public class TenantSettings
{
    public string Timezone { get; set; } = "UTC";
    public string Language { get; set; } = "en-US";
    public string Currency { get; set; } = "USD";
    public HashSet<string> EnabledFeatures { get; set; } = new();
    public Dictionary<string, object> CustomSettings { get; set; } = new();
}

// Usage
var tenant = await _tenantRepository.GetByIdAsync(tenantId);
var timezone = tenant.Settings.Timezone;

Feature Flags per Tenant

csharp
public class FeatureService
{
    private readonly ITenantContext _tenantContext;
    
    public bool IsFeatureEnabled(string featureName)
    {
        return _tenantContext.Tenant?.Settings.EnabledFeatures.Contains(featureName) ?? false;
    }
}

// Usage in controller
if (_featureService.IsFeatureEnabled("advanced-reports"))
{
    return await GenerateAdvancedReportAsync();
}
else
{
    return BadRequest("This feature is not available for your tenant.");
}

Database Strategies

All tenants in one database with TenantId column:

csharp
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection")));

Database per Tenant

csharp
builder.Services.AddDbContext<AppDbContext>((serviceProvider, options) =>
{
    var tenantContext = serviceProvider.GetRequiredService<ITenantContext>();
    var connectionString = GetTenantConnectionString(tenantContext.TenantId);
    options.UseNpgsql(connectionString);
});

private string GetTenantConnectionString(Guid? tenantId)
{
    if (tenantId == null)
        return Configuration.GetConnectionString("MasterConnection")!;
    
    return Configuration.GetConnectionString($"Tenant_{tenantId}")
        ?? Configuration.GetConnectionString("DefaultConnection")!;
}

Best Practices

  1. Always Use IHasTenant - Mark all tenant-specific entities
  2. Test Tenant Isolation - Write tests to verify data isolation
  3. Audit Cross-Tenant Access - Log when admins bypass tenant filters
  4. Use Tenant Middleware Early - Resolve tenant before authentication
  5. Cache Tenant Data - Tenant resolution can be performance-critical
  6. Handle Missing Tenant - Define behavior when tenant cannot be resolved

Released under the MIT License.