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
- 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>
559 lines
14 KiB
Markdown
559 lines
14 KiB
Markdown
---
|
|
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*
|