erp-core/docs/00-vision-general/ARQUITECTURA-SAAS.md
rckrdmrd 0086695b4c
Some checks failed
ERP Core CI / Backend Lint (push) Has been cancelled
ERP Core CI / Backend Unit Tests (push) Has been cancelled
ERP Core CI / Backend Integration Tests (push) Has been cancelled
ERP Core CI / Frontend Lint (push) Has been cancelled
ERP Core CI / Frontend Unit Tests (push) Has been cancelled
ERP Core CI / Frontend E2E Tests (push) Has been cancelled
ERP Core CI / Database DDL Validation (push) Has been cancelled
ERP Core CI / Backend Build (push) Has been cancelled
ERP Core CI / Frontend Build (push) Has been cancelled
ERP Core CI / CI Success (push) Has been cancelled
Performance Tests / Lighthouse CI (push) Has been cancelled
Performance Tests / Bundle Size Analysis (push) Has been cancelled
Performance Tests / k6 Load Tests (push) Has been cancelled
Performance Tests / Performance Summary (push) Has been cancelled
[SIMCO-V38] feat: Actualizar a SIMCO v3.8.0 + cambios backend
- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8
- Actualizaciones en modulos CRM y OpenAPI

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

14 KiB

id title type status version created_date updated_date
ARQUITECTURA-SAAS-ERP-CORE Arquitectura SaaS - ERP Core Architecture Published 1.0.0 2026-01-10 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

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

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

-- 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

// 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

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

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

-- 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

-- 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

// 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

// 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

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

// 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

-- 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

// 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:

// 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:

-- 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:

// 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

// 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


Actualizado: 2026-01-10