Arquitetura SaaS

6 de abril de 2026 · 11 min de leitura

Multi-tenancy no SaaS: o que é, por que importa e como implementar em .NET Core

Se você está construindo um SaaS B2B, multi-tenancy não é opcional — é a decisão arquitetural que vai definir se o seu produto consegue crescer de 10 para 10.000 clientes sem reescrever tudo.

O problema é que a maioria dos founders técnicos aprende isso da pior forma: depois que o produto está em produção com uma arquitetura que não suporta isolamento real de dados.

Neste artigo você vai entender o que é multi-tenancy de verdade, quais são as estratégias disponíveis, quando usar cada uma, e como implementar a abordagem certa no .NET Core desde o primeiro commit.


O que é multi-tenancy

Multi-tenancy é a capacidade de um único sistema atender múltiplos clientes (tenants) de forma isolada — onde cada cliente acessa apenas os seus próprios dados, mesmo que todos compartilhem a mesma infraestrutura.

O conceito vem de analogia com um prédio de apartamentos: um único edifício (a infraestrutura), vários apartamentos (os tenants), cada um com sua chave e sem acesso aos vizinhos.

O oposto é uma arquitetura single-tenant, onde cada cliente tem uma instância dedicada da aplicação. É mais simples de implementar, mas extremamente caro de operar e impossível de escalar sem um time de DevOps robusto.

Para um SaaS B2B, multi-tenancy é o padrão correto por três razões fundamentais:

Custo operacional controlado. Um único banco de dados e uma única instância da API atendem N clientes. O custo de infraestrutura cresce de forma sublinear em relação ao número de tenants.

Deploy simplificado. Você faz deploy de uma versão, todos os clientes recebem a atualização. Sem coordenar janelas de manutenção por cliente.

Escalabilidade horizontal. Quando precisar escalar, você escala o sistema — não multiplica instâncias por cliente.


As três estratégias de isolamento

Existem três abordagens principais, e a escolha correta depende do seu mercado, requisitos de conformidade e volume de clientes.

1. Database-per-tenant (isolamento total)

Cada tenant tem seu próprio banco de dados. É o modelo de maior isolamento — um problema em um banco não afeta os outros, e é possível fazer backup, restore e migração de dados por cliente de forma independente.

Quando usar: mercados regulados (saúde, financeiro), contratos enterprise com exigência de isolamento contratual, ou quando o cliente precisa de instâncias on-premise.

Trade-off: custo de infraestrutura alto, migrações de schema precisam ser executadas N vezes, complexidade de connection string management.

2. Schema-per-tenant (equilíbrio entre isolamento e custo)

Um único banco de dados PostgreSQL, mas cada tenant tem seu próprio schema. Os dados são isolados logicamente, mas compartilham o mesmo processo de banco.

Quando usar: SaaS B2B com requisitos de isolamento razoáveis, base de clientes de médio porte, quando você quer separação clara sem o custo de múltiplos bancos. É o modelo mais comum para SaaS B2B brasileiro.

Trade-off: migrações ainda precisam ser aplicadas por schema, mas é muito mais gerenciável. PostgreSQL suporta bem esse padrão.

3. Shared database, shared schema (isolamento por coluna)

Um único banco, um único schema, e uma coluna tenant_id em cada tabela. É o modelo mais simples de implementar e o mais barato de operar.

Quando usar: SaaS com muitos tenants pequenos, quando o custo é a principal restrição, ou no MVP antes de ter clareza sobre os requisitos de isolamento.

Risco crítico: um bug no filtro de tenant_id pode vazar dados de um cliente para outro. Precisa de testes robustos e Row Level Security no banco.


Implementando schema-per-tenant em .NET Core

1. Resolvendo o tenant a partir da requisição

public interface ITenantResolver
{
    string? ResolveTenantId(HttpContext context);
}

public class SubdomainTenantResolver : ITenantResolver
{
    public string? ResolveTenantId(HttpContext context)
    {
        var host = context.Request.Host.Host;
        var parts = host.Split('.');
        return parts.Length >= 3 ? parts[0] : null;
    }
}

2. TenantContext como serviço scoped

public class TenantContext
{
    public string TenantId { get; private set; } = string.Empty;
    public string Schema => $"tenant_{TenantId}";

    public void SetTenant(string tenantId)
    {
        if (string.IsNullOrWhiteSpace(tenantId))
            throw new ArgumentException("TenantId não pode ser vazio.");
        TenantId = tenantId;
    }
}

3. Middleware

public class TenantMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ITenantResolver _resolver;

    public TenantMiddleware(RequestDelegate next, ITenantResolver resolver)
    {
        _next = next;
        _resolver = resolver;
    }

    public async Task InvokeAsync(HttpContext context, TenantContext tenantContext)
    {
        var tenantId = _resolver.ResolveTenantId(context);
        if (string.IsNullOrEmpty(tenantId))
        {
            context.Response.StatusCode = 400;
            await context.Response.WriteAsync("Tenant não identificado.");
            return;
        }
        tenantContext.SetTenant(tenantId);
        await _next(context);
    }
}

4. DbContext com schema dinâmico

public class AppDbContext : DbContext
{
    private readonly TenantContext _tenantContext;

    public AppDbContext(DbContextOptions<AppDbContext> options, TenantContext tenantContext)
        : base(options)
    {
        _tenantContext = tenantContext;
    }

    public DbSet<Order> Orders => Set<Order>();
    public DbSet<Customer> Customers => Set<Customer>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.HasDefaultSchema(_tenantContext.Schema);
        base.OnModelCreating(modelBuilder);
    }
}

5. Program.cs

builder.Services.AddScoped<TenantContext>();
builder.Services.AddScoped<ITenantResolver, SubdomainTenantResolver>();
builder.Services.AddDbContext<AppDbContext>((provider, options) =>
{
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default"));
});

var app = builder.Build();
app.UseMiddleware<TenantMiddleware>();

6. Provisionamento de tenant

public class TenantProvisioningService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public async Task ProvisionTenantAsync(string tenantId)
    {
        using var scope = _scopeFactory.CreateScope();
        var tenantContext = scope.ServiceProvider.GetRequiredService<TenantContext>();
        tenantContext.SetTenant(tenantId);
        var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        await dbContext.Database.MigrateAsync();
    }
}

Migrações com múltiplos schemas

public async Task RunMigrationsForAllTenantsAsync()
{
    var tenants = await _tenantRepo.GetAllActiveTenantsAsync();
    foreach (var tenant in tenants)
    {
        using var scope = _scopeFactory.CreateScope();
        var tenantContext = scope.ServiceProvider.GetRequiredService<TenantContext>();
        tenantContext.SetTenant(tenant.Id);
        var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var pendingMigrations = await dbContext.Database.GetPendingMigrationsAsync();
        if (pendingMigrations.Any())
            await dbContext.Database.MigrateAsync();
    }
}

Conclusão

Multi-tenancy é a fundação do seu SaaS, não uma feature. Para a maioria dos SaaS B2B brasileiros em estágio inicial, schema-per-tenant com PostgreSQL e .NET Core é o ponto de equilíbrio ideal: isolamento real, custo controlado, e base sólida para crescer.

CTA: Se você está no estágio de definir a arquitetura do seu SaaS ou quer validar as decisões que já foram tomadas, a Logik Digital faz isso como parte do processo de desenvolvimento — do MVP ao scale-up. Agendar uma consultoria gratuita →

Tags: schema-per-tenant postgresql, multi-tenant entity framework, saas b2b arquitetura, isolamento de dados saas