SaaS

Guía Completa 2026: Arquitectura Multi-Tenant para Plataformas SaaS

Todo lo que necesitas saber sobre arquitecturas multi-tenant escalables y seguras. Patrones de diseño, mejores prácticas y código de ejemplo.

Equipo Nexgen
7 min de lectura
#SaaS#Arquitectura#Multi-Tenant#Escalabilidad#Bases de Datos
Guía Completa 2026: Arquitectura Multi-Tenant para Plataformas SaaS

Arquitectura Multi-Tenant para Plataformas SaaS

La arquitectura multi-tenant es el fundamento de las plataformas SaaS escalables. En esta guía completa, exploraremos los patrones, desafíos y soluciones para construir sistemas que sirvan a miles de clientes con una sola instancia de aplicación.

¿Qué es Multi-Tenancy?

Multi-tenancy (multi-inquilinato) es un modelo arquitectónico donde una sola instancia de software sirve a múltiples clientes (tenants). Cada tenant tiene sus datos aislados y potencialmente su propia configuración, pero todos comparten la misma infraestructura y código base.

Beneficios Clave

  • Costos operativos reducidos: Una infraestructura para todos los clientes
  • Actualizaciones simplificadas: Deploy una vez, actualiza a todos
  • Mejor utilización de recursos: Escala eficientemente con la demanda
  • Time-to-market más rápido: Menos complejidad operacional

Modelos de Aislamiento de Datos

Existen tres modelos principales para implementar multi-tenancy a nivel de base de datos:

1. Base de Datos Compartida, Schema Compartido

Todos los tenants comparten la misma base de datos y el mismo schema. La discriminación se hace mediante una columna tenant_id.

CREATE TABLE users (
  id UUID PRIMARY KEY,
  tenant_id UUID NOT NULL,
  email VARCHAR(255) NOT NULL,
  name VARCHAR(255),
  created_at TIMESTAMP DEFAULT NOW(),
  CONSTRAINT fk_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);

CREATE INDEX idx_users_tenant ON users(tenant_id);

Ventajas:

  • Máxima eficiencia de recursos
  • Simplicidad operacional
  • Fácil de escalar horizontalmente

Desventajas:

  • Mayor riesgo de exposición de datos
  • Complejidad en consultas (siempre filtrar por tenant_id)
  • Difícil personalización por tenant

2. Base de Datos Compartida, Schemas Separados

Cada tenant tiene su propio schema dentro de la misma base de datos.

-- Crear schema para tenant
CREATE SCHEMA tenant_abc123;

CREATE TABLE tenant_abc123.users (
  id UUID PRIMARY KEY,
  email VARCHAR(255) NOT NULL,
  name VARCHAR(255),
  created_at TIMESTAMP DEFAULT NOW()
);

Ventajas:

  • Mejor aislamiento de datos
  • Personalización por tenant más sencilla
  • Backups y restauraciones por tenant

Desventajas:

  • Límite de schemas por base de datos
  • Migraciones más complejas
  • Queries cross-tenant difíciles

3. Base de Datos por Tenant

Cada tenant tiene su propia base de datos completamente aislada.

Ventajas:

  • Aislamiento completo
  • Cumplimiento regulatorio simplificado
  • Personalización total

Desventajas:

  • Costos operacionales más altos
  • Complejidad de mantenimiento
  • Difícil de escalar (muchas DBs)

Implementación en Node.js con PostgreSQL

Veamos cómo implementar el modelo compartido con discriminación por tenant_id:

// middleware/tenantContext.ts
import { Request, Response, NextFunction } from 'express';

export interface TenantRequest extends Request {
  tenantId?: string;
}

export function extractTenant(req: TenantRequest, res: Response, next: NextFunction) {
  // Extraer tenant_id del token JWT
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ error: 'No authorization token' });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any;
    req.tenantId = decoded.tenantId;
    next();
  } catch (error) {
    return res.status(401).json({ error: 'Invalid token' });
  }
}
// models/User.ts
import { Pool } from 'pg';

export class UserModel {
  constructor(private pool: Pool) {}

  async findByTenant(tenantId: string) {
    const result = await this.pool.query(
      'SELECT * FROM users WHERE tenant_id = $1',
      [tenantId]
    );
    return result.rows;
  }

  async create(tenantId: string, userData: any) {
    const result = await this.pool.query(
      `INSERT INTO users (tenant_id, email, name)
       VALUES ($1, $2, $3)
       RETURNING *`,
      [tenantId, userData.email, userData.name]
    );
    return result.rows[0];
  }

  async update(tenantId: string, userId: string, userData: any) {
    // CRÍTICO: Siempre incluir tenant_id en el WHERE
    const result = await this.pool.query(
      `UPDATE users
       SET name = $1
       WHERE id = $2 AND tenant_id = $3
       RETURNING *`,
      [userData.name, userId, tenantId]
    );

    if (result.rows.length === 0) {
      throw new Error('User not found or access denied');
    }

    return result.rows[0];
  }
}

Patrón Repository con Tenant Scoping

Para evitar olvidos de filtrar por tenant_id, implementa un patrón repository:

// repositories/TenantAwareRepository.ts
export abstract class TenantAwareRepository<T> {
  constructor(
    protected pool: Pool,
    protected tableName: string
  ) {}

  protected async query(sql: string, params: any[], tenantId: string) {
    // Interceptar y agregar filtro de tenant automáticamente
    const modifiedSql = this.injectTenantFilter(sql, tenantId);
    return this.pool.query(modifiedSql, params);
  }

  private injectTenantFilter(sql: string, tenantId: string): string {
    // Lógica para agregar WHERE tenant_id = ...
    // o AND tenant_id = ... según el query
    // ADVERTENCIA: Este es un ejemplo simplificado
    // En producción, usa un query builder como Prisma o TypeORM
    return sql;
  }

  async findAll(tenantId: string): Promise<T[]> {
    const result = await this.pool.query(
      `SELECT * FROM ${this.tableName} WHERE tenant_id = $1`,
      [tenantId]
    );
    return result.rows;
  }
}

Seguridad: Row-Level Security (RLS) en PostgreSQL

PostgreSQL ofrece Row-Level Security para enforcement a nivel de base de datos:

-- Habilitar RLS en la tabla
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

-- Crear política que filtra por tenant_id
CREATE POLICY tenant_isolation_policy ON users
  USING (tenant_id = current_setting('app.current_tenant')::uuid);

-- En tu aplicación, configura el tenant antes de cada query:
BEGIN;
SET LOCAL app.current_tenant = 'abc-123-tenant-id';
SELECT * FROM users; -- Solo retorna usuarios del tenant actual
COMMIT;

Esta approach ofrece una capa adicional de seguridad, incluso si olvidas filtrar en tu código de aplicación.

Escalabilidad: Sharding por Tenant

A medida que creces, puedes implementar sharding basado en tenant:

// tenantRouter.ts
export class TenantRouter {
  private shards: Map<string, Pool> = new Map();

  constructor(shardConfigs: ShardConfig[]) {
    shardConfigs.forEach(config => {
      this.shards.set(config.name, new Pool(config.connectionString));
    });
  }

  getShardForTenant(tenantId: string): Pool {
    // Estrategia simple: hash del tenant_id
    const shardIndex = this.hashTenantId(tenantId) % this.shards.size;
    const shardName = Array.from(this.shards.keys())[shardIndex];
    return this.shards.get(shardName)!;
  }

  private hashTenantId(tenantId: string): number {
    // Implementación de hash simple
    let hash = 0;
    for (let i = 0; i < tenantId.length; i++) {
      hash = ((hash << 5) - hash) + tenantId.charCodeAt(i);
      hash |= 0;
    }
    return Math.abs(hash);
  }
}

Migraciones en Entornos Multi-Tenant

Las migraciones deben ejecutarse de manera que no afecten a los tenants activos:

// migrations/runner.ts
export class MigrationRunner {
  async runMigration(migration: Migration) {
    // 1. Comenzar transacción
    const client = await this.pool.connect();

    try {
      await client.query('BEGIN');

      // 2. Aplicar migración
      await migration.up(client);

      // 3. Registrar en tabla de migraciones
      await client.query(
        'INSERT INTO schema_migrations (version, applied_at) VALUES ($1, NOW())',
        [migration.version]
      );

      await client.query('COMMIT');
    } catch (error) {
      await client.query('ROLLBACK');
      throw error;
    } finally {
      client.release();
    }
  }

  async runForAllSchemas(migration: Migration) {
    const schemas = await this.getAllTenantSchemas();

    for (const schema of schemas) {
      await this.setSchema(schema);
      await this.runMigration(migration);
    }
  }
}

Monitoreo y Observabilidad

En sistemas multi-tenant, es crucial monitorear métricas por tenant:

// monitoring/metrics.ts
import { Counter, Histogram } from 'prom-client';

export const requestsByTenant = new Counter({
  name: 'http_requests_by_tenant_total',
  help: 'Total HTTP requests by tenant',
  labelNames: ['tenant_id', 'method', 'status']
});

export const queryDurationByTenant = new Histogram({
  name: 'db_query_duration_by_tenant_seconds',
  help: 'Database query duration by tenant',
  labelNames: ['tenant_id', 'query_type'],
  buckets: [0.001, 0.01, 0.1, 0.5, 1, 5]
});

// Middleware
export function trackTenantMetrics(req: TenantRequest, res: Response, next: NextFunction) {
  const start = Date.now();

  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    requestsByTenant.inc({
      tenant_id: req.tenantId || 'unknown',
      method: req.method,
      status: res.statusCode
    });
  });

  next();
}

Mejores Prácticas

  1. Siempre valida tenant_id: Nunca confíes en el cliente
  2. Usa índices compuestos: (tenant_id, other_column) para queries eficientes
  3. Implementa rate limiting por tenant: Protege contra tenants abusivos
  4. Backups por tenant: Facilita la restauración selectiva
  5. Testing con múltiples tenants: Incluye casos edge de cross-tenant
  6. Auditoría de acceso: Log todos los accesos a datos con tenant_id
  7. Planes de desastre: ¿Qué pasa si un tenant se compromete?

Herramientas y Frameworks

  • Prisma: ORM con soporte multi-tenant nativo
  • TypeORM: Flexible para implementar patrones custom
  • AWS RDS Proxy: Connection pooling optimizado
  • Vault: Gestión segura de credenciales por tenant

Conclusión

La arquitectura multi-tenant es poderosa pero requiere diseño cuidadoso. Los beneficios en costos y escalabilidad superan ampliamente la complejidad adicional cuando se implementa correctamente.

En Nexgen, hemos implementado arquitecturas multi-tenant para clientes que sirven desde cientos hasta millones de usuarios finales. La clave está en elegir el modelo correcto según tus necesidades específicas de aislamiento, compliance y escala.

¿Estás construyendo una plataforma SaaS? Agenda una consulta gratuita para discutir la arquitectura ideal para tu proyecto.