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