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