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