Skip to content

Multi-Tenancy

Tayra supports multi-tenant key isolation out of the box. When enabled, each tenant's encryption keys are stored with a tenant-specific prefix, ensuring complete cryptographic separation between tenants. Tenant A can never access or decrypt Tenant B's data, even though they share the same physical key store.

How It Works

Multi-tenancy in Tayra is implemented as a decorator pattern. The TenantAwareKeyStore wraps any existing IKeyStore registration and transparently prefixes all key operations with the current tenant ID:

Logical key ID:  patient-abc123
Stored key ID:   tenant-a:patient-abc123

When no tenant context is set (the tenant provider returns null), key operations pass through unchanged. This makes multi-tenancy opt-in and backward-compatible with single-tenant deployments.

Setup

Register multi-tenancy after your key store registration. You must provide an ITenantProvider implementation that resolves the current tenant:

cs
var services = new ServiceCollection();
services.AddTayra(opts => opts.LicenseKey = licenseKey);

// Add multi-tenancy with a custom tenant provider
services.AddTayraMultiTenancy<HttpHeaderTenantProvider>(options =>
{
    options.TenantSeparator = ":";
});
anchor

The AddTayraMultiTenancy<T>() method:

  1. Registers your ITenantProvider implementation as a singleton
  2. Removes the existing IKeyStore registration
  3. Wraps it with a TenantAwareKeyStore decorator
  4. Re-registers the decorated store as the IKeyStore service

Register Key Store First

You must register Tayra via AddTayra() (which defaults to the built-in InMemoryKeyStore, or chain a key store like .UseVaultKeyStore()) before calling AddTayraMultiTenancy(). The extension method decorates the existing IKeyStore registration, so it throws InvalidOperationException if no IKeyStore is found.

Configuration Options

The TayraMultiTenancyOptions class controls the key prefixing behavior:

cs
var mtOptions = new TayraMultiTenancyOptions
{
    // Separator between tenant ID and key ID (default: ":")
    // With tenant "tenant-a" and key "patient-abc123",
    // the actual key stored is "tenant-a:patient-abc123"
    TenantSeparator = ":",
};
anchor
PropertyDefaultDescription
TenantSeparator":"The character(s) inserted between the tenant ID and the original key ID

With the default separator, a key ID patient-abc123 for tenant acme becomes acme:patient-abc123 in the underlying key store.

Implementing ITenantProvider

The ITenantProvider interface has a single method that returns the current tenant ID:

cs
/// <summary>
/// Example tenant provider that resolves the tenant ID from
/// an ambient context. In a real application, this would read
/// from HttpContext headers, JWT claims, or a similar source.
/// </summary>
public class HttpHeaderTenantProvider : ITenantProvider
{
    // In production, inject IHttpContextAccessor and read from headers/claims
    private static readonly AsyncLocal<string?> CurrentTenant = new();

    public string? GetCurrentTenantId()
    {
        return CurrentTenant.Value;
    }

    /// <summary>
    /// Sets the current tenant for the async flow. Call this in middleware
    /// or at the start of a request pipeline.
    /// </summary>
    public static void SetTenant(string? tenantId)
    {
        CurrentTenant.Value = tenantId;
    }
}
anchor

In real applications, you would typically resolve the tenant from:

  • HTTP request headers (e.g., X-Tenant-Id)
  • JWT claims (e.g., a tenant_id claim)
  • Subdomain (e.g., acme.yourapp.com)
  • Route parameters (e.g., /api/{tenantId}/patients)

ASP.NET Core Integration

For ASP.NET Core applications, inject IHttpContextAccessor into your tenant provider to read headers or claims from the current request. Register it with services.AddHttpContextAccessor().

Null Tenant Passthrough

When ITenantProvider.GetCurrentTenantId() returns null, the TenantAwareKeyStore passes all operations through to the inner key store without any prefixing. This is useful for:

  • Background jobs that run outside a tenant context
  • System-level keys that are shared across tenants
  • Migration scenarios where you need to access unprefixed keys

Key Isolation Guarantees

The TenantAwareKeyStore provides the following guarantees:

OperationBehavior
StoreAsyncKey is stored with {tenantId}:{keyId}
GetAsyncOnly retrieves keys prefixed with the current tenant
DeleteAsyncOnly deletes keys prefixed with the current tenant
ExistsAsyncOnly checks keys prefixed with the current tenant
DeleteByPrefixAsyncPrefix is scoped to the current tenant
ListKeyIdsAsyncReturns only the current tenant's keys, with the tenant prefix stripped

Production Deployment

In production multi-tenant deployments, always ensure your ITenantProvider returns a non-null value for tenant-scoped requests. A null tenant in a multi-tenant context could lead to keys being stored without isolation, potentially accessible from other tenant contexts.

See Also