La mayoría de los sistemas web tienen una relación uno a uno con sus usuarios: una base de datos, una aplicación, un cliente. Cuando el modelo de negocio es SaaS —cobrar suscripción a múltiples empresas que usan el mismo producto— esa relación cambia. Y si la arquitectura no está preparada para ese cambio, el sistema se convierte en un problema antes de tener tracción real.
Multitenancy es la respuesta técnica a esa realidad. Este artículo explica cómo implementarla en Laravel de forma robusta, con el nivel de aislamiento que exigen clientes empresariales.
El problema de diseño
Cuando un SaaS crece, inevitablemente aparece la pregunta: ¿cómo servimos a múltiples organizaciones desde una sola base de código sin que los datos de una empresa sean accesibles desde otra?
La respuesta superficial es agregar una columna tenant_id a todas las tablas y filtrar por ella en cada query. Funciona. Hasta que un desarrollador olvida el where, o hasta que un cliente enterprise exige que sus datos estén físicamente separados de los demás para cumplir con su política de seguridad.
La arquitectura multitenant no es solo una decisión técnica. Es una decisión de producto que determina qué clientes podés aceptar y a qué precio.
Las tres estrategias de aislamiento
Hay tres formas de implementar multitenancy. La elección correcta depende del perfil de cliente, el volumen de datos y los requisitos de cumplimiento normativo.
Base de datos compartida, tabla compartida
Todos los tenants comparten las mismas tablas. La separación se logra exclusivamente con una columna tenant_id y políticas de filtrado en la capa de aplicación. Es la arquitectura más simple de operar y la más frágil desde el punto de vista de seguridad. Aceptable para productos con datos de baja sensibilidad y equipos técnicos muy pequeños.
Base de datos compartida, schema por tenant (PostgreSQL)
Cada tenant tiene su propio schema dentro de la misma instancia de Postgres. La separación es más fuerte que en el caso anterior, pero el soporte en MySQL/MariaDB es limitado. Es una opción intermedia que combina buena separación lógica con operación centralizada.
Base de datos separada por tenant
Cada tenant tiene su propia base de datos. El aislamiento es total: un bug en un query nunca puede exponer datos de otro cliente. Es la única arquitectura aceptable para clientes con requisitos de seguridad serios, sectores regulados (salud, legal, finanzas) o cualquier producto que necesite ofrecer SLAs individuales por cliente.
El resto de este artículo asume esta tercera estrategia, implementada con el paquete stancl/tenancy.
Instalación y estructura del proyecto
El paquete stancl/tenancy es el estándar de facto en el ecosistema Laravel para multitenancy con bases de datos separadas. Maneja la creación de tenants, el cambio de conexión y el ciclo de vida completo de cada cliente.
$ composer require stancl/tenancy $ php artisan tenancy:install $ php artisan migrate
La instalación crea las tablas de la base de datos central (tenants, domains) y publica el archivo de configuración. A partir de ahí, la estructura del proyecto distingue dos mundos: el central y el de cada tenant.
Separar las migraciones desde el principio
Esta es la decisión estructural más importante. Las migraciones que corresponden a la base de datos central (planes, facturación, administración global) van en database/migrations/. Las que corresponden a cada tenant van en database/migrations/tenant/. Mezclarlas genera problemas difíciles de resolver una vez que hay clientes en producción.
Modelo Tenant
namespace App\Models; use Stancl\Tenancy\Database\Models\Tenant as BaseTenant; use Stancl\Tenancy\Contracts\TenantWithDatabase; use Stancl\Tenancy\Database\Concerns\HasDatabase; use Stancl\Tenancy\Database\Concerns\HasDomains; class Tenant extends BaseTenant implements TenantWithDatabase { use HasDatabase, HasDomains; public static function getCustomColumns(): array { // Columnas que viven en la base de datos central return ['id', 'plan', 'activo', 'razon_social']; } }
Ciclo de vida de un tenant
Cuando un nuevo cliente se registra en el SaaS, el sistema debe ejecutar tres operaciones en orden: crear el registro del tenant en la base central, provisionar su base de datos y configurar el subdominio. Esto se encapsula en un job de Laravel para ejecutarlo de forma asíncrona.
public function handle(): void { $tenant = Tenant::create([ 'id' => $this->slug, 'plan' => $this->plan, 'activo' => true, 'razon_social' => $this->razonSocial, ]); // Subdominio propio: cliente.tuapp.com $tenant->domains()->create([ 'domain' => $this->slug . '.' . config('tenancy.domain_suffix') ]); // Correr migraciones en la nueva base de datos $tenant->run(function () { Artisan::call('migrate', [ '--path' => 'database/migrations/tenant', '--force' => true, ]); }); }
La separación en un job asíncrono es importante en producción: provisionar una base de datos puede tomar algunos segundos, y eso no debería bloquear el request HTTP del usuario que acaba de registrarse.
Routing y middleware
Una de las partes más críticas del sistema es garantizar que las rutas del producto corran siempre dentro de un contexto de tenant válido. El paquete provee middleware específico para inicializar el tenant correcto a partir del subdominio de cada request.
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Middleware\PreventAccessFromCentralDomains; Route::middleware([ 'web', InitializeTenancyByDomain::class, PreventAccessFromCentralDomains::class, ])->group(function () { // A partir de acá, todas las queries apuntan // automáticamente a la base de datos del tenant // que corresponde al subdominio del request. Route::get('/dashboard', [DashboardController::class, 'index']); Route::resource('clientes', ClienteController::class); Route::resource('expedientes', ExpedienteController::class); });
El middleware detecta el subdominio, resuelve el tenant en la base central, cambia la conexión de base de datos y setea el contexto del tenant para todo el ciclo de vida de ese request. El resto del código de aplicación no necesita saber que está en un entorno multitenant.
Migraciones en escala
Cuando se lanza una nueva funcionalidad que requiere un cambio de schema, la migración debe ejecutarse en cada una de las bases de datos de tenants activos.
# Correr migraciones en todos los tenants activos $ php artisan tenants:migrate # Solo en tenants específicos $ php artisan tenants:migrate --tenants=tenant-a,tenant-b # Rollback en todos los tenants $ php artisan tenants:migrate:rollback
En producción con muchos tenants, conviene ejecutar esto como un job en background que registra el resultado por tenant y alerta si alguna migración falla. Una migración fallida en un tenant específico no debería interrumpir el proceso completo.
Consideraciones de infraestructura
La arquitectura de base de datos separada resuelve bien el problema de aislamiento, pero introduce complejidad operacional que hay que anticipar:
- DNS wildcard:
*.tuapp.comdebe apuntar al servidor para que los subdominios funcionen sin configuración manual por tenant. Esto va en el proveedor de DNS, no en el código. - TLS wildcard: un certificado
*.tuapp.comcubre automáticamente todos los subdominios. Let's Encrypt con Certbot soporta wildcards vía DNS challenge. - Backups por tenant: cada base de datos es un backup independiente. En producción se traduce en scripts que iteran sobre los tenants activos y ejecutan
mysqldumppara cada uno. - Monitoreo por tenant: las métricas de uso deben poder filtrarse por tenant para detectar problemas específicos antes de que escalen.
Cuándo esta arquitectura tiene sentido y cuándo no
La arquitectura de base de datos separada por tenant tiene sentido cuando el producto maneja datos sensibles (salud, legal, financiero), cuando los clientes son empresas medianas o grandes con políticas de seguridad propias, cuando se necesita ofrecer backups o restauraciones individuales, o cuando el volumen de datos por cliente justifica el overhead operacional.
No tiene sentido cuando el producto tiene miles de tenants con pocos datos cada uno, cuando el equipo técnico es de una sola persona sin experiencia operacional, o cuando los datos son inherentemente no sensibles y el time-to-market importa más que el aislamiento.
La arquitectura correcta es la que resuelve los problemas reales del producto, no la más sofisticada ni la más simple disponible.