Como implementar multi-tenancy em aplicações SaaS
1. Fundamentos do Multi-Tenancy
Multi-tenancy é um padrão arquitetural onde uma única instância de aplicação atende múltiplos clientes (tenants), garantindo isolamento lógico entre eles. Existem três modelos principais de isolamento de dados:
Database-per-tenant: Cada tenant possui seu próprio banco de dados. Oferece o maior isolamento, mas aumenta custos operacionais e complexidade de manutenção.
Schema-per-tenant: Banco de dados único com schemas separados por tenant. Equilíbrio entre isolamento e custo, mas requer gerenciamento cuidadoso de migrações.
Shared database: Todas as tabelas possuem uma coluna tenant_id. Menor custo, mas exige validação rigorosa em todas as consultas para evitar vazamento de dados.
Os trade-offs envolvem custo de infraestrutura, complexidade de backup/restore, performance de queries e facilidade de manutenção. Em SaaS para CRM ou ERP, o schema-per-tenant é comum; para plataformas de e-commerce com muitos tenants pequenos, o shared database é mais econômico.
2. Estratégias de Identificação e Roteamento de Tenants
A identificação do tenant pode ocorrer via subdomínio (tenant1.app.com), domínio personalizado (app.tenant1.com) ou cabeçalho HTTP (X-Tenant-ID). O middleware de resolução no backend extrai essa informação e configura o contexto da requisição.
// Exemplo de middleware em Node.js/Express
const tenantMiddleware = (req, res, next) => {
const tenantId = req.headers['x-tenant-id'] ||
req.subdomains[0] ||
req.query.tenant;
if (!tenantId) {
return res.status(400).json({ error: 'Tenant não identificado' });
}
req.tenant = { id: tenantId };
next();
};
Para evitar consultas repetidas ao banco, utilize cache de sessão com Redis:
// Cache de configurações do tenant
const getTenantConfig = async (tenantId) => {
const cacheKey = `tenant:${tenantId}:config`;
let config = await redis.get(cacheKey);
if (!config) {
config = await db.query('SELECT * FROM tenants WHERE id = $1', [tenantId]);
await redis.setex(cacheKey, 3600, JSON.stringify(config));
}
return JSON.parse(config);
};
3. Modelagem de Dados e Migrações
No modelo shared database, todas as tabelas devem incluir tenant_id como chave estrangeira:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id),
customer_id INTEGER NOT NULL,
total DECIMAL(10,2),
created_at TIMESTAMP DEFAULT NOW(),
FOREIGN KEY (tenant_id, customer_id) REFERENCES customers(tenant_id, id)
);
CREATE INDEX idx_orders_tenant ON orders(tenant_id);
Para migrações parametrizadas por tenant, utilize scripts que aceitam o identificador:
-- migration_003_add_tax_column.sql
DO $$
DECLARE
tenant_cursor CURSOR FOR SELECT id FROM tenants WHERE active = true;
tenant_record RECORD;
BEGIN
FOR tenant_record IN tenant_cursor LOOP
EXECUTE format(
'ALTER TABLE %I.orders ADD COLUMN IF NOT EXISTS tax_rate DECIMAL(5,2) DEFAULT 0',
'tenant_' || tenant_record.id
);
END LOOP;
END $$;
4. Isolamento de Recursos e Performance
Para gerenciar conexões de banco, duas abordagens são comuns:
// Pool segregado por tenant (maior isolamento)
const tenantPools = new Map();
const getTenantPool = (tenantId) => {
if (!tenantPools.has(tenantId)) {
const pool = new Pool({
database: `saas_${tenantId}`,
max: 5
});
tenantPools.set(tenantId, pool);
}
return tenantPools.get(tenantId);
};
// Pool único com filtro (menor custo)
const globalPool = new Pool({ max: 20 });
const queryWithTenant = (text, params, tenantId) => {
return globalPool.query(text, [...params, tenantId]);
};
Para cache, utilize Redis com prefixo de tenant:
const getCachedData = async (tenantId, key) => {
return redis.get(`tenant:${tenantId}:${key}`);
};
const setCachedData = async (tenantId, key, value, ttl = 300) => {
return redis.setex(`tenant:${tenantId}:${key}`, ttl, JSON.stringify(value));
};
Implemente rate limiting por tenant:
const rateLimiter = new RateLimiter({
store: new RateLimitRedis({ client: redis }),
keyGenerator: (req) => `rate:${req.tenant.id}:${req.ip}`,
points: 100,
duration: 60
});
5. Segurança e Controle de Acesso
Utilize JWT com claim de tenant para autenticação:
const generateToken = (user, tenantId) => {
return jwt.sign(
{
userId: user.id,
tenantId: tenantId,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
};
// Middleware de validação de tenant
const validateTenantAccess = (req, res, next) => {
const tokenTenant = req.user.tenantId;
const requestTenant = req.tenant.id;
if (tokenTenant !== requestTenant) {
return res.status(403).json({ error: 'Acesso negado' });
}
next();
};
Para prevenir vazamento de dados, valide o tenant em todas as consultas:
const getOrdersByTenant = async (tenantId, limit = 10) => {
const result = await db.query(
'SELECT * FROM orders WHERE tenant_id = $1 ORDER BY created_at DESC LIMIT $2',
[tenantId, limit]
);
return result.rows;
};
Para auditoria, implemente logs segregados:
const auditLog = (tenantId, action, details) => {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
tenant: tenantId,
action: action,
details: details,
environment: process.env.NODE_ENV
}));
};
6. Deploy e Operação Multi-Tenant
Para CI/CD, utilize feature flags e deploys graduais:
# Deploy por grupos de tenants
deploy:
stages:
- canary
- progressive
- full
canary:
script: |
TENANTS="tenant_a,tenant_b"
for tenant in $(echo $TENANTS | tr "," "\n"); do
kubectl set image deployment/$tenant app=$IMAGE_TAG
done
progressive:
script: |
kubectl set image deployment/all-tenants app=$IMAGE_TAG
kubectl rollout status deployment/all-tenants --timeout=5m
Para monitoramento por tenant, utilize métricas customizadas:
const metricsMiddleware = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
metrics.histogram('http_request_duration_ms', duration, {
tenant: req.tenant.id,
method: req.method,
path: req.path,
status: res.statusCode
});
});
next();
};
Backup individual por tenant:
# Script de backup por tenant
backup_tenant() {
local tenant_id=$1
local db_name="saas_${tenant_id}"
pg_dump -h $DB_HOST -U $DB_USER \
--format=custom \
--file="/backups/${tenant_id}_$(date +%Y%m%d).dump" \
$db_name
aws s3 cp "/backups/${tenant_id}_$(date +%Y%m%d).dump" \
"s3://saas-backups/${tenant_id}/"
}
7. Estratégias de Migração e Evolução
Para migrar de shared database para schema-per-tenant:
-- Passo 1: Criar schema para cada tenant ativo
DO $$
DECLARE
tenant RECORD;
BEGIN
FOR tenant IN SELECT DISTINCT tenant_id FROM orders LOOP
EXECUTE 'CREATE SCHEMA IF NOT EXISTS tenant_' || tenant.tenant_id;
-- Passo 2: Copiar dados
EXECUTE format(
'CREATE TABLE %I.orders AS SELECT * FROM public.orders WHERE tenant_id = %s',
'tenant_' || tenant.tenant_id,
tenant.tenant_id
);
END LOOP;
END $$;
Para depreciação de tenants inativos, implemente políticas de retenção:
-- Marcar tenant para exclusão
UPDATE tenants SET status = 'deprecated', deprecated_at = NOW()
WHERE last_active_at < NOW() - INTERVAL '6 months';
-- Exportar dados antes da exclusão
COPY (
SELECT * FROM orders WHERE tenant_id IN (
SELECT id FROM tenants WHERE status = 'deprecated'
)
) TO '/tmp/deprecated_tenants_export.csv' CSV HEADER;
A implementação de multi-tenancy exige planejamento cuidadoso desde o início do projeto. Comece com um modelo simples (shared database) e evolua conforme necessário, sempre priorizando o isolamento de dados e a segurança como requisitos não negociáveis.
Referências
- AWS Multi-Tenancy Guide — Documentação oficial da AWS sobre padrões de arquitetura multi-tenant para SaaS
- PostgreSQL Multi-Tenancy with Row-Level Security — Documentação oficial do PostgreSQL sobre políticas de segurança em nível de linha para isolamento de tenants
- Redis Multi-Tenant Caching Strategies — Guia oficial do Redis sobre particionamento e estratégias de cache para múltiplos tenants
- JWT Multi-Tenant Authentication — Documentação da Auth0 sobre implementação de autenticação multi-tenant com JWT
- Docker Multi-Tenant Deployment — Guia oficial do Docker sobre estratégias de deploy para aplicações multi-tenant
- Kubernetes Multi-Tenancy Patterns — Documentação oficial do Kubernetes sobre padrões de segurança e isolamento para múltiplos tenants