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>
14 KiB
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
- VISION-ERP-CORE.md - Vision general
- INTEGRACIONES-EXTERNAS.md - Integraciones
- PostgreSQL RLS - Documentacion oficial
- Stripe Billing - Documentacion oficial
Actualizado: 2026-01-10