ConTodo ERP — Arquitectura de Solución SaaS Multi-Tenant (Rails + PostgreSQL)
ConTodo ERP — Arquitectura de Solución SaaS Cloud-Native Multi-Tenant
Propósito. Definir la arquitectura de referencia de ConTodo, un ERP SaaS multi-tenant para PYMEs y medianas empresas de Perú y LATAM (textiles, importadoras, comercializadoras, distribuidoras y manufactura ligera). Este documento es el contrato técnico que gobierna las decisiones de aislamiento de datos, escalabilidad, disponibilidad y seguridad sobre el stack objetivo Ruby on Rails + PostgreSQL + Redis + Sidekiq desplegado en AWS.
Disclaimer de arquitectura. Toda decisión aquí es reversible solo a un costo. El modelo de tenancy es la decisión menos reversible de todas: migrar de un modelo a otro tras tener cientos de tenants en producción es un proyecto de meses. Por eso se trata con el mayor rigor. Anti-overclaiming: ninguna arquitectura es "la correcta" en abstracto; se selecciona la que mejor equilibra el perfil real de ConTodo (multi-tenancy de PYMEs, compliance SUNAT, presupuesto de startup).
1. Contexto y supuestos explícitos
| # | Supuesto | Valor / Justificación |
|---|---|---|
| S1 | Tenant = persona jurídica (RUC) | Cada empresa cliente es un tenant. Un tenant tiene N sucursales, N almacenes, N usuarios. |
| S2 | Escala objetivo año 1–2 | 200–800 tenants, perfil PYME (5–50 usuarios c/u). Año 3+: 2.000–5.000 tenants. |
| S3 | Distribución de tamaño | Power-law: ~5% de tenants generan ~50% de la carga (medianas/distribuidoras con alto volumen de CPE). |
| S4 | Datos sensibles | RUC, planillas (PII de trabajadores), datos financieros, certificados digitales SUNAT. Compliance: Ley 29733 (Protección de Datos Personales Perú), futura adecuación a estándares LATAM. |
| S5 | Región | AWS us-east-1 o sa-east-1 (São Paulo) para latencia LATAM. Se evalúa sa-east-1 por residencia de datos. |
| S6 | SLA objetivo | 99.9% (≈8.76 h/año de downtime) para MVP; 99.95% en madurez. |
| S7 | RPO / RTO | RPO ≤ 5 min (PITR), RTO ≤ 1 h. Datos contables/tributarios son irrecuperables si se pierden. |
| S8 | Equipo inicial | 4–6 ingenieros. La complejidad operativa es una restricción de primer orden, no un detalle. |
2. Decisión nuclear: modelo de Multi-Tenancy
La pregunta que define todo: ¿cómo aislamos los datos de un tenant de los de otro en PostgreSQL? Existen tres patrones canónicos. Los comparamos en las dimensiones que realmente importan para un ERP.
2.1 Definición de los tres modelos
- Shared Schema (Discriminator Column / "Pool"). Una sola base de datos, un solo esquema. Todas las tablas tienen una columna
tenant_id. El aislamiento es a nivel de aplicación (y opcionalmente PostgreSQL Row-Level Security, RLS). - Separate Schema (Schema-per-Tenant / "Bridge"). Una sola base de datos PostgreSQL, pero un
schemapor tenant (tenant_acme.invoices,tenant_textil.invoices). PostgreSQL aísla mediantesearch_path. - Database-per-Tenant ("Silo"). Una base de datos PostgreSQL (o instancia RDS) por tenant. Aislamiento físico máximo.
2.2 Tabla comparativa detallada
| Dimensión | Shared Schema (tenant_id) | Separate Schema (schema/tenant) | Database-per-Tenant |
|---|---|---|---|
| Costo infraestructura | Muy bajo. Una RDS sirve a miles de tenants. ~US$ 0.20–2/tenant/mes a escala. | Bajo–medio. Una RDS, pero overhead de catálogo: PostgreSQL degrada con >5.000–10.000 schemas (pg_catalog explota). | Alto. Mínimo 1 conexión + overhead por DB; RDS factura por instancia. US$ 15–50+/tenant/mes salvo Aurora Serverless. |
| Aislamiento de datos | Débil por defecto (un WHERE mal escrito = data leak). Mitigable con RLS + default_scope. | Fuerte. Cross-tenant requiere cambiar search_path explícitamente. Un error de query no cruza schemas. | Máximo. Aislamiento físico. Imposible leak por bug de query. |
| Escalabilidad (nº tenants) | Excelente. Decenas de miles sin problema. | Limitada. PostgreSQL sufre con miles de schemas (migraciones, pg_dump, planner). Techo práctico ~3.000–5.000. | Limitada por gestión. Cada DB es un objeto a operar; >500–1.000 DBs es pesadilla operativa sin automatización fuerte. |
| Escalabilidad (volumen/tenant) | Riesgo: tablas gigantes compartidas. Mitigable con particionado por tenant_id y sharding por DB. | Buena: tablas por tenant son pequeñas e independientes. | Excelente. Cada tenant puede escalar/sizearse de forma independiente. |
| Complejidad operativa | Baja. Una migración = un rails db:migrate. Un backup. Un monitoreo. | Alta. Una migración debe correr en N schemas (N llamadas, transaccionalidad parcial, ventanas largas). | Muy alta. N migraciones, N backups, N conexiones, N upgrades. Requiere orquestación (Terraform/automation) madura. |
| Compliance / residencia | Difícil ofrecer "tus datos en tu propia DB/región". | Intermedio. | Ideal para clientes enterprise que exigen DB dedicada o región específica. |
| Noisy-neighbor | Alto riesgo. Un tenant pesado (reporte BI, importación masiva) degrada a todos. Mitigable con connection pooling, rate-limit, colas Sidekiq por prioridad. | Medio. Comparten CPU/IO de la misma instancia, pero las tablas están separadas (menos lock contention). | Nulo (instancia dedicada) o controlado (Aurora Serverless escala por tenant). |
| Migraciones / schema evolution | Trivial: 1 sola migración atómica. | Riesgosa: N ejecuciones; manejo de fallos parciales; tiempo lineal con nº de tenants. | Riesgosa y lenta: N DBs; ventanas de mantenimiento escalonadas. |
| Onboarding de tenant | Instantáneo (insert de fila + seed). | Medio (crear schema + correr migraciones del schema). | Lento (provisionar DB/instancia, migrar, conectar). Segundos a minutos. |
| Backup / restore selectivo | Difícil restaurar un solo tenant (hay que filtrar por tenant_id). | Medio (pg_dump -n schema). | Trivial (restaurar la DB del tenant). |
| "Borrar tenant" | DELETE WHERE tenant_id (lento, riesgoso). | DROP SCHEMA (limpio). | DROP DATABASE (limpísimo, cumple "derecho al olvido"). |
2.3 Veredicto por dimensión
3. Arquitectura recomendada para ConTodo
3.1 Decisión: Shared Schema + Row-Level Security (RLS), con camino a "Silo" para enterprise
Recomendamos un modelo híbrido por capas (Pooled-first, Silo-on-demand):
- Por defecto (99% de tenants PYME): Shared Schema reforzado. Una RDS PostgreSQL, todas las tablas con
tenant_id NOT NULL, PostgreSQL Row-Level Security activado como segunda barrera de aislamiento, ydefault_scope/middleware en Rails como primera barrera. - Tenants enterprise / con exigencia de residencia o aislamiento físico: se "gradúan" a su propia base de datos (Silo) usando el multiple databases nativo de Rails 6+ (
connects_to/connected_to). La misma codebase apunta a otra DB según el tenant.
3.2 Justificación
| Criterio | Por qué Shared Schema + RLS gana para ConTodo |
|---|---|
| Perfil de cliente | PYMEs textiles/comercializadoras: muchos tenants, cada uno relativamente pequeño. El pooled model fue diseñado exactamente para este caso (Salesforce, Shopify, Zoho operan así). |
| Equipo de 4–6 | Una migración, un backup, un dashboard. La complejidad operativa del Silo (N DBs) quebraría a un equipo pequeño. |
| Costo unitario | A 800 tenants, Database-per-Tenant en RDS costaría US$ 12.000–40.000/mes solo en instancias. El pooled cabe en 1–2 instancias RDS (US$ 800–3.000/mes). El margen SaaS depende de esto. |
| Onboarding instantáneo | Self-service signup requiere crear tenants en milisegundos. El Silo no lo permite sin pipeline de provisioning. |
| Aislamiento "suficiente" | RLS lleva el aislamiento de "depende del código" a "garantizado por el motor". Un bug de query no filtra datos si la policy RLS está activa. |
| Compliance Ley 29733 | RLS + cifrado en reposo (KMS) + auditoría satisface la protección de datos. El Silo se ofrece como upsell para quien lo exija contractualmente. |
3.3 Por qué NO Separate Schema (schema-per-tenant)
Aunque la gema apartment lo popularizó en Rails, lo descartamos como modelo primario:
- Techo de escala duro: PostgreSQL degrada el planner y
pg_dump/migraciones con miles de schemas; ConTodo apunta a 5.000+ tenants. - Migraciones O(N): cada
db:migraterecorre todos los schemas — ventanas de mantenimiento que crecen linealmente y fallan a la mitad. - Lo peor de ambos mundos: más complejidad que Shared, menos aislamiento que Silo. Es un óptimo local que ConTodo supera con RLS (aislamiento) + multiple-databases (silo on-demand).
3.4 Implementación del aislamiento (capas de defensa)
- Capa 1 (aplicación):
set_current_tenantenApplicationController,default_scope { where(tenant_id: Current.tenant_id) }víaActsAsTenanto concern propio. - Capa 2 (motor):
ALTER TABLE ... ENABLE ROW LEVEL SECURITY+ policyUSING (tenant_id = current_setting('app.current_tenant')::bigint). Defensa en profundidad: aunque la Capa 1 falle, el motor bloquea. - Particionado: las tablas de alto volumen (
comprobantes,kardex_movimientos,asientos_contables) se particionan porLIST/HASH (tenant_id)para contener noisy-neighbor y acelerar VACUUM.
4. Modelo C4 — Diagrama de Contexto (Nivel 1)
5. Modelo C4 — Diagrama de Contenedores (Nivel 2)
6. Escalabilidad
| Vector | Estrategia |
|---|---|
| Compute (API) | ECS Fargate con auto-scaling por CPU/latencia/RPS. Stateless: sesiones en Redis, archivos en S3. Escala horizontal sin límite práctico. |
| Workers | Sidekiq con colas por prioridad (critical, cpe, sire, reports, default, low) y escalado independiente. Aísla noisy-neighbor: un reporte BI pesado no bloquea la emisión de un CPE. |
| Base de datos (lectura) | RDS read replicas + Rails connected_to(role: :reading) para reportes/BI. |
| Base de datos (escritura) | Particionado por tenant_id en tablas calientes. Camino de evolución: sharding por DB (rango de tenants → shard) cuando una sola RDS no alcance. |
| Tenants enterprise | Promoción a Silo (RDS dedicada o Aurora Serverless v2) sin tocar la codebase, vía connects_to. |
| Cache | ElastiCache (Redis): fragment cache, query cache de catálogos (PCGE, tipo de cambio, tablas SUNAT) compartidos cross-tenant cuando son globales. |
7. Alta disponibilidad y resiliencia
| Componente | HA / DR |
|---|---|
| RDS | Multi-AZ (failover automático ~60–120 s). PITR (Point-in-Time Recovery) → RPO ≤ 5 min. Snapshots automáticos diarios + retención 35 días. |
| ECS | Tareas en ≥2 AZ tras el ALB. Health checks; despliegue blue/green (rolling sin downtime). |
| Redis | ElastiCache Multi-AZ con replica + auto-failover. |
| S3 | Durabilidad 99.999999999%; versioning + replicación cross-region para CPE/XML (obligación de conservar comprobantes 5 años — SUNAT). |
| DR cross-region | Replicación de snapshots RDS y S3 a región secundaria. RTO ≤ 1 h mediante runbook + IaC (Terraform). |
| Backups por tenant | Para Shared Schema, export lógico filtrado por tenant_id programado (Sidekiq) → S3, para soportar "exportar/eliminar mis datos" (Ley 29733). |
8. Seguridad (defensa en profundidad)
| Capa | Controles |
|---|---|
| Edge | CloudFront + AWS WAF (reglas OWASP Top 10, rate limiting, geo/IP), AWS Shield (DDoS). |
| Red | VPC privada; RDS y Redis en subredes privadas (sin IP pública); Security Groups mínimos; Bastion/SSM en vez de SSH. |
| Aplicación | JWT/sesión con expiración corta + refresh; CSRF; RBAC multirol (módulo Seguridad); rate-limit por tenant; validación de tenant_id en cada request. |
| Datos | Cifrado en reposo (RDS/S3/EBS con KMS), en tránsito (TLS 1.2+). Certificados digitales SUNAT en AWS Secrets Manager / KMS, nunca en DB plana. |
| Aislamiento tenant | RLS (sección 3.4) como garantía de motor; auditoría de cualquier acceso cross-tenant. |
| Auditoría | Tabla audit_logs append-only por tenant; CloudTrail; integración SIEM. Trazabilidad para compliance tributario. |
| Secretos / IAM | Roles IAM por servicio (least privilege); rotación de secretos; sin credenciales en código (OIDC para CI/CD). |
| Compliance | Ley 29733 (Perú); base para SOC 2 / ISO 27001 a futuro (logging, control de acceso, cifrado ya cubiertos por diseño). |
9. Riesgos y mitigaciones
| # | Riesgo | Severidad | Mitigación |
|---|---|---|---|
| R1 | Data leak cross-tenant por bug de query (Shared Schema) | Crítica | RLS en motor (no solo default_scope) + tests de aislamiento automáticos en CI + auditoría. |
| R2 | Noisy-neighbor: tenant pesado degrada a todos | Alta | Colas Sidekiq segregadas, particionado por tenant, rate-limit por tenant, read replicas para BI; promoción a Silo si reincide. |
| R3 | Tabla compartida gigante (kardex/CPE) frena VACUUM y queries | Alta | Particionado nativo PostgreSQL por tenant_id/fecha; archivado a S3/Glacier de periodos cerrados. |
| R4 | Migración futura a sharding dolorosa | Media | Diseñar tenant_id como clave de sharding desde el día 1; evitar IDs globales secuenciales que dificulten el split. |
| R5 | Lock-in AWS | Media | Stack portable (Rails/PG/Redis estándar); IaC en Terraform; servicios gestionados con equivalentes (RDS→Postgres, ElastiCache→Redis). |
| R6 | Pérdida de comprobantes (obligación SUNAT 5 años) | Crítica | S3 versioning + replicación cross-region + lifecycle a Glacier; nunca borrado físico de CPE. |
| R7 | Costo descontrolado del Silo enterprise | Media | Aurora Serverless v2 (escala a 0.5 ACU); pricing tier que cubra el costo dedicado. |
10. Oportunidades estratégicas
| # | Oportunidad | Valor |
|---|---|---|
| O1 | Tier "Dedicado/Soberanía de datos" (Silo) como upsell premium | Diferenciador vs Defontana/StarSoft para medianas reguladas; mayor ticket. |
| O2 | Pooled model = margen SaaS alto | Costo unitario bajo permite precio competitivo frente a SAP B1/NetSuite, ganando PYMEs por precio. |
| O3 | BI/IA sobre read replicas sin afectar transaccional | Analítica y forecasting de inventario/ventas como módulo de valor agregado. |
| O4 | Onboarding self-service instantáneo | El Shared Schema habilita signup→empresa operativa en minutos: motor de crecimiento PLG (Product-Led Growth). |
| O5 | Multi-región LATAM (sa-east-1 + expansión) | Cumplir residencia de datos por país abre mercados (Chile, Colombia) reusando la misma arquitectura. |
| O6 | Datos agregados anónimos (benchmarks de sector textil/comercio) | Producto de inteligencia de mercado sobre la base instalada, respetando privacidad. |
11. Decisiones de arquitectura (ADR resumido)
| ADR | Decisión | Alternativas descartadas | Estado |
|---|---|---|---|
| ADR-01 | Shared Schema + RLS como modelo de tenancy primario | Separate Schema (techo de escala), Database-per-Tenant (costo/op) | Aceptada |
| ADR-02 | Silo on-demand vía Rails multiple databases para enterprise | Forzar a todos a Silo | Aceptada |
| ADR-03 | ECS Fargate para compute stateless | EKS (overhead op para equipo de 6), EC2 puro | Aceptada |
| ADR-04 | Particionado por tenant_id en tablas calientes desde MVP | Tablas monolíticas | Aceptada |
| ADR-05 | RDS PostgreSQL Multi-AZ + read replicas | Aurora desde MVP (sobreingeniería inicial) | Aceptada (Aurora a evaluar en escala) |
| ADR-06 | Secretos/certificados SUNAT en KMS/Secrets Manager | Cifrado a nivel app en DB | Aceptada |
12. Conclusión
ConTodo debe nacer pooled (Shared Schema + RLS) para maximizar margen, simplicidad operativa y velocidad de onboarding — el perfil exacto de un SaaS de PYMEs LATAM —, y graduar a Silo a los pocos tenants enterprise que lo exijan, sin reescribir nada gracias a Rails multiple databases. Esta estrategia híbrida evita el falso dilema "todo compartido vs todo aislado": entrega el aislamiento garantizado por motor (RLS) que un ERP financiero requiere, conservando la economía de escala que hace viable competir en precio contra SAP Business One, NetSuite y Defontana. La decisión de tenancy se toma con disciplina porque es la menos reversible; el resto de la arquitectura (compute, cache, HA) sigue patrones cloud-native estándar, deliberadamente aburridos y probados, apropiados para un equipo pequeño que debe entregar compliance SUNAT sin distraerse en infraestructura exótica.