Makingcode
Volver al blog

NestJS Enterprise · Parte 4 de 4

Ver todos
Arquitectura4 min de lecturaAvailable in English

Cómo construir una aplicación SaaS Multi-Tenant en NestJS sin duplicar tu código

Si agregar un nuevo cliente implica desplegar una nueva aplicación o copiar una base de datos completa, probablemente tu arquitectura SaaS no está preparada para escalar. Aprende cómo implementar multi-tenancy en NestJS de forma limpia y mantenible

Cómo construir una aplicación SaaS Multi-Tenant en NestJS sin duplicar tu código

Existe un momento en toda aplicación SaaS exitosa donde aparece una pregunta incómoda:

¿Cómo agregamos un nuevo cliente sin desplegar otra aplicación?

Al principio todo parece sencillo.

Tienes una API.

Tienes una base de datos.

Tienes un único cliente.

La arquitectura funciona perfectamente.

Luego llega un segundo cliente.

Después un tercero.

Y eventualmente alguien propone algo como:

Cliente A -> Base de datos A
Cliente B -> Base de datos B
Cliente C -> Base de datos C

Al principio parece una solución razonable.

Meses después descubres que cada despliegue, migración o cambio de esquema debe ejecutarse múltiples veces.

El mantenimiento comienza a convertirse en una pesadilla.

El problema no es la cantidad de clientes.

El problema es la arquitectura.

¿Qué significa Multi-Tenant?

Multi-Tenant significa que múltiples organizaciones utilizan la misma plataforma mientras sus datos permanecen completamente aislados.

Por ejemplo:

Empresa A
 ├── Usuarios
 ├── Facturas
 └── Inventario
 
Empresa B
 ├── Usuarios
 ├── Facturas
 └── Inventario

Ambas empresas utilizan exactamente la misma aplicación.

La diferencia está en los datos.

Cada solicitud debe ejecutarse dentro del contexto correcto.

El error más común

Muchas implementaciones comienzan agregando:

companyId
tenantId
organizationId

a todas las tablas.

@Entity('users')
export class UserEntity {
  id: string;
 
  tenantId: string;
 
  email: string;
}

Esto funciona.

Hasta que alguien olvida filtrar:

WHERE tenant_id = ?

Y de repente un cliente puede visualizar información de otro.

Ese es uno de los errores más costosos que puede sufrir una plataforma SaaS.

La pregunta correcta

No deberíamos preguntarnos:

¿Cómo filtramos los tenants?

Deberíamos preguntarnos:

¿Cómo garantizamos que nunca olvidaremos filtrar un tenant?

La diferencia es enorme.

Tenant Resolution

Lo primero que necesita una aplicación multi-tenant es descubrir quién realiza la solicitud.

Existen varias estrategias.

Subdominios

acme.miapp.com
 
globex.miapp.com

Headers

X-Tenant-Id: acme

JWT

{
  "sub": "123",
  "tenantId": "acme"
}

En la mayoría de los proyectos modernos, JWT suele ser la opción más práctica.

Creando el Tenant Context

Una vez identificado el tenant, debemos almacenarlo durante toda la solicitud.

Por ejemplo:

export interface TenantContext {
  tenantId: string;
}

Middleware:

@Injectable()
export class TenantMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    req['tenantId'] = extractTenant(req);
 
    next();
  }
}

A partir de este momento todos los módulos pueden acceder al contexto actual.

Shared Database vs Dedicated Database

Aquí aparece una de las decisiones más importantes.

Shared Database

Database
 ├── users
 ├── orders
 └── invoices

Cada tabla contiene tenantId.

Ventajas:

  • Menor costo.
  • Menor complejidad operativa.
  • Más fácil de administrar.

Desventajas:

  • Menor aislamiento.
  • Riesgo de errores de filtrado.

Dedicated Database

Tenant A -> Database A
 
Tenant B -> Database B
 
Tenant C -> Database C

Ventajas:

  • Máximo aislamiento.
  • Mayor seguridad.

Desventajas:

  • Costos más altos.
  • Migraciones más complejas.
  • Operación más costosa.

El modelo híbrido

Muchas plataformas modernas utilizan:

Plan Starter
  -> Shared Database
 
Plan Enterprise
  -> Dedicated Database

Esto permite optimizar costos sin sacrificar flexibilidad.

Resolución dinámica de conexiones

Cuando utilizamos bases de datos dedicadas, la aplicación debe resolver dinámicamente la conexión.

const connection =
  await tenantConnectionFactory.getConnection(
    tenantId,
  );

A partir de aquí los repositorios trabajan utilizando la conexión correcta.

El dominio nunca necesita conocer cómo se resolvió.

Multi-Tenancy y Arquitectura Hexagonal

Aquí es donde la arquitectura comienza a mostrar su valor.

Los casos de uso no deberían saber nada sobre:

  • PostgreSQL
  • MongoDB
  • Tenant Resolution
  • Connection Factories

Los handlers trabajan normalmente:

await userRepository.create(user);

La infraestructura decide qué conexión utilizar.

Esto mantiene el dominio limpio y desacoplado.

Seguridad

La seguridad en sistemas multi-tenant no es opcional.

Siempre valida:

  • Tenant del JWT.
  • Tenant del recurso.
  • Tenant de la conexión.

Nunca confíes únicamente en información enviada por el cliente.

Lo que ganamos en la práctica

Beneficio Impacto
Escalabilidad Nuevos clientes sin nuevas aplicaciones
Menor costo Infraestructura compartida
Seguridad Aislamiento de datos
Mantenibilidad Menos despliegues
Flexibilidad Soporte para distintos planes

Para cerrar

Muchas empresas creen que construir una aplicación SaaS consiste en agregar usuarios.

En realidad consiste en administrar aislamiento.

Si la incorporación de un nuevo cliente requiere crear otra aplicación, duplicar repositorios o desplegar nuevos servicios, probablemente tu arquitectura aún no sea verdaderamente multi-tenant.

Una buena implementación permite agregar organizaciones, cambiar planes, escalar infraestructura y mantener aislamiento sin modificar la lógica de negocio.

Y cuando el dominio deja de preocuparse por los tenants, la plataforma comienza a escalar de verdad.

Recibe artículos por email

Sin spam — solo un aviso cuando publique algo nuevo sobre backend, cloud y arquitectura.

Un email cuando salga un artículo. Puedes darte de baja cuando quieras.

Artículos relacionados