ConTodo ERP — Documento Técnico Consolidado: Stack, Arquitectura de Software, Estándares de Código, Testing, Observabilidad, Performance y ADRs
ConTodo ERP — Documento Técnico Consolidado
Propósito. Este es el contrato técnico único y vinculante de ConTodo, un ERP SaaS cloud-native multi-tenant para PYMEs y medianas empresas de Perú y LATAM (textiles, importadoras, comercializadoras, distribuidoras y manufactura ligera). Consolida los entregables de Arquitectura de Solución, Backend Rails, Frontend, DevOps/AWS y Ciberseguridad, y —de forma deliberada— reconcilia las contradicciones inter-documento que la revisión crítica (Debate 02) identificó. Donde los agentes individuales divergían, este documento decide. Es la fuente de verdad para Ingeniería, QA, Seguridad y Plataforma.
Disclaimer 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, equipo de 4–6, presupuesto de startup). Toda decisión es reversible solo a un costo; las menos reversibles —modelo de tenancy, tipo de clave de tenant, región— se tratan con el mayor rigor. No declaramos "compliance" sino readiness: un control diseñado no es un control con evidencia de operación.
0. Resumen ejecutivo y decisiones reconciliadas
La revisión adversarial (Debate 02) emitió un veredicto contundente: las piezas eran de calidad superior, pero el ensamble contenía 8 contradicciones (5 de severidad Alta), un modelo de costos que omitía IA y BI, y un riesgo de aislamiento cross-tenant no resuelto en la intersección de pooler + RLS. Este documento incorpora esas correcciones como decisiones cerradas.
| # | Contradicción detectada | Decisión consolidada (vinculante) |
|---|---|---|
| C1 | Región AWS (us-east-1 vs sa-east-1) | us-east-1 primaria para MVP y crecimiento (menor costo, latencia ~90 ms aceptable). sa-east-1 (São Paulo) se ofrece como SKU "Residencia LATAM" para Enterprise. DR en us-west-2. |
| C2 | Nombre de columna de tenant (tenant_id vs company_id) | Canónico: tenant_id. Único nombre en esquema, RLS, jobs, cache keys y headers (X-Tenant-Id). |
| C3 | Tipo de tenant_id (UUID vs BIGINT) | BIGINT para PK internas y tenant_id (índices y particionado eficientes); UUID solo para identificadores expuestos públicamente (slugs, links de comprobante, event_id). |
| C4/C6 | Bus de eventos y stack de BI (Redis vs SNS/EventBridge vs Kafka/MSK) | Un solo pipeline en MVP: Outbox transaccional → relay Sidekiq → EventBridge para integración. BI se alimenta del mismo outbox (batch). Kafka/MSK/Debezium se difieren a Fase 3 y solo si la latencia BI <5 min lo exige. |
| C5 | OpenAPI vs GraphQL / SSR | REST versionado + OpenAPI como contrato único. No hay GraphQL ni SSR. Frontend es SPA pura sobre CloudFront+S3. |
| C7 | Modelo de Silo Enterprise | Silo = RDS dedicada vía Rails connects_to, compartiendo el cluster ECS por defecto; ECS dedicado solo en el SKU "Aislamiento físico total". |
| C8 | Costo de IA ausente del modelo | Modelo de costos consolidado incluye líneas de IA (Bedrock managed) y BI. Margen de infra realista 65–75%, no >80%. |
| RT1 | Fuga cross-tenant vía pooler + RLS | Mandato: toda query corre en transacción con SET LOCAL; tests de aislamiento a través del pooler; validación de tenant_id tras cada checkout de conexión. Es el riesgo de seguridad nº 1. |
Principio rector unificado: "deliberadamente aburrido". Stack maduro, contratable en Perú/LATAM, sin servicios exóticos en el MVP. La complejidad operativa es una restricción de primer orden para un equipo de 4–6. Cada servicio gestionado adicional debe justificar su costo operativo, no solo su beneficio técnico.
1. Stack tecnológico completo
1.1 Tabla maestra de tecnologías
| Capa | Tecnología | Versión objetivo | Justificación |
|---|---|---|---|
| Lenguaje backend | Ruby | 3.3 (YJIT activo) | +15–25% throughput sin cambios de código; ecosistema maduro. |
| Framework backend | Ruby on Rails | 7.2 (modo API) | Productividad, transaccionalidad ACID, multiple databases nativo. |
| Modularización | packwerk (modular monolith) | última estable | Bounded contexts con dependencias enforced en CI. |
| Base de datos | PostgreSQL | 16 | RLS, particionado nativo, MERGE, paralelismo. |
| Cache / colas / sesiones | Redis | 7 | Broker Sidekiq, fragment cache, rate-limit, idempotency store. |
| Jobs asíncronos | Sidekiq | 7 | Colas por prioridad, retries, observabilidad. |
| Pool de conexiones | RDS Proxy / PgBouncer (modo transaction) | — | Multiplexa conexiones; obligatorio desde 100 tenants (con mandato RT1). |
| Autenticación | Devise + JWT (access/refresh) | — | Access 15 min + refresh rotatorio; MFA para admins. |
| Autorización | Pundit + Rolify (RBAC) | — | Policies testeables; RBAC módulo × sucursal × acción. |
| API | REST /api/v1 (JSON:API-like) + OpenAPI | OpenAPI 3.1 | Cacheable, integrable con SUNAT/bancos; contrato tipado FE. |
| Lenguaje frontend | TypeScript | 5 (strict) | Tipado extremo a extremo; cero any en CI. |
| Framework frontend | React | 18 | Estándar de mercado, ecosistema amplio. |
| Bundler / dev | Vite | última estable | HMR excelente, build a estáticos para S3/CloudFront. |
| Routing FE | React Router | v6 | Rutas anidadas, lazy por módulo, guards RBAC. |
| Server state | TanStack Query | v5 | Caché/sincronización con tenant_id en cada key. |
| Client state | Zustand | última | Session/tenant/UI/prefs; persistencia selectiva. |
| Estilos | Tailwind CSS + Radix UI (headless) | — | Control total de a11y y estilo; design system "Telar". |
| Validación FE | Zod + React Hook Form | — | Schemas derivados del dominio, sin drift cliente/servidor. |
| Gráficos | Recharts | — | Composable, accesible, ligero para módulo BI. |
| i18n | i18next + react-i18next + Intl | — | es-PE base; localización fiscal separada de UI. |
| Cómputo | AWS ECS Fargate | — | Stateless, sin gestión de nodos (vs EKS). |
| Base de datos gestionada | Amazon RDS PostgreSQL Multi-AZ | — | OLTP; Aurora a evaluar a escala. |
| Objetos | Amazon S3 (+ Intelligent-Tiering, Object Lock) | — | SPA, CPE/XML/CDR, backups; WORM para audit log. |
| CDN / Edge | CloudFront + ACM | — | TLS, cache SPA, PoP Lima. |
| Firewall L7 | AWS WAF + Shield Standard | — | OWASP, rate-limit, geo. |
| DNS | Route53 | — | Failover DR, health checks. |
| Cache gestionada | ElastiCache Redis (Multi-AZ) | — | Cluster mode desde ~1.000 tenants. |
| Secretos | AWS Secrets Manager + KMS | — | Credenciales, certificados SUNAT, rotación. |
| Bus de eventos (integración) | Amazon EventBridge / SNS | — | Destino del outbox relay. |
| IaC | Terraform | ≥ 1.7 | State en S3 + lock DynamoDB; policy-as-code (checkov). |
| CI/CD | GitHub Actions (OIDC) | — | Sin llaves estáticas; deploy gated a prod. |
| Registro de imágenes | Amazon ECR | — | Escaneo on-push. |
| Observabilidad | CloudWatch + X-Ray | — | Logs, métricas, trazas, alarmas, SLO. |
| IA (MVP) | Amazon Bedrock (managed) | — | Sin self-host GPU en MVP; Llama-VPC como add-on enterprise. |
1.2 Decisiones de stack descartadas y por qué
| Alternativa | Estado | Razón del descarte |
|---|---|---|
| Microservicios desde el día 1 | Descartado | Transacciones cross-módulo (venta→kardex→contabilidad) exigen ACID; complejidad operativa 5x sin demanda de escala. |
| Hanami/dry-rb | Descartado | Ecosistema menor, curva de equipo más alta. |
Schema-per-tenant (gema apartment) | Descartado | Techo de escala (~3–5k schemas), migraciones O(N), lo peor de ambos mundos. |
| Database-per-tenant universal | Descartado como default | Costo US$ 12k–40k/mes a 800 tenants; ofrecido solo como Silo Enterprise. |
| Next.js / Remix (SSR) | Descartado | App tras login: SEO irrelevante; SSR runtime es TCO y complejidad evitables. |
| GraphQL como entrada única | Pospuesto | N+1 y autorización por campo complican control fiscal; REST se cachea mejor. |
| EKS (Kubernetes) | Pospuesto | Overhead de control plane/nodos para equipo de 6; reconsiderar >2.000 tenants. |
| Kafka/MSK + Debezium en MVP | Diferido a Fase 3 | Contradice "stack aburrido"; +US$ 1.500–2.500/mes y ops; el outbox ya captura cambios. |
| Llama self-host (GPU) en MVP | Diferido | g5/p4 24/7 = US$ 1.000–6.000/mes; Bedrock managed cubre el MVP. |
| Cifrado por-tenant (BYOK/KMS) | Diferido | Miles de CMK = costo/ops; add-on enterprise "Soberanía". |
2. Arquitectura de software
2.1 Estilo arquitectónico
ConTodo es un Modular Monolith sobre Rails, organizado por bounded contexts (DDD), con comunicación interna por eventos de dominio + Outbox pattern transaccional, y exposición vía API REST versionada + webhooks firmados. La evolución a microservicios es selectiva y bajo demanda (un pack se extrae solo cuando su perfil de escala lo justifica), nunca prematura.
| Principio | Decisión | Justificación |
|---|---|---|
| Consistencia transaccional | Modular monolith con packs | El asiento contable derivado de una venta no puede vivir en una saga distribuida frágil. |
| Comunicación interna | Eventos asíncronos (outbox) salvo invariantes ACID | Desacopla módulos; invariantes (venta+kardex) se resuelven in-process en una TX. |
| Lecturas pesadas | CQRS ligero (vistas materializadas + read replica) | Reportes/EEFF no compiten por locks con la operación transaccional. |
| Contrato de API | REST /api/v1 + OpenAPI como fuente de verdad | Tipos FE generados; gate en CI si el spec cambia. |
| Aislamiento de tenant | Defensa en profundidad (app + RLS + storage + jobs) | Una sola barrera nunca es suficiente para un ERP contable. |
2.2 Mapa de bounded contexts
Reglas de dependencia (enforced por packwerk en CI):
SharedKernelno depende de nadie; todos dependen de él.- Operaciones y Finanzas nunca se referencian por clase; solo por eventos o published APIs (
Sales::PublicApi). - IAM/ORG son dependencias síncronas permitidas (tenant y usuario están en todo request).
- Un PR que introduce una dependencia no declarada falla el build.
2.3 Eventos de dominio y Outbox pattern (pipeline único)
El evento se persiste en la misma transacción que el cambio de estado; un relay Sidekiq lo publica después a EventBridge. Garantía at-least-once → consumidores idempotentes por event_id.
CREATE TABLE outbox_events (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
event_id UUID NOT NULL UNIQUE, -- idempotency key (público)
name TEXT NOT NULL, -- 'sales.sale_confirmed'
schema_ver INT NOT NULL DEFAULT 1,
aggregate TEXT NOT NULL, -- 'Sale#1234'
payload JSONB NOT NULL,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
published_at TIMESTAMPTZ
);
CREATE INDEX idx_outbox_unpublished ON outbox_events (id) WHERE published_at IS NULL;
Decisión de reconciliación (RT3/C4/C6): un solo mecanismo de captura de cambios en MVP — el outbox. BI consume el outbox (batch). No se introduce CDC/Debezium/Kafka mientras el batch satisfaga la latencia objetivo de BI (<5 min no es requisito de MVP). Esto elimina el "doble pipeline de eventos" y ~US$ 1.500–2.500/mes de infra prematura.
2.4 Modelo de tenancy (decisión menos reversible)
Shared Schema + PostgreSQL Row-Level Security (RLS), con Silo on-demand para Enterprise.
| Capa | Mecanismo | Falla segura |
|---|---|---|
| HTTP | Middleware resuelve tenant; rechaza request sin tenant_id. | 401/403 |
| ORM | Tenantable concern: scope implícito + asignación automática. | Excepción si falta Current.tenant_id |
| DB (RLS) | USING (tenant_id = current_setting('app.current_tenant')::bigint) | 0 filas (no fuga) |
| Storage | Prefijos S3 tenant/<id>/... + condiciones IAM | Acceso denegado |
| Background | TenantSetter.with(args[:tenant_id]) envuelve cada job | Job aborta sin tenant |
Mandato crítico RT1 (pooler + RLS). Con RDS Proxy/PgBouncer en modo
transaction, una variable de sesión mal scopeada puede filtrar contexto entre tenants. Reglas obligatorias:
- Toda query (lectura incluida) corre dentro de una transacción explícita.
- Se usa
SET LOCAL(transaccional), nuncaSETde sesión.- Tests de aislamiento corren a través del pooler, no solo contra Postgres directo.
- Validación de
app.current_tenanttras cada checkout de conexión. Sin esto validado y probado en CI, la promesa de aislamiento es papel. Es bloqueante de GA.
Particionado: tablas calientes (comprobantes, kardex_movimientos, asientos_contables) particionadas por HASH/LIST (tenant_id) y/o fecha desde el MVP. Para el ~5% de tenants pesados (power-law) cuyo particionado no resuelve noisy-neighbor, el disparador de promoción a Silo se define en §7.
3. Arquitectura frontend
3.1 Decisiones
SPA Vite + React 18 + TypeScript servida desde CloudFront + S3, organización feature-first (vertical slices), un slice por módulo del ERP. Separación estricta server state (TanStack Query) vs client state (Zustand).
| Principio | Decisión | Justificación |
|---|---|---|
| Velocidad percibida | Code-splitting por módulo (lazy routes) | El usuario abre la app una vez al día; navegar entre módulos debe ser instantáneo. |
| Estado servidor ≠ cliente | TanStack Query (server) + Zustand (UI/sesión) | Elimina ~80% de bugs de estado. |
| Tipado E2E | TS strict + tipos generados de OpenAPI | Errores fiscales (CPE/PLE) se detectan en compilación. |
| Multi-tenant en cliente | tenant_id en cada query key + queryClient.clear() al cambiar empresa | Evita fuga de datos en caché. |
| A11y | Radix headless + tokens accesibles | WCAG 2.1 AA; amplía mercado (sector público/licitaciones). |
3.2 Estructura y design system
src/
├── app/ # providers, router, guards (Auth/Tenant/Permission)
├── shared/ # ui (design system "Telar"), lib (apiClient, money), hooks, i18n, types (OpenAPI)
├── features/ # 18 slices: ventas, inventario, kardex, contabilidad, ...
└── stores/ # Zustand: session, tenant, ui, preferences
Regla de dependencias (ESLint boundaries): features/* → shared/*, nunca al revés; un feature no importa otro feature directamente.
Design system "Telar" sobre Radix + Tailwind, distribuido como @contodo/ui. Componentes de plataforma clave: <DataTable> (TanStack Table + virtualización para 10k+ filas), <FormBuilder> (Zod + React Hook Form, campos condicionales como tipo de cambio), <KpiCard>, <Money> (tabular-nums, nunca toFixed() suelto), <CompanySwitcher>, <RucLookup>, <CommandPalette> (Ctrl+K).
3.3 Multimoneda e i18n
| Concepto | Tratamiento UI |
|---|---|
| Moneda base (funcional, PEN) | Reportes consolidados, EEFF |
| Moneda del documento (PEN/USD) | Captura en formulario; aparece tipo de cambio si ≠ base |
| Moneda de presentación | Toggle en dashboards; conversión la calcula el backend (consistencia contable) |
i18n con i18next + Intl (no hardcodear separadores). Locale base es-PE; roadmap es-CO/CL/MX/EC + en-US. La localización fiscal (SUNAT, PLE, SIRE, detracción) vive en namespaces por país, separada de la traducción de UI.
Política fiscal innegociable: la emisión de comprobantes nunca usa optimistic update. El usuario ve estado "Procesando" explícito hasta el CDR de SUNAT. La integridad fiscal pesa más que la fluidez percibida.
4. Infraestructura cloud (AWS)
4.1 Diagrama de arquitectura
4.2 Red y seguridad de infraestructura
- VPC
10.0.0.0/16enus-east-1, 2 AZ (3 AZ desde 1.000 tenants). - Subnets públicas (ALB, NAT); privadas app (Fargate); privadas datos (RDS, Redis sin ruta a internet).
- VPC Endpoints (S3, ECR, Secrets Manager, CloudWatch) para reducir egreso por NAT.
- WAF managed rules + rate limit 2.000 req/5min/IP + geo-allow LATAM+EEUU; Shield Standard.
- Security Groups mínimos (ALB→web:3000, web→RDS:5432, web→Redis:6379).
- CloudTrail org-wide, GuardDuty, Security Hub, Config rules.
4.3 CI/CD
- Migraciones como ECS one-off task con advisory lock, siempre expand/contract (backward-compatible).
- Mandato de migración a escala (RT5): estrategia
pg-osc/expand-contract + runbook desde la primera tabla. A 5.000 tenants, unadd_indexsobre una tabla particionada de cientos de millones de filas es un evento de mantenimiento planificado, no unrails db:migratetrivial. - Deploy a prod gated (OIDC, sin llaves estáticas).
4.4 IaC, backups y DR
- Terraform ≥1.7, state en S3 + lock DynamoDB, módulos versionados,
prevent_destroyen RDS/S3,checkovpolicy-as-code. - RDS: snapshots diarios + PITR (retención 14 días) + copia cross-region a
us-west-2. - S3: versioning + CRR + lifecycle a Glacier para CPE/XML (SUNAT exige 5 años).
- RPO/RTO: Pyme RPO 1h / RTO 4h (Pilot Light); Enterprise RPO 5min / RTO 1h (Warm Standby).
- Game days trimestrales de failover.
4.5 Modelo de costos consolidado (reconciliado)
El modelo original de DevOps omitía IA y BI. Esta versión los incorpora (Debate 02 §2.3). Cifras
us-east-1~junio 2026, ±20%, a revalidar trimestralmente con factura real y prácticas FinOps.
| Servicio | MVP | 100 | 500 | 1.000 | 5.000 |
|---|---|---|---|---|---|
| ECS web | 60 | 180 | 520 | 950 | 3.800 |
| ECS Sidekiq (Spot) | 25 | 70 | 220 | 420 | 1.700 |
| RDS PostgreSQL Multi-AZ | 130 | 290 | 620 | 1.150 | 8.000–12.000 (sharded/Aurora) |
| RDS Read Replicas | 0 | 70 | 220 | 480 | 3.000–4.500 |
| ElastiCache Redis | 50 | 90 | 230 | 430 | 1.500 |
| S3 + CloudFront + transfer | 23 | 90 | 330 | 640 | 2.750 |
| ALB + NAT + Route53 + WAF + KMS + Secrets | 77 | 104 | 161 | 229 | 520 |
| CloudWatch + X-Ray | 15 | 40 | 110 | 200 | 700 |
| AWS Backup + CRR | 10 | 30 | 90 | 170 | 650 |
| IA (Bedrock managed, tokens) | 20 | 150 | 700 | 1.500 | 3.000–15.000 |
| BI (batch outbox; sin MSK en MVP) | 0 | 30 | 120 | 300 | 800 |
| Subtotal infra (rango medio) | ~410 | ~1.140 | ~3.300 | ~6.470 | ~30.000–42.000 |
| TOTAL con buffer 15% | ~470 | ~1.310 | ~3.800 | ~7.440 | ~35.000–48.000 |
| Costo por cliente/mes | ~94 | ~13 | ~7,6 | ~7,4 | ~7–10 |
Lectura: economías de escala reales (de ~US$ 94 a ~US$ 7–10/cliente), pero el margen de infra realista es 65–75% (no >80%) una vez costeados IA y BI —los diferenciadores que el negocio venderá. Si el pricing es US$ 25–40/cliente/mes, el margen sigue siendo sano. RDS es el mayor centro de costo (Graviton, Reserved Instances, Aurora Serverless v2 como palancas). Cola critical siempre On-Demand, nunca Spot (RT6: riesgo fiscal de job a medias en interrupción).
5. Seguridad
5.1 RBAC + ABAC multi-tenant
Unidad atómica de autorización: el permiso (recurso:acción), no el rol. Usuarios reciben roles dentro del scope de un tenant (y opcionalmente sucursal). Soporte ABAC selectivo (sucursal propia, monto ≤ X, horario).
| Rol base | Scope | Principio |
|---|---|---|
super_admin (plataforma) | Global | Sin acceso a datos de negocio en claro salvo break-glass auditado |
owner (tenant) | Tenant | Único que crea otros admins |
contador | Tenant | Registra, no aprueba pagos (SoD) |
tesorero | Tenant/sucursal | Aprueba pagos (SoD con contador) |
vendedor | Sucursal | ABAC: emite CPE de su sucursal, límite de descuento |
almacenero | Almacén | Movimientos kardex; ajustes valorizados requieren contador |
rrhh | Tenant | Calcula planilla; aprobación = owner/admin |
auditor | Tenant | Read-only total + audit log (para revisores Big Four) |
Segregación de funciones (SoD) codificada y validada en tiempo de asignación:
| Combinación prohibida | Riesgo |
|---|---|
compra:registrar + pago:aprobar | Pago a proveedor ficticio |
proveedor:crear + pago:aprobar | Proveedor fantasma + autopago |
planilla:calcular + planilla:aprobar | Pago indebido de remuneraciones |
asiento:registrar + periodo:cerrar | Ocultamiento de errores contables |
Enforcement: Pundit deny-by-default + scope where(tenant_id) + RLS como doble candado de DB.
5.2 Auditoría inmutable
Tabla audit_events append-only (revocado UPDATE/DELETE al rol app + trigger bloqueante + hash chain prev_hash para tamper-evidence). Réplica a S3 Object Lock (WORM, modo Compliance) + SIEM. Retención: 1 año caliente, 7 años Glacier (plazos tributarios). El hash chain detecta manipulación; el WORM la previene.
5.3 Cifrado y llaves
| Capa | Control |
|---|---|
| In-transit externo | TLS 1.3, CloudFront+ACM, HSTS, OCSP stapling |
| In-transit interno | RDS force_ssl, Redis TLS, mTLS en ECS donde aplique |
| At-rest DB/objetos | AES-256 KMS (CMK por dominio), S3 SSE-KMS + bucket key |
| Application-level | Active Record Encryption (AEAD) para PII/secretos: DNI, CUSPP, cuentas, claves SOL, certificados .pfx |
| Secretos | Secrets Manager, rotación automática, sin secretos en código/env claro |
5.4 STRIDE, OWASP y riesgos
Modelo STRIDE y controles OWASP Top 10 (2021) completos: A01 (Pundit deny-by-default + RLS + tests cross-tenant), A02 (TLS 1.3 + KMS + AR Encryption), A03 (AR parametrizado + escape React + CSP), A06 (Dependabot + bundler-audit + SCA + SBOM), A10 (allowlist de destinos para webhooks/SUNAT).
| ID | Riesgo | Nivel | Mitigación |
|---|---|---|---|
| R01 | Fuga cross-tenant (IDOR/scope) | Alto | RLS + Pundit + tests cross-tenant bloqueantes en CI |
| RT1 | Fuga cross-tenant vía pooler (SET multiplexado) | Crítica | SET LOCAL en TX + tests a través del pooler (§2.4) |
| R02 | Compromiso de certificado SUNAT/.pfx/clave SOL | Alto | Cifrado de campo + Secrets Manager + auditoría de uso |
| R03 | Exfiltración PII planillas (DNI/CUSPP) | Alto | AEAD + RBAC rrhh + masking en logs |
| R08 | Ransomware/pérdida de datos | Alto | PITR + snapshots inmutables + DR |
| R13 | Incumplimiento Ley 29733 (ARCO) | Alto | Registro de tratamiento, flujos ARCO, DPA subprocesadores |
Hueco operativo reconocido (Debate 02 §2.4): sostener SOC 2 Type II + ISO 27001 + pentest + bug bounty + ARCO con 1 Security Engineer es trabajo de 3–4 personas. Decisión: outsourcing explícito —vCISO + plataforma Vanta/Drata (automatiza ~60–70% de la evidencia)— presupuestado en el P&L.
5.5 Readiness SOC 2 / ISO 27001
Camino: política de seguridad → controles CC6–CC9 → Type I (~mes 6) → ventana de observación 6–12 meses → Type II. ISO 27001:2022 en paralelo (comparte ~80% de controles; palanca en licitaciones públicas peruanas). SoA sobre los 93 controles del Anexo A.
6. Estándares de código, testing, observabilidad y performance
6.1 Estándares de código
| Área | Estándar |
|---|---|
| Backend lint/format | RuboCop (config compartida), bloqueante en CI |
| Backend seguridad | Brakeman (SAST), bundler-audit, Dependabot |
| Modularización | packwerk valida dependencias entre packs; published APIs explícitas |
| Migraciones | strong_migrations + expand/contract + pg-osc desde día 1 |
| Frontend lint | ESLint + eslint-plugin-boundaries (reglas de dependencia entre features) |
| Frontend tipos | tsc --strict, cero any en CI |
| Contrato API | OpenAPI 3.1 como fuente de verdad; dueño designado del spec + proceso de breaking changes documentado (deprecación con header Sunset + 6 meses de solape) |
| Idempotencia | Idempotency-Key obligatorio en POST de mutación financiera (24h en Redis) |
| Errores API | RFC 7807 (application/problem+json) con code, detail, errors[] |
6.2 Estrategia de testing
| Capa | Backend | Frontend | Objetivo |
|---|---|---|---|
| Unit | RSpec (models, services, policies) | Vitest (hooks, formatters) | Lógica pura |
| Integración | RSpec request specs | Testing Library + jest-axe | Comportamiento + a11y |
| Aislamiento tenant | Test cross-tenant por modelo Tenantable, a través del pooler | tenant_id en query keys | Bloqueante de merge (R01/RT1) |
| Contrato | Validación OpenAPI ↔ tipos FE en CI | openapi-typescript con gate | Sin drift |
| E2E | — | Playwright (emisión CPE, login, kardex) | Flujos críticos |
| Seguridad | Brakeman, bundler-audit, Trivy | npm audit | SAST/SCA |
| Visual | — | Storybook + Chromatic | Regresión del design system |
| Cobertura | ≥ 80% | ≥ 80% | Gate en CI |
6.3 Observabilidad
- Logs centralizados CloudWatch (Fargate
awslogs), retención 30 días + export a S3/Athena. - Métricas custom: latencia de colas Sidekiq, tiempo de facturación electrónica por tenant, errores SUNAT, p95 por endpoint, lag del outbox relay (
now()-occurred_at), medidor de tokens/costo IA por tenant (integrado a CloudWatch, no aislado). - Trazas: AWS X-Ray cross-service.
- Alarmas: 5xx > 1%, p95 > 800 ms, CPU RDS > 80%, cola Sidekiq > 5 min, free storage RDS < 15%, lag outbox > 60s → SNS → Slack/PagerDuty.
- SLO: 99,9% disponibilidad mensual; dashboards por dominio (App, DB, Colas, Costos, Margen por tenant).
6.4 Performance
| Vector | Estrategia / presupuesto |
|---|---|
| Throughput backend | YJIT activo; ECS auto-scaling por CPU 60% + RequestCountPerTarget |
| Conexiones DB | RDS Proxy obligatorio desde 100 tenants (con mandato SET LOCAL) |
| Lecturas pesadas | CQRS: read replica + vistas materializadas (kardex valorizado, EEFF) |
| Locks kardex | Advisory lock por producto en escritura serializada |
| Overhead de RLS | Benchmark obligatorio (Debate 02 §2.2.1): índices alineados con current_setting; objetivo p95 ≤ 300 ms en tablas particionadas a escala |
| Bundle FE | Shell+auth+dashboard ≤ 250 KB gzip; cada módulo lazy ≤ 150 KB; Lighthouse ≥ 90 |
| DataTable | Virtualización (TanStack Virtual) para 10k+ filas; paginación server-side |
| Web Vitals | Reportados a BI para monitoreo continuo |
7. ADRs resumidos (registro de decisiones)
| ADR | Decisión | Alternativas descartadas | Estado |
|---|---|---|---|
| ADR-01 | Shared Schema + RLS como tenancy primario | Schema-per-tenant (techo escala), DB-per-tenant universal (costo) | Aceptada |
| ADR-02 | Silo on-demand vía Rails connects_to (RDS dedicada, ECS compartido) | Forzar todos a Silo; ECS dedicado por defecto | Aceptada |
| ADR-03 | Modular monolith + DDD + packwerk | Microservicios prematuros | Aceptada |
| ADR-04 | Outbox transaccional → EventBridge como pipeline único de eventos | Doble pipeline (outbox + CDC/Kafka); publicar directo al broker | Aceptada |
| ADR-05 | ECS Fargate stateless | EKS (overhead), EC2 puro | Aceptada |
| ADR-06 | RDS PostgreSQL Multi-AZ + read replicas; Aurora a escala | Aurora desde MVP | Aceptada |
| ADR-07 | tenant_id BIGINT interno; UUID solo público | UUID para PK; nombre company_id | Aceptada (reconcilia C2/C3) |
| ADR-08 | Región us-east-1 primaria; sa-east-1 como SKU residencia; DR us-west-2 | sa-east-1 primaria | Aceptada (reconcilia C1) |
| ADR-09 | REST + OpenAPI como contrato único; sin GraphQL ni SSR | GraphQL entrada única; SSR | Aceptada (reconcilia C5) |
| ADR-10 | Mandato SET LOCAL en TX + tests a través del pooler | SET de sesión con RDS Proxy | Aceptada (mitiga RT1) |
| ADR-11 | IA = Bedrock managed en MVP; Llama-VPC como add-on enterprise | Multi-proveedor + self-host GPU en MVP | Aceptada (reconcilia C8) |
| ADR-12 | BI = batch sobre outbox en MVP; CDC/Kafka diferido a Fase 3 con disparador | MSK/Debezium/Redshift desde MVP | Aceptada (reconcilia C6) |
| ADR-13 | Cola critical On-Demand, Spot solo para reports/ai | Todos los workers en Spot | Aceptada (mitiga RT6) |
| ADR-14 | Particionado por tenant_id desde MVP + ADR de sharding con métrica gatillo antes de 1.000 tenants | Diferir sharding sin disparador ni dueño | Aceptada (mitiga RT7) |
| ADR-15 | Secretos/certificados SUNAT en KMS/Secrets Manager; AEAD para PII | Cifrado app en DB plana | Aceptada |
| ADR-16 | Seguridad operada con vCISO + Vanta/Drata | 1 Security Engineer sostiene todo | Aceptada (mitiga hueco §5.4) |
7.1 Disparadores explícitos (lo que no se difiere "al futuro")
| Decisión diferida | Métrica gatillo | Dueño (R) / Aprobador (A) |
|---|---|---|
| ADR de sharding por cohorte | >70% CPU sostenido en RDS al máximo de su clase | R: Solution Architect / A: CTO — antes de 1.000 tenants |
| Migración a Aurora | RDS escalado al tope + variabilidad de carga alta | R: DevOps / A: CTO |
| Introducir CDC/Kafka (BI) | Requisito de latencia BI <5 min confirmado por negocio | R: Data / A: CTO |
| Promoción tenant shared→silo | Tenant en el ~5% pesado con noisy-neighbor reincidente, o contrato de residencia/aislamiento | R: SRE+Architect / A: CTO |
| Contratar SRE dedicado + 3 AZ | 500 tenants | R: Eng Manager / A: CTO |
8. Roadmap técnico incremental
- Fase 0 — Cimientos: SharedKernel (tenancy, RLS, Money, EventBus), IAM, Organization, API v1 esqueleto, CI con packwerk + strong_migrations + tests de aislamiento a través del pooler, OpenAPI spec con dueño.
- Fase 1 — Núcleo operativo: Inventory/Kardex, Purchasing, Sales con outbox y subscribers contables.
- Fase 2 — Núcleo financiero: Accounting (PCGE), Treasury, SUNAT (SIRE/PLE), facturación electrónica.
- Fase 3 — Verticales + escala: Manufacturing, Imports, Logistics, Payroll/RRHH; evaluar CDC/Kafka, sharding, Aurora según disparadores.
- Fase 4 — Inteligencia: BI (vistas materializadas + read replica), IA (Bedrock: forecast/anomalías/copiloto) consumiendo el changelog del outbox.
9. Conclusión
ConTodo nace pooled (Shared Schema + RLS) para maximizar margen, simplicidad operativa y onboarding —el perfil exacto de un SaaS de PYMEs LATAM— y gradúa a Silo los pocos tenants enterprise que lo exijan, sin reescribir código. La combinación modular monolith + DDD + outbox transaccional + RLS entrega la integridad contable que un ERP exige y el aislamiento que un SaaS demanda, con una ruta de evolución selectiva a microservicios.
A diferencia de los entregables individuales —de calidad superior pero escritos en paralelo sin contrato maestro—, este documento resuelve las contradicciones: una sola región, un solo nombre y tipo de tenant_id, un solo pipeline de eventos, un solo contrato de API, y un modelo de costos consolidado que incluye IA y BI (margen realista 65–75%, no >80%). Trata el riesgo dominante —fuga cross-tenant en la intersección de pooler + RLS— como bloqueante de GA con mandato técnico y verificación automatizada. El stack es deliberadamente aburrido, probado y operable por un equipo de 4–6, con disparadores explícitos —no "al futuro"— para cada decisión de escala diferida. La arquitectura pasó de ser excelente en sus piezas a ser coherente en su ensamble.