Saltar al contenido
Casos de ingeniería
En desarrollo2026FullstackSaaS

SIGEDReino

Ecosistema SaaS multi-tenant que sustituye ocho herramientas sueltas por un solo sistema

Plataforma de gestión para organizaciones con sedes distribuidas: personas, membresías, tesorería, eventos y comunicación en un mismo dominio. Diseñé la arquitectura completa —backend hexagonal, persistencia políglota, control de acceso en tres capas— y las tres aplicaciones cliente que la consumen.

Mi rol
Arquitecto de software y desarrollador principal
Cliente
Proyecto propio
20
Módulos de dominio
3
Aplicaciones cliente

web admin, portal público y móvil

3
Idiomas en lockstep

es · en · pt, verificados por test

6
Reglas de frontera automatizadas

fallan el build, no el code review

Contexto#

Las organizaciones con varias sedes acaban operando sobre una colección de herramientas que no se hablan entre sí: una hoja de cálculo para las personas, otra para las cuentas, un grupo de WhatsApp para coordinar, y la memoria de quien lleva más años para todo lo demás.

El problema no es la falta de software. Es que cada dato vive en un sitio distinto y nadie sabe cuál es el bueno. Cuando alguien pregunta cuántas personas hay activas, o cuánto se recaudó el mes pasado, la respuesta depende de a quién se le pregunte.

SIGEDReino nace para que exista una sola fuente de verdad, con las particularidades de este dominio: varias organizaciones independientes sobre la misma plataforma, jerarquías internas que determinan quién ve qué, y dinero de por medio, que no admite aproximaciones.

Restricciones#

  • Multi-tenant desde el día uno. No es una funcionalidad futura: cada organización debe ser incapaz de ver datos de otra, y eso condiciona el modelo de datos y el de seguridad desde la primera línea.
  • Tres clientes, un solo dominio. Panel administrativo, portal público y app móvil. Mantener tres definiciones del mismo concepto era garantía de que divergirían.
  • Equipo pequeño. Las decisiones que exigen operación continua —bases que administrar, infraestructura que vigilar— se pagan en tiempo que no se dedica a producto.
  • Dominio que todavía se está entendiendo. Varios módulos cambiaron de forma durante la construcción, así que la arquitectura tenía que tolerar que el negocio cambiara de opinión.

Arquitectura#

Monorepo con pnpm y Turborepo. Un backend NestJS con arquitectura hexagonal por módulo, tres aplicaciones cliente, y paquetes compartidos que llevan el contrato y las primitivas de dominio.

Cada módulo del backend tiene la misma estructura, y una única regla la sostiene: domain/ no importa nada, application/ importa domain/, infrastructure/ importa ambas. Nunca al revés.

Ejecución#

Veinte módulos de dominio —personas, membresías, tesorería, gobierno, eventos, notificaciones, facturación, entre otros—, cada uno con sus entidades, sus casos de uso y sus códigos de error propios.

Los casos de uso devuelven un Result en lugar de lanzar excepciones. Una regla de negocio incumplida no es un error del programa, es un resultado posible; y tratarla como excepción obliga a envolver medio código en try/catch para distinguir "el gasto no estaba pendiente" de "la base de datos se cayó".

Los tres clientes consumen el mismo api-contracts, así que renombrar un campo rompe su build en el mismo commit. Con una trampa que costó encontrar: el backend externaliza ese paquete al empaquetar, de modo que puede importar sus tipos pero no sus valores en tiempo de ejecución. Los enums que necesita viven duplicados en su dominio, con un test que falla si las dos copias divergen.

La interfaz sale de un design system propio: uno para web sobre Radix y Tailwind, otro nativo sobre NativeWind. No comparten componentes —no pueden— pero sí tokens, escala tipográfica y nombres de variantes.

Resultado#

El sistema está en desarrollo activo, con los módulos de identidad, personas, membresías y tesorería operativos en entorno de pruebas.

Lo que ya se puede afirmar es estructural: veinte módulos con fronteras verificadas por herramienta, tres aplicaciones que comparten dominio sin duplicarlo, y tres idiomas cuya paridad valida un test en cada ejecución. Las cifras de uso llegarán cuando el sistema entre en producción, y este caso se actualizará entonces con datos reales en lugar de proyecciones.

Arquitectura del sistema

Decisiones de arquitectura

  1. Persistencia políglota en lugar de una sola base de datos

    Lo que elegí
    Firestore para identity y personas, PostgreSQL para tesorería
    Lo que descarté
    • Solo PostgreSQL
    • Solo Firestore
    Por qué
    Los dos módulos tienen patrones de acceso opuestos. Un perfil de persona es un documento de forma variable que se lee por identificador y cambia de esquema cada pocos meses; un movimiento de tesorería exige transacciones ACID, integridad referencial y agregaciones exactas. Forzar tesorería en documentos significa reimplementar a mano lo que un motor relacional lleva cuarenta años resolviendo, y cada error ahí es dinero mal contado. Firestore además aporta reglas de seguridad en el borde, que en multi-tenant es la diferencia entre confiar en que ningún endpoint olvide filtrar y que sea imposible leer datos de otra organización.
    Lo que acepté perder
    Dos modelos mentales, dos estrategias de respaldo, y ninguna transacción que cruce ambas: la sincronización va por eventos de dominio con consistencia eventual, que hay que decidir explícitamente dónde se tolera.
  2. Control de acceso en tres capas en vez de roles planos

    Lo que elegí
    Capacidad del tenant + rango + perfil funcional, componibles y evaluadas en ese orden
    Lo que descarté
    • Roles planos por usuario
    • Permisos granulares por recurso
    Por qué
    Los roles planos no distinguen tres preguntas que en este dominio son distintas: si la organización tiene contratado el módulo, si la persona tiene autoridad suficiente, y si ejerce esa función concreta. Mezclarlas en un solo campo obliga a inventar roles compuestos que se multiplican con cada combinación nueva. Los permisos granulares por recurso resolvían el caso pero producían una matriz que nadie del negocio era capaz de auditar.
    Lo que acepté perder
    Tres comprobaciones por endpoint en vez de una, y un documento de control de acceso que hay que mantener al día: cuando se añade un módulo, decidir qué capas aplican es trabajo explícito y no un valor por defecto.
  3. Cross-module únicamente mediante eventos de dominio

    Lo que elegí
    Ningún módulo importa internals de otro; se comunican con eventos y handlers
    Lo que descarté
    • Inyección directa de servicios entre módulos
    • Un módulo compartido con la lógica común
    Por qué
    Con veinte módulos, permitir imports directos convierte el backend en un grafo que nadie puede razonar a los seis meses: cambiar tesorería obliga a leer membresías. Los eventos dejan al emisor sin conocimiento de sus consumidores, y dependency-cruiser rechaza el import directo con severidad de error, así que la frontera no depende de que alguien la recuerde.
    Lo que acepté perder
    Seguir un flujo completo exige saltar entre emisor y handler en archivos distintos, y aparece consistencia eventual donde antes había una llamada síncrona. Depurar cuesta más; entender el sistema entero, mucho menos.

Stack por capa

Backend
NestJSTypeScriptArquitectura hexagonalDDDEventos de dominioJSend
Datos
PostgreSQLPrismaFirestoreCloudflare R2
Frontend
React 19Next.jsViteTanStack QueryZustandTailwindRadix UI
Móvil
React NativeExpoNativeWind
Infraestructura
Firebase AuthDockerGitHub Actions
Herramientas
pnpmTurborepodependency-cruiserVitestJestStorybook

Qué haría distinto hoy

Empezaría por el control de acceso, no por los módulos de negocio. Lo construí cuando ya había varios módulos funcionando, y adaptarlos costó más que haberlo diseñado primero: cada endpoint existente hubo que revisarlo uno por uno decidiendo qué capas le aplicaban. También automatizaría antes las reglas de frontera — durante las primeras semanas fueron un acuerdo, y en ese periodo se colaron imports cruzados que después hubo que deshacer. La lección se repite: la regla que no rompe el build no existe.

¿Tienes un sistema por construir?

Cuéntame qué necesitas resolver. Respondo con un diagnóstico honesto: si no soy la persona indicada, te lo digo.

Hablemos de tu sistema