Multi-tenancy: estratégias para aplicações SaaS
1. Introdução ao Multi-tenancy em Aplicações SaaS
Multi-tenancy é um padrão arquitetural onde uma única instância de aplicação atende múltiplos clientes (inquilinos ou tenants), garantindo isolamento lógico entre eles. Diferentemente de usuários comuns, que compartilham o mesmo contexto, cada tenant opera em um ambiente isolado com seus próprios dados, configurações e permissões.
Para aplicações SaaS em PHP, o multi-tenancy é fundamental por três razões principais:
- Isolamento de dados: cada tenant acessa exclusivamente seus registros
- Escalabilidade: um único deploy atende milhares de clientes
- Segurança: falhas em um tenant não comprometem os demais
A escolha da estratégia de isolamento impacta diretamente no custo operacional, na complexidade de manutenção e na performance da aplicação.
2. Abordagens de Isolamento de Dados
Banco de dados separado por tenant
Cada tenant possui seu próprio banco de dados. É a abordagem com maior isolamento, ideal para aplicações com requisitos rigorosos de compliance (LGPD, GDPR).
// Exemplo de configuração dinâmica no Laravel
'connections' => [
'tenant' => [
'driver' => 'mysql',
'host' => env('DB_HOST'),
'database' => 'tenant_' . $tenantId,
'username' => env('DB_USERNAME'),
'password' => env('DB_PASSWORD'),
],
];
Vantagens: isolamento total, backup independente, fácil restauração individual
Desvantagens: maior custo de infraestrutura, conexões simultâneas elevadas
Schema separado por tenant
Comum em PostgreSQL, onde cada tenant possui um schema próprio dentro do mesmo banco.
// Configuração para PostgreSQL com schemas
'pgsql' => [
'driver' => 'pgsql',
'host' => env('DB_HOST'),
'database' => 'saas_app',
'schema' => 'tenant_' . $tenantId,
'username' => env('DB_USERNAME'),
'password' => env('DB_PASSWORD'),
];
Banco compartilhado com coluna tenant_id
A abordagem mais simples e econômica, onde todas as tabelas possuem uma coluna tenant_id.
// Migration exemplo
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->onDelete('cascade');
$table->string('product_name');
$table->decimal('amount', 10, 2);
$table->timestamps();
$table->index('tenant_id'); // Índice essencial para performance
});
Risco principal: vazamento de dados entre tenants se o filtro WHERE tenant_id = ? for esquecido em alguma query.
3. Implementação no Laravel: Pacotes e Estrutura
O pacote stancl/tenancy é a solução mais madura para multi-tenancy no ecossistema Laravel.
composer require stancl/tenancy
php artisan tenancy:install
Configuração inicial no config/tenancy.php:
'tenant_model' => App\Models\Tenant::class,
'identification' => [
'driver' => Stancl\Tenancy\Identification\DomainIdentification::class,
],
Para identificar o tenant via subdomínio:
// App\Providers\TenancyServiceProvider.php
public function boot()
{
$this->app->make(Stancl\Tenancy\Middleware\InitializeTenancyByDomain::class);
}
Estrutura de migrations específicas por tenant:
// database/migrations/tenant/2024_01_01_000001_create_tenant_tables.php
class CreateTenantTables extends Migration
{
public function up()
{
Schema::create('tenant_specific_data', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}
}
4. Gerenciamento de Conexões de Banco de Dados
A configuração dinâmica de conexões permite alternar entre bancos de dados sem alterar o código da aplicação.
// App\Services\TenantManager.php
class TenantManager
{
protected array $connections = [];
public function connect(Tenant $tenant): void
{
$connectionName = 'tenant_' . $tenant->id;
if (!isset($this->connections[$connectionName])) {
config(["database.connections.{$connectionName}" => [
'driver' => 'mysql',
'host' => $tenant->db_host ?? config('database.connections.mysql.host'),
'database' => $tenant->database,
'username' => $tenant->db_username,
'password' => decrypt($tenant->db_password),
'charset' => 'utf8mb4',
]]);
$this->connections[$connectionName] = true;
}
DB::purge($connectionName);
DB::setDefaultConnection($connectionName);
}
}
Para cache de conexões, utilize o Redis:
// Cache de configurações de conexão
Cache::store('redis')->remember("tenant.connection.{$tenant->id}", 3600, function () use ($tenant) {
return [
'database' => $tenant->database,
'username' => $tenant->db_username,
'password' => decrypt($tenant->db_password),
];
});
5. Autenticação e Autorização Multi-tenant
O isolamento de sessões é crítico. Cada tenant deve ter seu próprio provedor de autenticação.
// config/auth.php
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'tenants',
],
],
'providers' => [
'tenants' => [
'driver' => 'eloquent',
'model' => App\Models\TenantUser::class,
],
],
Implementação de guard personalizado com escopo de tenant:
// App\Providers\AuthServiceProvider.php
public function boot()
{
Auth::viaRequest('tenant-scoped', function ($request) {
$tenant = tenancy()->tenant;
return TenantUser::where('email', $request->email)
->where('tenant_id', $tenant->id)
->first();
});
}
Políticas de acesso com verificação de tenant:
// App\Policies\OrderPolicy.php
public function view(User $user, Order $order): bool
{
return $user->tenant_id === $order->tenant_id;
}
6. Migrações e Seeds por Tenant
Comandos Artisan personalizados para gerenciar tenants:
// App\Console\Commands\MigrateTenant.php
class MigrateTenant extends Command
{
protected $signature = 'tenancy:migrate {tenant?}';
public function handle()
{
$tenants = $this->argument('tenant')
? Tenant::where('id', $this->argument('tenant'))->get()
: Tenant::all();
foreach ($tenants as $tenant) {
$tenant->run(function () {
$this->call('migrate', [
'--path' => 'database/migrations/tenant',
'--database' => 'tenant',
]);
});
}
}
}
Seeds iniciais por tenant:
php artisan tenancy:seed --tenants=1,2,3 --class=TenantDatabaseSeeder
// database/seeders/TenantDatabaseSeeder.php
class TenantDatabaseSeeder extends Seeder
{
public function run()
{
$this->call([
RolesAndPermissionsSeeder::class,
DefaultSettingsSeeder::class,
]);
}
}
7. Desempenho e Cache em Ambiente Multi-tenant
Cache com chaves prefixadas por tenant evita conflitos:
// Cache prefixado
Cache::store('redis')->tags(['tenant:' . tenancy()->tenant->id])
->put('settings', $settings, 3600);
// Ou com prefixo manual
$cacheKey = 'tenant:' . $tenant->id . ':settings';
Cache::put($cacheKey, $settings, 3600);
Otimização de queries com índices compostos:
Schema::table('orders', function (Blueprint $table) {
$table->index(['tenant_id', 'created_at']); // Índice composto
$table->index(['tenant_id', 'status']);
});
Filas com escopo de tenant:
// App\Jobs\ProcessOrderJob.php
class ProcessOrderJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tenantId;
public function __construct(Order $order)
{
$this->tenantId = $order->tenant_id;
}
public function handle()
{
tenancy()->initialize(Tenant::find($this->tenantId));
// Lógica do job dentro do contexto do tenant
}
}
8. Considerações Finais e Boas Práticas
Testes automatizados com múltiplos tenants:
// tests/Feature/MultiTenantTest.php
class MultiTenantTest extends TestCase
{
use RefreshDatabase;
public function test_tenant_isolation()
{
$tenant1 = Tenant::factory()->create();
$tenant2 = Tenant::factory()->create();
tenancy()->initialize($tenant1);
$order1 = Order::factory()->create();
tenancy()->initialize($tenant2);
$this->assertDatabaseMissing('orders', ['id' => $order1->id]);
}
}
Monitoramento segregado: utilize logs com identificação do tenant:
Log::channel('tenant')->info('Order created', [
'tenant_id' => tenancy()->tenant->id,
'order_id' => $order->id,
]);
Estratégias de backup: implemente backups individuais por tenant utilizando scripts shell ou serviços como AWS RDS snapshots com tags de tenant.
Boas práticas finais:
- Sempre valide o tenant atual em middlewares globais
- Utilize índices em todas as colunas tenant_id
- Implemente rate limiting por tenant
- Monitore o uso de recursos (CPU, memória) por tenant
- Documente claramente as estratégias de isolamento escolhidas
O multi-tenancy bem implementado em PHP transforma sua aplicação SaaS em uma plataforma escalável, segura e de fácil manutenção, permitindo atender desde pequenos clientes até grandes corporações com uma única base de código.
Referências
- Documentação oficial do stancl/tenancy — Guia completo do pacote mais utilizado para multi-tenancy no Laravel, com exemplos de configuração e uso avançado
- Multi-Tenancy Strategies for SaaS Applications — Artigo técnico da DigitalOcean comparando as três principais abordagens de isolamento de dados
- Laravel Multi-Tenancy: Complete Guide — Tutorial prático no Laravel News com implementação passo a passo de multi-tenancy
- PostgreSQL Schemas for Multi-Tenancy — Documentação oficial do PostgreSQL sobre schemas, ideal para implementação de isolamento por schema
- Designing a Multi-Tenant Database — Padrões de arquitetura multi-tenant da Microsoft Azure, com foco em escalabilidade e segurança
- Redis Cache Tagging for Multi-Tenant Apps — Documentação do Redis sobre tags de cache, essencial para implementar cache seguro em ambientes multi-tenant