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 →