template-saas/docs/00-vision-general/ARQUITECTURA-MULTI-TENANT.md
rckrdmrd 50a821a415
Some checks failed
CI / Backend CI (push) Has been cancelled
CI / Frontend CI (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / CI Summary (push) Has been cancelled
[SIMCO-V38] feat: Actualizar a SIMCO v3.8.0
- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8
- Actualizaciones de configuracion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 08:53:08 -06:00

16 KiB

id title type status priority version created_date updated_date
VIS-004 Arquitectura Multi-Tenant Architecture Published P0 1.0.0 2026-01-07 2026-01-10

ARQUITECTURA MULTI-TENANT

Proyecto: template-saas Version: 1.0.0 Tipo: Especificacion Tecnica


Modelo de Multi-Tenancy

Este template implementa multi-tenancy a nivel de base de datos usando PostgreSQL Row-Level Security (RLS).

Estrategia: Shared Database, Shared Schema

┌─────────────────────────────────────────────────────────────┐
│                    PostgreSQL Instance                       │
│  ┌────────────────────────────────────────────────────────┐ │
│  │                    Database: saas_db                    │ │
│  │  ┌──────────┬──────────┬──────────┬──────────────────┐ │ │
│  │  │ Tenant A │ Tenant B │ Tenant C │ ... Tenant N     │ │ │
│  │  │ (RLS)    │ (RLS)    │ (RLS)    │     (RLS)        │ │ │
│  │  └──────────┴──────────┴──────────┴──────────────────┘ │ │
│  └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

Ventajas

  • Simplicidad operacional - Una sola base de datos
  • Eficiencia de recursos - Conexiones compartidas
  • Mantenimiento facil - Migraciones unicas
  • Escalabilidad - Cientos de tenants sin overhead

Consideraciones

  • RLS DEBE estar habilitado en TODAS las tablas con datos de tenant
  • Cada query DEBE incluir el tenant_id en el contexto
  • Backups incluyen todos los tenants (encriptacion requerida)

Implementacion RLS

Estructura Base de Tablas

-- Todas las tablas de tenant heredan esta estructura
CREATE TABLE tenants.base_entity (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants.tenants(id),
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),
    created_by UUID,
    updated_by UUID
);

-- Indice obligatorio en tenant_id
CREATE INDEX idx_base_entity_tenant ON tenants.base_entity(tenant_id);

Politicas RLS Estandar

-- Habilitar RLS
ALTER TABLE {schema}.{table} ENABLE ROW LEVEL SECURITY;

-- Politica de lectura
CREATE POLICY "{table}_tenant_isolation_select" ON {schema}.{table}
    FOR SELECT
    USING (tenant_id = current_setting('app.current_tenant_id')::UUID);

-- Politica de insercion
CREATE POLICY "{table}_tenant_isolation_insert" ON {schema}.{table}
    FOR INSERT
    WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::UUID);

-- Politica de actualizacion
CREATE POLICY "{table}_tenant_isolation_update" ON {schema}.{table}
    FOR UPDATE
    USING (tenant_id = current_setting('app.current_tenant_id')::UUID)
    WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::UUID);

-- Politica de eliminacion
CREATE POLICY "{table}_tenant_isolation_delete" ON {schema}.{table}
    FOR DELETE
    USING (tenant_id = current_setting('app.current_tenant_id')::UUID);

Configuracion de Contexto

-- Funcion para establecer tenant actual
CREATE OR REPLACE FUNCTION auth.set_current_tenant(p_tenant_id UUID)
RETURNS VOID AS $$
BEGIN
    PERFORM set_config('app.current_tenant_id', p_tenant_id::TEXT, FALSE);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Funcion para obtener tenant actual
CREATE OR REPLACE FUNCTION auth.get_current_tenant()
RETURNS UUID AS $$
BEGIN
    RETURN current_setting('app.current_tenant_id', TRUE)::UUID;
END;
$$ LANGUAGE plpgsql STABLE;

Arquitectura Backend

Middleware de Tenant

// src/shared/middleware/tenant.middleware.ts

@Injectable()
export class TenantMiddleware implements NestMiddleware {
  constructor(
    private readonly tenantService: TenantService,
    private readonly dataSource: DataSource,
  ) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const tenantId = this.extractTenantId(req);

    if (!tenantId) {
      throw new UnauthorizedException('Tenant not identified');
    }

    // Validar que el tenant existe y esta activo
    const tenant = await this.tenantService.findById(tenantId);
    if (!tenant || tenant.status !== 'active') {
      throw new ForbiddenException('Tenant inactive or not found');
    }

    // Establecer en request para uso posterior
    req['tenant'] = tenant;

    // Establecer en PostgreSQL para RLS
    await this.dataSource.query(
      `SELECT auth.set_current_tenant($1)`,
      [tenantId]
    );

    next();
  }

  private extractTenantId(req: Request): string | null {
    // 1. Desde JWT token
    if (req.user?.tenantId) {
      return req.user.tenantId;
    }

    // 2. Desde header
    const headerTenant = req.headers['x-tenant-id'];
    if (headerTenant) {
      return headerTenant as string;
    }

    // 3. Desde subdominio
    const host = req.headers.host;
    if (host) {
      const subdomain = host.split('.')[0];
      // Lookup tenant by subdomain
    }

    return null;
  }
}

Guard de Tenant

// src/shared/guards/tenant.guard.ts

@Injectable()
export class TenantGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();

    if (!request.tenant) {
      throw new ForbiddenException('Tenant context required');
    }

    return true;
  }
}

Decorador de Tenant

// src/shared/decorators/tenant.decorator.ts

export const CurrentTenant = createParamDecorator(
  (data: unknown, ctx: ExecutionContext): Tenant => {
    const request = ctx.switchToHttp().getRequest();
    return request.tenant;
  },
);

// Uso en controlador
@Get('products')
@UseGuards(TenantGuard)
async getProducts(@CurrentTenant() tenant: Tenant) {
  return this.productService.findAll(tenant.id);
}

Arquitectura de Schemas

┌─────────────────────────────────────────────────────────────┐
│                     DATABASE SCHEMAS                         │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │    auth     │  │   tenants   │  │       users         │  │
│  │             │  │             │  │                     │  │
│  │ - sessions  │  │ - tenants   │  │ - users             │  │
│  │ - tokens    │  │ - settings  │  │ - roles             │  │
│  │ - oauth     │  │ - domains   │  │ - permissions       │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
│                                                              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │   billing   │  │    plans    │  │       audit         │  │
│  │             │  │             │  │                     │  │
│  │ - invoices  │  │ - plans     │  │ - audit_logs        │  │
│  │ - payments  │  │ - features  │  │ - activity_logs     │  │
│  │ - subscript │  │ - limits    │  │                     │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
│                                                              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │notifications│  │   storage   │  │      webhooks       │  │
│  │             │  │             │  │                     │  │
│  │ - templates │  │ - files     │  │ - endpoints         │  │
│  │ - queue     │  │ - metadata  │  │ - deliveries        │  │
│  │ - logs      │  │             │  │ - logs              │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Flujo de Autenticacion

┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│  Client  │────▶│  Login   │────▶│  Verify  │────▶│  Issue   │
│          │     │ Request  │     │  Creds   │     │  JWT     │
└──────────┘     └──────────┘     └──────────┘     └──────────┘
                                                         │
                                                         ▼
┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│ Response │◀────│   Set    │◀────│  Create  │◀────│  Token   │
│  + JWT   │     │  Tenant  │     │ Session  │     │ contains │
│          │     │  Context │     │          │     │ tenantId │
└──────────┘     └──────────┘     └──────────┘     └──────────┘

JWT Payload

interface JwtPayload {
  sub: string;           // user_id
  tenantId: string;      // tenant_id
  email: string;
  roles: string[];
  permissions: string[];
  iat: number;
  exp: number;
}

Modelo de Datos Principal

Tenant

CREATE TABLE tenants.tenants (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    slug VARCHAR(100) UNIQUE NOT NULL,
    domain VARCHAR(255) UNIQUE,
    status VARCHAR(50) DEFAULT 'active',
    plan_id UUID REFERENCES plans.plans(id),
    settings JSONB DEFAULT '{}',
    metadata JSONB DEFAULT '{}',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),

    CONSTRAINT valid_status CHECK (status IN ('active', 'suspended', 'cancelled'))
);

User (pertenece a Tenant)

CREATE TABLE users.users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants.tenants(id),
    email VARCHAR(255) NOT NULL,
    password_hash VARCHAR(255),
    first_name VARCHAR(100),
    last_name VARCHAR(100),
    status VARCHAR(50) DEFAULT 'active',
    email_verified BOOLEAN DEFAULT FALSE,
    last_login TIMESTAMPTZ,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),

    CONSTRAINT unique_email_per_tenant UNIQUE (tenant_id, email)
);

-- RLS
ALTER TABLE users.users ENABLE ROW LEVEL SECURITY;
CREATE POLICY "users_tenant_isolation" ON users.users
    USING (tenant_id = current_setting('app.current_tenant_id')::UUID);

Plan

CREATE TABLE plans.plans (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(100) NOT NULL,
    slug VARCHAR(50) UNIQUE NOT NULL,
    description TEXT,
    price_monthly DECIMAL(10,2),
    price_yearly DECIMAL(10,2),
    stripe_price_id_monthly VARCHAR(255),
    stripe_price_id_yearly VARCHAR(255),
    features JSONB DEFAULT '[]',
    limits JSONB DEFAULT '{}',
    is_active BOOLEAN DEFAULT TRUE,
    sort_order INT DEFAULT 0,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Ejemplo de limits JSONB
-- {
--   "max_users": 5,
--   "max_storage_gb": 10,
--   "max_api_calls_month": 10000,
--   "max_products": 100
-- }

Integracion Stripe

Flujo de Suscripcion

┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│  Tenant  │────▶│  Select  │────▶│  Stripe  │────▶│ Webhook  │
│  Signup  │     │   Plan   │     │ Checkout │     │ Received │
└──────────┘     └──────────┘     └──────────┘     └──────────┘
                                                         │
                                                         ▼
┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│  Tenant  │◀────│  Update  │◀────│  Create  │◀────│ Process  │
│  Active  │     │  Status  │     │ Invoice  │     │  Event   │
└──────────┘     └──────────┘     └──────────┘     └──────────┘

Webhooks de Stripe

Evento Accion
checkout.session.completed Activar suscripcion
customer.subscription.updated Actualizar plan
customer.subscription.deleted Cancelar suscripcion
invoice.paid Registrar pago
invoice.payment_failed Notificar y suspender

Seguridad

Capas de Seguridad

  1. Network - HTTPS obligatorio, rate limiting
  2. Authentication - JWT, OAuth, MFA
  3. Authorization - RBAC, tenant isolation
  4. Database - RLS, conexiones encriptadas
  5. Application - Input validation, OWASP

Headers de Seguridad

// Helmet configuration
app.use(helmet({
  contentSecurityPolicy: true,
  crossOriginEmbedderPolicy: true,
  crossOriginOpenerPolicy: true,
  crossOriginResourcePolicy: true,
  dnsPrefetchControl: true,
  frameguard: true,
  hidePoweredBy: true,
  hsts: true,
  ieNoOpen: true,
  noSniff: true,
  originAgentCluster: true,
  permittedCrossDomainPolicies: true,
  referrerPolicy: true,
  xssFilter: true,
}));

Escalabilidad

Horizontal

  • Backend stateless (multiples instancias)
  • Redis para sesiones y cache
  • PostgreSQL read replicas
  • CDN para assets estaticos

Vertical

  • Connection pooling (PgBouncer)
  • Query optimization
  • Indices estrategicos
  • Particionamiento de tablas grandes

Por Tenant

  • Rate limiting por tenant
  • Cuotas de almacenamiento
  • Limites de API calls
  • Fair usage policies

Monitoreo

Metricas Clave

  • Requests por tenant
  • Latencia de API
  • Errores por tenant
  • Uso de recursos
  • Conexiones de DB

Alertas

  • Tenant excede cuota
  • Error rate alto
  • Latencia degradada
  • DB connections altas

Referencias

  • Vision: VISION-TEMPLATE-SAAS.md
  • Inventarios: ../../orchestration/inventarios/
  • Database DDL: ../../apps/database/ddl/

Creado: 2026-01-07 Actualizado: 2026-01-07