---
id: ARQUITECTURA-SAAS-ERP-CORE
title: Arquitectura SaaS - ERP Core
type: Architecture
status: Published
version: 1.0.0
created_date: 2026-01-10
updated_date: 2026-01-10
---

# Arquitectura SaaS - ERP Core

> Detalle de la arquitectura de plataforma SaaS multi-tenant

## Resumen

ERP Core implementa una arquitectura SaaS completa que permite a multiples organizaciones (tenants) usar la misma instancia de la aplicacion con aislamiento total de datos.

---

## 1. Diagrama de Arquitectura

```mermaid
graph TB
    subgraph "Clientes"
        U1[Usuario Tenant A]
        U2[Usuario Tenant B]
        U3[SuperAdmin]
    end

    subgraph "Frontend"
        FE[React App]
        PA[Portal Admin]
        PSA[Portal SuperAdmin]
    end

    subgraph "API Gateway"
        AG[Express.js]
        MW1[Auth Middleware]
        MW2[Tenant Middleware]
        MW3[Rate Limiter]
    end

    subgraph "Servicios Backend"
        AUTH[Auth Service]
        USER[User Service]
        BILL[Billing Service]
        PLAN[Plans Service]
        NOTIF[Notification Service]
        WH[Webhook Service]
        FF[Feature Flags]
    end

    subgraph "Base de Datos"
        PG[(PostgreSQL)]
        RLS[Row-Level Security]
    end

    subgraph "Servicios Externos"
        STRIPE[Stripe]
        SG[SendGrid]
        REDIS[Redis]
    end

    U1 --> FE
    U2 --> FE
    U3 --> PSA

    FE --> AG
    PA --> AG
    PSA --> AG

    AG --> MW1 --> MW2 --> MW3

    MW3 --> AUTH
    MW3 --> USER
    MW3 --> BILL
    MW3 --> PLAN
    MW3 --> NOTIF
    MW3 --> WH
    MW3 --> FF

    AUTH --> PG
    USER --> PG
    BILL --> PG
    PLAN --> PG
    NOTIF --> PG
    WH --> PG
    FF --> PG

    PG --> RLS

    BILL --> STRIPE
    NOTIF --> SG
    WH --> REDIS
```

---

## 2. Multi-Tenancy con Row-Level Security (RLS)

### 2.1 Concepto

Row-Level Security (RLS) es una caracteristica de PostgreSQL que permite filtrar automaticamente las filas de una tabla basandose en politicas definidas. Esto garantiza aislamiento de datos entre tenants a nivel de base de datos.

### 2.2 Implementacion

#### 2.2.1 Estructura de Tabla Multi-Tenant

```sql
CREATE TABLE tenants.tenants (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    name VARCHAR(100) NOT NULL,
    slug VARCHAR(50) UNIQUE NOT NULL,
    settings JSONB DEFAULT '{}',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Tabla de ejemplo con tenant_id
CREATE TABLE users.users (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES tenants.tenants(id),
    email VARCHAR(255) NOT NULL,
    name VARCHAR(100),
    created_at TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(tenant_id, email)
);
```

#### 2.2.2 Politicas RLS

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

-- Politica de SELECT
CREATE POLICY users_tenant_isolation_select
    ON users.users FOR SELECT
    USING (tenant_id = current_setting('app.current_tenant_id')::UUID);

-- Politica de INSERT
CREATE POLICY users_tenant_isolation_insert
    ON users.users FOR INSERT
    WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::UUID);

-- Politica de UPDATE
CREATE POLICY users_tenant_isolation_update
    ON users.users FOR UPDATE
    USING (tenant_id = current_setting('app.current_tenant_id')::UUID);

-- Politica de DELETE
CREATE POLICY users_tenant_isolation_delete
    ON users.users FOR DELETE
    USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
```

#### 2.2.3 Middleware de Contexto

```typescript
// tenant.middleware.ts
export async function tenantMiddleware(req, res, next) {
    const tenantId = req.user?.tenantId;

    if (!tenantId) {
        return res.status(401).json({ error: 'Tenant not found' });
    }

    // Establecer contexto de tenant en PostgreSQL
    await db.query(`SET app.current_tenant_id = '${tenantId}'`);

    next();
}
```

### 2.3 Ventajas de RLS

| Ventaja | Descripcion |
|---------|-------------|
| Seguridad | Aislamiento a nivel de base de datos |
| Simplicidad | Una sola base de datos para todos los tenants |
| Performance | Indices compartidos, optimizacion global |
| Migraciones | Una migracion aplica a todos los tenants |
| Escalabilidad | Puede manejar millones de tenants |

---

## 3. Billing y Suscripciones

### 3.1 Diagrama de Flujo

```mermaid
sequenceDiagram
    participant U as Usuario
    participant FE as Frontend
    participant BE as Backend
    participant S as Stripe
    participant DB as Database

    U->>FE: Selecciona plan
    FE->>BE: POST /billing/checkout
    BE->>S: Crear Checkout Session
    S-->>BE: Session URL
    BE-->>FE: Redirect URL
    FE->>S: Redirect a Stripe
    U->>S: Completa pago
    S->>BE: Webhook: checkout.session.completed
    BE->>DB: Crear suscripcion
    BE->>S: Obtener detalles
    S-->>BE: Subscription details
    BE->>DB: Actualizar tenant
    BE-->>FE: Success
```

### 3.2 Estados de Suscripcion

```mermaid
stateDiagram-v2
    [*] --> trialing: Registro
    trialing --> active: Pago exitoso
    trialing --> cancelled: No paga
    active --> past_due: Pago fallido
    past_due --> active: Pago recuperado
    past_due --> cancelled: Sin pago 30 dias
    active --> cancelled: Cancelacion
    cancelled --> [*]
```

### 3.3 Modelo de Datos

```sql
-- Schema: billing

CREATE TABLE billing.subscriptions (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES tenants.tenants(id),
    stripe_subscription_id VARCHAR(100) UNIQUE,
    stripe_customer_id VARCHAR(100),
    plan_id UUID REFERENCES plans.plans(id),
    status VARCHAR(20) NOT NULL, -- trialing, active, past_due, cancelled
    current_period_start TIMESTAMPTZ,
    current_period_end TIMESTAMPTZ,
    trial_end TIMESTAMPTZ,
    cancel_at_period_end BOOLEAN DEFAULT false,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE billing.invoices (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL,
    subscription_id UUID REFERENCES billing.subscriptions(id),
    stripe_invoice_id VARCHAR(100) UNIQUE,
    amount_due INTEGER NOT NULL, -- en centavos
    amount_paid INTEGER DEFAULT 0,
    currency VARCHAR(3) DEFAULT 'USD',
    status VARCHAR(20), -- draft, open, paid, void, uncollectible
    invoice_url TEXT,
    invoice_pdf TEXT,
    due_date TIMESTAMPTZ,
    paid_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ DEFAULT NOW()
);
```

### 3.4 Webhooks de Stripe

| Evento | Accion |
|--------|--------|
| `customer.subscription.created` | Crear registro de suscripcion |
| `customer.subscription.updated` | Actualizar plan/status |
| `customer.subscription.deleted` | Marcar como cancelado |
| `invoice.paid` | Registrar pago exitoso |
| `invoice.payment_failed` | Notificar fallo, marcar past_due |

---

## 4. Planes y Feature Gating

### 4.1 Modelo de Planes

```sql
-- Schema: plans

CREATE TABLE plans.plans (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    name VARCHAR(50) NOT NULL,
    slug VARCHAR(50) UNIQUE NOT NULL,
    stripe_price_id VARCHAR(100),
    price_monthly INTEGER NOT NULL, -- en centavos
    price_yearly INTEGER,
    currency VARCHAR(3) DEFAULT 'USD',
    trial_days INTEGER DEFAULT 14,
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE plans.plan_features (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    plan_id UUID NOT NULL REFERENCES plans.plans(id),
    feature_key VARCHAR(50) NOT NULL, -- ej: 'ai_assistant'
    feature_value JSONB NOT NULL, -- true/false o {limit: 100}
    UNIQUE(plan_id, feature_key)
);
```

### 4.2 Planes Propuestos

| Plan | Precio/mes | Usuarios | Storage | AI | Webhooks |
|------|-----------|----------|---------|-----|----------|
| Free | $0 | 1 | 100MB | No | No |
| Starter | $29 | 5 | 1GB | No | No |
| Pro | $79 | 20 | 10GB | Si | Si |
| Enterprise | $199 | Unlimited | Unlimited | Si | Si |

### 4.3 Feature Gating

```typescript
// plans.service.ts

// Verificar si tenant tiene feature
async hasFeature(tenantId: string, feature: string): Promise<boolean> {
    const subscription = await this.getActiveSubscription(tenantId);
    const planFeature = await this.getPlanFeature(subscription.planId, feature);
    return planFeature?.feature_value === true;
}

// Verificar limite numerico
async checkLimit(tenantId: string, limitKey: string, currentCount: number): Promise<boolean> {
    const subscription = await this.getActiveSubscription(tenantId);
    const planFeature = await this.getPlanFeature(subscription.planId, limitKey);
    const limit = planFeature?.feature_value?.limit ?? 0;
    return limit === -1 || currentCount < limit; // -1 = unlimited
}
```

### 4.4 Uso en Controllers

```typescript
// users.controller.ts

@Post()
@RequiresFeature('users.create')
@CheckLimit('users')
async createUser(@Body() dto: CreateUserDto) {
    // Solo se ejecuta si tiene la feature y no excede el limite
}
```

---

## 5. Webhooks Outbound

### 5.1 Diagrama de Flujo

```mermaid
sequenceDiagram
    participant App as Aplicacion
    participant WS as Webhook Service
    participant Q as Redis Queue
    participant W as Worker
    participant EP as Endpoint Externo

    App->>WS: Evento ocurrio
    WS->>Q: Encolar trabajo
    Q-->>W: Procesar
    W->>EP: POST con payload firmado

    alt Exito (2xx)
        EP-->>W: 200 OK
        W->>DB: Marcar entregado
    else Fallo
        EP-->>W: Error
        W->>Q: Re-encolar (retry)
    end
```

### 5.2 Firma HMAC

```typescript
// webhook.service.ts

function signPayload(payload: string, secret: string, timestamp: number): string {
    const signatureInput = `${timestamp}.${payload}`;
    const signature = crypto
        .createHmac('sha256', secret)
        .update(signatureInput)
        .digest('hex');
    return `t=${timestamp},v1=${signature}`;
}

// Header enviado
// X-Webhook-Signature: t=1704067200000,v1=abc123...
```

### 5.3 Politica de Reintentos

| Intento | Delay |
|---------|-------|
| 1 | Inmediato |
| 2 | +1 minuto |
| 3 | +5 minutos |
| 4 | +30 minutos |
| 5 | +2 horas |
| 6 | +6 horas |
| Fallo | Marcar como fallido |

### 5.4 Eventos Disponibles

| Evento | Descripcion |
|--------|-------------|
| `user.created` | Usuario creado |
| `user.updated` | Usuario actualizado |
| `user.deleted` | Usuario eliminado |
| `subscription.created` | Suscripcion creada |
| `subscription.updated` | Suscripcion actualizada |
| `subscription.cancelled` | Suscripcion cancelada |
| `invoice.paid` | Factura pagada |
| `invoice.failed` | Pago fallido |

---

## 6. Feature Flags

### 6.1 Modelo de Datos

```sql
-- Schema: feature_flags

CREATE TABLE feature_flags.flags (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    key VARCHAR(50) UNIQUE NOT NULL,
    name VARCHAR(100) NOT NULL,
    description TEXT,
    default_value BOOLEAN DEFAULT false,
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE feature_flags.tenant_flags (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES tenants.tenants(id),
    flag_id UUID NOT NULL REFERENCES feature_flags.flags(id),
    value BOOLEAN NOT NULL,
    UNIQUE(tenant_id, flag_id)
);
```

### 6.2 Evaluacion de Flags

```typescript
// feature-flags.service.ts

async isEnabled(tenantId: string, flagKey: string): Promise<boolean> {
    // 1. Buscar override de tenant
    const tenantFlag = await this.getTenantFlag(tenantId, flagKey);
    if (tenantFlag !== null) return tenantFlag.value;

    // 2. Buscar valor default del flag
    const flag = await this.getFlag(flagKey);
    if (flag) return flag.default_value;

    // 3. Flag no existe
    return false;
}
```

---

## 7. Patrones de Extension SaaS

### 7.1 Extension de Billing

Las verticales pueden extender el sistema de billing agregando:

```typescript
// En vertical: erp-construccion

// Agregar producto de Stripe para servicios adicionales
await billingService.addOneTimeCharge(tenantId, {
    name: 'Cotizacion Premium',
    amount: 9900, // $99.00
    description: 'Generacion de cotizacion con IA'
});
```

### 7.2 Extension de Planes

Las verticales pueden definir features adicionales:

```sql
-- Feature especifica de construccion
INSERT INTO plans.plan_features (plan_id, feature_key, feature_value)
VALUES
    ('pro-plan-id', 'construction.budgets', '{"limit": 100}'),
    ('enterprise-plan-id', 'construction.budgets', '{"limit": -1}');
```

### 7.3 Extension de Webhooks

Las verticales pueden agregar eventos adicionales:

```typescript
// Registrar evento personalizado
webhookService.registerEvent('construction.budget.approved', {
    description: 'Presupuesto aprobado',
    payload_schema: BudgetApprovedPayload
});
```

---

## 8. Seguridad SaaS

### 8.1 Checklist de Seguridad

- [ ] RLS habilitado en todas las tablas multi-tenant
- [ ] Tokens JWT con expiracion corta (15 min)
- [ ] Refresh tokens con rotacion
- [ ] Rate limiting por tenant
- [ ] Webhooks firmados con HMAC
- [ ] Secrets encriptados en base de datos
- [ ] Audit log de acciones sensibles

### 8.2 Headers de Seguridad

```typescript
// 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
}));
```

---

## Referencias

- [VISION-ERP-CORE.md](VISION-ERP-CORE.md) - Vision general
- [INTEGRACIONES-EXTERNAS.md](INTEGRACIONES-EXTERNAS.md) - Integraciones
- [PostgreSQL RLS](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) - Documentacion oficial
- [Stripe Billing](https://stripe.com/docs/billing) - Documentacion oficial

---

*Actualizado: 2026-01-10*
