A decisão arquitetural que define seu SaaS
Antes do primeiro cliente, você precisa decidir como separa os dados dele dos outros. Essa escolha cascateia em tudo: custo de infra, complexidade operacional, ciclo de release, compliance, vendabilidade enterprise. Errar aqui não mata — só obriga você a gastar 6 meses reescrevendo depois.
O AWS Well-Architected Framework — SaaS Lens codificou três padrões: Pool, Silo e Bridge (que a gente chama de Hybrid). Cada um é uma decisão sobre o que compartilhar e o que isolar.
Documentação canônica: . Vale ler mesmo se você não está na AWS — os princípios valem em qualquer cloud.
Os 3 modelos lado a lado
| Dimensão | Pool | Bridge | Silo |
|---|---|---|---|
| Custo /tenant | Muito baixo (1/N) | Médio | Alto (linear) |
| Time-to-onboard | Segundos (INSERT) | Segundos a minutos | Minutos a horas (provisiona DB) |
| Isolamento | Lógico (RLS, app) | Misto | Físico |
| Noisy neighbor | Alto | Médio | Zero |
| Compliance enterprise | Difícil (precisa provar RLS) | Médio | Trivial |
| Deploy | 1 release atinge todos | 1 release + per-tenant migrations | N releases ou auto |
| Backup/restore por tenant | Difícil (extract com tenant_id) | Médio | Trivial (pg_dump do DB) |
| DR cross-region | Replicação global | Misto | Replicação por tenant |
| Schema migration | Atômica | Atômica no pool + N nos silos | N execuções |
| Operacionalmente | Simples | Complexo | Trabalhoso em escala |
Pool: o modelo padrão para começar
O modelo Pool é o "Postgres com tenant_id em todas as tabelas". Toda query tem um filtro WHERE tenant_id = ?. Isolamento é feito por defesa em camadas: app (middleware injeta filtro) + DB (RLS força a regra).
-- 1. Toda tabela tem tenant_id (UUID, com FK pra tenants)
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
plan TEXT NOT NULL DEFAULT 'pro',
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
total NUMERIC(12,2),
created_at TIMESTAMPTZ DEFAULT now()
);
-- 2. Index composto começando por tenant_id (planner usa como filtro inicial)
CREATE INDEX orders_tenant_created_idx ON orders (tenant_id, created_at DESC);
-- 3. Habilita RLS na tabela
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY; -- pega até o owner
-- 4. Policy: cada SELECT/INSERT/UPDATE/DELETE filtra pelo session setting
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant_id', true)::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant_id', true)::uuid);// middleware/tenant.ts
import { db } from '@/lib/db';
export async function withTenant<T>(
tenantId: string,
fn: () => Promise<T>,
): Promise<T> {
return db.transaction(async (tx) => {
// SET LOCAL escopa o setting à transação — limpa no commit/rollback
await tx.execute(`SET LOCAL app.tenant_id = '${tenantId}'`);
return fn();
});
}
// uso em route handler
export async function GET(req: Request) {
const tenantId = await getTenantFromJWT(req);
return withTenant(tenantId, async () => {
const orders = await db.select().from(orders); // RLS filtra automaticamente
return Response.json(orders);
});
}Defesa em camadas, não única. Aplique tenant_id na ORM/query manualmente (não confie só no RLS). RLS é a rede de segurança contra o bug do dev que esqueceu. Se algum dia você desabilitar RLS para uma migration de massa, a app continua segura porque o WHERE explícito ainda está lá.
Connection pooling e tenant context
Em pool, um problema comum: você usa PgBouncer (transaction mode) e os settings SET LOCAL ficam confusos. Soluções:
Silo: quando enterprise exige (e paga por) isolamento real
Silo é "1 stack por tenant". Pode ser:
📋 Cliente Fortune 500 exige 'banco de dados dedicado' como condição para fechar contrato de $300k/ano
ARR $300k justifica $300-500/mês de DB dedicado. Mesma app code-base (deploy continua atômico). Cliente pode pedir audit do isolamento e você mostra: 'cluster RDS Aurora dedicado, KMS key específica, backup separado'. Compliance: troca de auditor satisfeito em horas.
Alt: Silo pesado (conta AWS dedicada) —
Alt: Overkill para 1 cliente, vira pesadelo operacional. Vale quando há 5+ clientes enterprise. —
Alt: Pool + RLS —
Alt: Cliente pode recusar audit. Você perde o contrato. Não vale brigar. —
Bridge / Hybrid: o que SaaS de verdade fazem
Quase nenhum SaaS bem-sucedido é 100% pool ou 100% silo. O modelo Bridge (Hybrid) é:
Pragmatismo: Linear, Notion, Vercel — todos começaram pool e introduziram silo só para top 5-10% dos clientes que pagavam $50k+/ano e exigiam isolation. Bridge permite isso sem reescrever a app.
Citus: quando o Postgres pool não dá mais
Antes de Citus, escale vertical. Postgres em db.r6i.16xlarge (64 vCPU, 512GB RAM) aguenta absurdamente. Plain.com publicou em 2024 que rodam milhões de tenants em 1 cluster vanilla Postgres com tuning.
Quando o vertical não dá (tipicamente >1-5TB ou >10k QPS sustentado), Citus entra:
-- Habilita extensão Citus
CREATE EXTENSION citus;
-- Marca tabela como distribuída por tenant_id
SELECT create_distributed_table('orders', 'tenant_id');
SELECT create_distributed_table('events', 'tenant_id');
-- Tabelas de referência (pequenas, replicadas em todos os shards)
SELECT create_reference_table('countries');
-- Query típica: por tenant_id → vai pra 1 shard só (single-node performance)
SELECT * FROM orders WHERE tenant_id = '...' AND created_at > now() - interval '7 days';
-- Cross-tenant: scatter-gather paralelizado
SELECT count(*) FROM orders WHERE created_at > now() - interval '1 day';| Cenário | Citus bom? | Por quê |
|---|---|---|
| Muitos tenants pequenos | ✅ Excelente | Distribuídos uniformemente, todas queries por tenant_id = single-shard |
| Poucos tenants enormes | ⚠️ Cuidado | Hot shard — 1 worker sobrecarregado. Force a redistribuição manual ou migre para silo. |
| Analytics cross-tenant | ✅ Bom | Citus parallelize scatter-gather; OLAP workloads se beneficiam. |
| Joins entre tenants | ❌ Ruim | Joins não-colocados = movimento de dados entre shards = lento. Evite design. |
| Real-time / OLTP intenso | 🟡 Médio | Funciona, mas adiciona latência de rede entre coordinator e workers. |
Schema migrations em multi-tenant
Migrations em SaaS pool são triviais — 1 ALTER TABLE atinge tudo. Em silo / bridge, vira orquestração:
Noisy neighbor: o pesadelo do pool em escala
Cliente A faz SELECT * FROM events sem LIMIT, varre 50M linhas, IO do banco satura, todos os outros tenants veem p99 subindo. Esse é o noisy neighbor. Defesas:
Storage (S3/Blob) multi-tenant
Arquivos seguem mesma lógica: pool (1 bucket, prefixo por tenant) vs silo (1 bucket/conta por tenant). Em pool:
// Backend gera pre-signed URL com tenant_id validado
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
export async function getUploadUrl(tenantId: string, filename: string) {
// tenant_id na key — backend SEMPRE controla
const key = `${tenantId}/uploads/${crypto.randomUUID()}-${filename}`;
const cmd = new PutObjectCommand({
Bucket: 'meu-saas-prod',
Key: key,
ContentType: 'application/octet-stream',
});
const url = await getSignedUrl(s3Client, cmd, { expiresIn: 300 });
return { url, key };
}
// Cliente uploada direto para S3 com PUT
// Servidor só sabe da key — armazena em DB ligada ao tenant_id
// Nunca aceita key que o cliente envia (poderia escrever em outro tenant)Erro clássico: aceitar uma do client. Sempre gere a key no backend, prefixada com o da sessão. Caso contrário, cliente A pode escrever em .
Observabilidade multi-tenant
O caminho recomendado para solo SaaS
Para solo SaaS começando agora: Pool com tenant_id + RLS, hospedado em Neon ou Supabase. Vai aguentar até alguns milhares de tenants antes de você precisar pensar em silo. Resista à tentação de over-engineer.
Perguntas reais da trincheira
❓ Tenant_id como UUID ou string slug ('acme')?
❓ Como faço soft delete em multi-tenant?
❓ Posso fazer backup de 1 tenant específico em pool?
❓ Como gerencio limites por plano (storage, MAU) em multi-tenant?
❓ Tenant pediu 'data residency UE' — como atender em pool?
Referências canônicas
Próximo módulo: arquitetura pronta. Agora o cliente precisa entender seu produto em <5 minutos. Onboarding flows: time-to-value, empty states, product tours, activation rate.