deploya

Documento 2 de 4 · Documento de diseño

Documento de diseño

Arquitectura defendida decisión por decisión: monolito modular hexagonal, modelo C4, UML por módulo, SOLID y patrones, modelo entidad-relación y diseño de la base de datos.

01RequisitosQué hace y para quién
02DiseñoCómo está hecho y por qué
03Manual técnicoCómo se instala y opera
04Manual de usuarioCómo se usa
Institución
Facultad de Ingeniería
Ingeniería en Informática y Sistemas
Curso
Ingeniería de Software I · Proyecto final 2026
Entrega
Presentación final · 13 de noviembre de 2026
Repositorio
github.com/derekCmorales/deploya

Equipo

  • Eddy PorojCuentas, seguridad y notificaciones (M1, M10)
  • Javier RodríguezSuscripciones, pagos y administración (M2, M9)
  • Eduardo PortilloProyectos, observabilidad y design system (M3, M7)
  • Derek Calderón MoralesMotor de despliegue, arquitectura e infraestructura (M4–M6)
Versión
1.1
Fecha
13 de noviembre de 2026
Estado
Presentación final
Fuente
docs/sitio/doc/02-deploya-diseno.html

Contenido

  1. 01Arquitectura general4
  2. 1.1Resumen de la decisión4
  3. 1.2Contexto (C4 nivel 1)5
  4. 1.3Contenedores (C4 nivel 2)6
  5. 1.4El ciclo de despliegue6
  6. 02Arquitectura de cada módulo8
  7. 2.1Puertos y adaptadores9
  8. 2.2Módulos y contratos entre ellos11
  9. 03El motor de despliegue13
  10. 3.1Componentes del motor (C4 nivel 3)13
  11. 3.2El pipeline como cadena de pasos13
  12. 3.3Máquina de estados del despliegue16
  13. 3.4Detección de stack18
  14. 04Diagramas UML19
  15. 4.1Clases · Identidad y notificaciones19
  16. 4.2Clases · Suscripciones y administración20
  17. 4.3Clases · Proyectos y observabilidad21
  18. 4.4Secuencia · Recorrido completo22
  19. 4.5Secuencia · Contratación de un plan23
  20. 4.6Secuencia · Despliegue24
  21. 4.7Secuencia · Reversión sin reconstruir25
  22. 4.8Estados · Suscripción26
  23. 4.9Actividad · Registro y verificación27
  24. 4.10Actividad · Contratación y activación28
  25. 05SOLID aplicado29
  26. 5.1Clean code29
  27. 06Patrones de diseño30
  28. 07Modelo de datos32
  29. 7.1Modelo entidad-relación33
  30. 7.2Decisiones de diseño de la base35
  31. 7.3Diccionario de datos35
  32. 7.4M1 Identidad (Eddy)36
  33. 7.5M2 Suscripciones (Javier)37
  34. 7.6M9 Administración (Javier)39
  35. 7.7M3 Proyectos (Eduardo)39
  36. 7.8M4 · M5 · M6 Motor (Derek)40
  37. 7.9M5 · Recursos de datos (Derek)43
  38. 7.10Dominios enumerados44
  39. 08Seguridad por diseño46
  40. 09Infraestructura y despliegue47
  41. 9.1Diagrama de despliegue48
  42. 9.2Entornos49
  43. 10Diseño de la interfaz50
  44. 11Decisiones de arquitectura51
  45. 12Calidad y pruebas52

Arquitectura general

Resumen de la decisión

Deploya es un monolito modular con arquitectura hexagonal dentro de cada módulo, desplegado como cinco servicios de docker compose en una sola EC2 de AWS creada con Terraform. La decisión completa, con alternativas y consecuencias, está en docs/arquitectura.md; el piloto en AWS, en el ADR 0009. En una línea: un solo sistema fácil de operar y de instalar, con fronteras internas tan claras como las de un sistema distribuido, y un dominio que se prueba sin infraestructura.

Atributo de calidad que mandaCómo lo atiende la arquitectura
Aislamiento y seguridad de las apps de los clientesLa API no toca Docker: solo el trabajador monta docker.sock. Cada app corre sin privilegios, con límites de CPU y memoria y en su propia red
Disponibilidad de lo que ya está publicadoConmutación sin corte: la ruta se mueve al contenedor nuevo solo después de que pasa la verificación de salud
Capacidad de respuesta del panelConstruir tarda minutos: va por una cola (BullMQ) a un proceso aparte, y la API responde al encolar
Mantenibilidad y pruebasPuertos como abstract class y adaptadores intercambiables: 1 250 pruebas corren sin Docker, red, base ni reloj reales
Portabilidad (autoalojado)Todo es configuración: el mismo código corre en local, en AWS y en el servidor de un tercero
Costo en un nodo de 8 GBDos procesos Node en vez de diez servicios; la memoria queda para las apps de los clientes

Contexto (C4 nivel 1)

Deploya se ubica entre dos actores humanos y cinco sistemas externos. El cliente solo usa el navegador; la plataforma traduce sus acciones en operaciones sobre GitHub, Docker, el DNS, la autoridad certificadora y el correo.

Sistemas externos

Actores

panel web, HTTPS

panel web, HTTPS

visita su app
Unsupported markdown: link

lee archivos y clona

construye y corre contenedores

resuelve subdominios

desafío DNS-01

verificación y recuperación

Cliente
despliega sus aplicaciones

Administrador
ve usuarios y suspende cuentas

Deploya
PaaS de un solo nodo
panel, API y motor de despliegue

GitHub
repositorios públicos

Docker Engine
nodo único (VPS)

DNS comodín
*.deploya.larasolutions.dev

Let's Encrypt
certificado comodín

Proveedor SMTP
Mailpit en desarrollo

C4 Nivel 1 — Contexto de Deploya (núcleo v4.1)
Figura 1. C4 Nivel 1 — Contexto de Deploya (núcleo v4.1)

Contenedores (C4 nivel 2)

ContenedorTecnologíaResponsabilidadHabla con
webNext.js 15 · React 19 · Tailwind 4Panel del cliente y del administrador con el design system v4.1. Sondea cada 3 s lo que está en cursoapi (HTTPS + cookie)
apiNestJS 10 · Prisma 6Casos de uso de M1, M2, M3, M7, M9 y M10; produce trabajos de M4 y M5. No habla con Dockerpostgres, redis, GitHub, SMTP
workerMisma imagen de la API (trabajador.ts)Consume las colas: clona, construye, ejecuta, publica, muestrea métricasredis, postgres, Docker, Traefik
postgresPostgreSQL 16Almacén único de la plataforma—
redisRedis 7Colas BullMQ (despliegues, operación, muestreo) y señales de cancelación—
traefikTraefik v3.3Enrutador de borde: rutas por archivo, TLS comodín, redirección a HTTPScontenedores de clientes
mailpitMailpit (solo desarrollo)Atrapa los correos para verlos en el navegador—

Deploya — monolito modular + trabajador

HTTPS

REST JSON, cookie de sesión

encola TrabajoDespliegue

lee archivos (11a)

SMTP

SMTP en el VPS

consume

estado, etapas, bitácora, artefactos

git clone --depth 1

build, run, stop

escribe rutas dinámicas

http(s)://proyecto.dominio

red del proyecto

Cliente / Administrador

Visitante de una app desplegada

web
Next.js · design system v4.1
polling cada 3 s

api
NestJS · M1 M2 M3 M7 M9 M10
+ productor de M4
no habla con Docker

worker
misma imagen de la API
M4 construye · M5 ejecuta · M6 publica

redis
cola BullMQ «despliegues»

postgres
Prisma · schema del núcleo

traefik v3
proveedor de archivo
*.localhost / *.deploya.larasolutions.dev

mailpit
SMTP de desarrollo · :8025

Docker Engine
/var/run/docker.sock

Contenedores de clientes
límites del plan · red por proyecto

GitHub

Proveedor SMTP (VPS)

C4 Nivel 2 — Contenedores de Deploya (núcleo v4.1, docker compose)
Figura 2. C4 Nivel 2 — Contenedores de Deploya (núcleo v4.1, docker compose)

El ciclo de despliegue

Todo el producto gira alrededor de un ciclo de cinco etapas. Cada una tiene un módulo responsable y un paso del pipeline del trabajador.

EtapaMóduloPasoQué produce
RecepciónM4PasoRecepcionClon de la rama, commit registrado, stack detectado
ConstrucciónM4PasoConstruccionImagen deploya/<subdominio>:<n> y su artefacto (digest, tamaño, receta)
EjecuciónM5PasoEjecucionContenedor con los límites del plan que pasa la salud
EnrutamientoM6PasoEnrutamientoRuta de Traefik al contenedor nuevo; el anterior se retira después
OperaciónM5 / M7PasoOperacionDespliegue Saludable, versión activa actualizada, oyentes avisados

Una reversión usa el mismo pipeline con otro plan: arranca en Revirtiendo y omite Recepción y Construcción, porque la imagen ya existe.

Arquitectura de cada módulo

Puertos y adaptadores

Dentro de apps/api/src/modules/<módulo>/ el código se ordena en cinco anillos. Las flechas de dependencia apuntan siempre hacia dentro: el dominio no conoce a nadie y los adaptadores conocen solo el puerto que implementan.

llama

decide con

depende solo de

implementan

hablan con

Entrada · adapta
DesplieguesController
AccionesDespliegueController
ReversionController
consumirDespliegues() del trabajador
SesionGuard · RolGuard

Aplicación · orquesta
ConstruccionService
ReversionService
AccionesDespliegueService
PipelineDespliegue y sus cinco pasos

Dominio · decide
TypeScript puro, sin Nest ni Prisma
TransicionesDespliegue
PoliticaAcciones · PoliticaDespliegue
PoliticaRetencion · RecetaStack

Puertos · contratos
ColaConstruccionPuerto
RepositorioDespliegues · RepositorioArtefactos
ClonadorRepositorioPuerto · ConstructorImagenPuerto
ContenedorPuerto · VerificacionEntornoPuerto
EnrutamientoPuerto · CertificadoPuerto

Adaptadores · hablan con el exterior
ColaBullMq · ColaMemoria
RepositorioDesplieguesPrisma · …Memoria
ClonadorGit · ConstructorDocker
ContenedorDocker · VerificacionHttp
EnrutamientoTraefikArchivo · CertificadoAcmeArchivo

Exterior
PostgreSQL · Redis
git · Docker Engine
Traefik · Let's Encrypt

Figura 3. Arquitectura hexagonal dentro de cada módulo — ejemplo real del motor (M4–M6)
CapaQué vive ahíPuede depender deNunca depende de
EntradaControladores HTTP, consumidores de cola, guards, filtros de erroresEl servicio de su móduloPrisma, adaptadores
AplicaciónServicios que orquestan un caso de uso; el pipeline del motorDominio y puertosAdaptadores concretos
DominioEntidades, objetos valor, políticas y máquinas de estadoSolo TypeScriptNest, Prisma, Docker, HTTP, Date.now()
PuertosContratos que la aplicación necesita, como abstract classTipos del dominioImplementaciones
AdaptadoresPrisma, Docker, BullMQ, Traefik, SMTP, GitHub, memoria y stubsEl puerto que implementanOtros módulos

Un puerto real del motor, tal como está en el código:

/** Puerto DIP — la API no llama a Docker. Sin prefijo I. */
export abstract class ContenedorPuerto {
  abstract crear(espec: EspecContenedor): Promise<ContenedorCreado>;
  abstract iniciar(contenedorId: string): Promise<void>;
  abstract detener(contenedorId: string): Promise<void>;
  abstract eliminar(contenedorId: string): Promise<void>;
  abstract eliminarContenedoresDe(subdominio: string): Promise<void>;
  abstract eliminarImagen(imagen: string): Promise<void>;
  abstract eliminarImagenesDe(subdominio: string): Promise<void>;
  abstract bitacora(contenedorId: string, consulta: ConsultaSalida): Promise<LineaSalida[]>;
}

El adaptador se elige en un solo lugar y por configuración. Así, MOTOR_ADAPTADORES=stub levanta la plataforma completa sin Docker (desarrollo y CI) y docker usa los adaptadores reales, sin tocar ningún servicio:

PuertoAdaptador realDoble para pruebas y modo stub
ColaConstruccionPuertoColaBullMqColaMemoria
ClonadorRepositorioPuertoClonadorGitClonadorStub
ConstructorImagenPuertoConstructorDockerConstructorImagenStub
ContenedorPuertoContenedorDocker (dockerode)ContenedorStub
VerificacionEntornoPuertoVerificacionHttpVerificacionEntornoStub
EnrutamientoPuertoEnrutamientoTraefikArchivoEnrutamientoStub
CertificadoPuertoCertificadoAcmeArchivoCertificadoAusente (Null Object)
MetricasContenedorPuertoMetricasDockerStatsMetricasContenedorStub
CorreoPuertoCorreoSmtpAdaptador (nodemailer)CorreoConsolaAdaptador
PasarelaPagoPasarelaSimuladaLa misma (es simulada por diseño)
ProveedorFuenteFuenteGitHubPublicaDobles en las pruebas
RepositorioDespliegues, RepositorioProyectos……Prisma…Memoria

adapters/stubs/contratos-stubs.spec.ts comprueba, método por método, que el stub y el adaptador real exponen el mismo contrato: es la prueba de Liskov hecha código.

Módulos y contratos entre ellos

Un módulo solo usa lo que otro exporta en su *.module.ts, y casi siempre a través de un puerto del consumidor: M3 no importa el servicio de M2, declara CuotaProyectosPuerto y un adaptador lo resuelve contra M2. Los datos que cruzan módulos están firmados en docs/contratos/.

MóduloExpone a otrosConsume de otros (puerto del consumidor)
M1 IdentidadSesionGuard, RolGuard, @UsuarioActual(), directorio de cuentasM2 AsignacionSandboxPuerto; M10 CorreoPuerto; M9 PoliticaRegistroPuerto
M2 SuscripcionescuotaDe(usuarioId), catálogo, estado de la suscripciónM5 ContenedoresClientePuerto (detener y reanudar); M10 AvisosPuerto
M3 ProyectosLectura de proyectos y variables descifradasM2 CuotaProyectosPuerto; M4 DeteccionStackService, crearDespliegue
M4 ConstruccióncrearDespliegue, vista y bitácora del despliegue, tiempos por etapaM3 ProyectosLecturaPuerto; M5 BloqueoDespliegue; M10 AvisosPuerto (Observer)
M5 OrquestaciónAcciones de contenedor, reversión, métricasM2 CuotaPlanPuerto; M3 VariablesEntornoPuerto
M5 · datosRecursos de datos, conexión y variables inyectadasM2 CuotaDatosPuerto; M5 RecursosDatosPuerto (contenedor con volumen)
M6 EnrutamientoEnrutamientoPuerto, CertificadoPuerto—
M7 ObservabilidadResumen, historial, consumo, métricas, bitácora de ejecuciónM4 y M5 (lectura)
M9 AdministraciónUsuarios, suspensión, instalación, infraestructuraM1 CuentasSuspensionPuerto; M5 contenedores
M10 NotificacionesCorreoPuerto, AvisosPuertoM1 DestinatariosPuerto
ContratoProductor → consumidorQué fija
datos-nucleo.mdPrisma → todosModelo de datos del núcleo (v2 con métricas)
despliegues.mdM4/M5 → webVista del despliegue, etapas, bitácora, acciones (v2 con reversión)
metricas.mdM5 → M7 → webSerie de 1 h y 24 h, picos, eventos
avisos.mdM2/M4 → M10Avisos de despliegue y de suscripción
instalacion.mdinstalador → M1/M9Código de configuración, modo, salud de la instalación
recursos-datos.mdM5 datos → web, M2, appsTipos, estados, límites del plan, API y variables inyectadas

El motor de despliegue

Componentes del motor (C4 nivel 3)

Adaptadores (y un stub por puerto para pruebas)

Puertos (abstract class, sin I)

Dominio puro

Aplicación

Entrada

web · pantallas 11a, 12, 13, 14

M3 ProyectosService

M2 SuscripcionesService
cuotaDe(usuarioId)

DesplieguesController
POST /proyectos/:id/despliegues
GET /despliegues/:id · /bitacora?desde=

AccionesController (v2)
reversiones · cancelar · reintentar
reiniciar · detener · artefactos

ProcesadorDespliegues
consumidor BullMQ (worker)

ConstruccionService
Facade exportada: crearDespliegue,
consultar, bitacoraDesde

DeteccionStackService
exportado a M3

PipelineDespliegue
recorre los pasos del plan

OrquestacionService
aprovisionar, activar, detener,
detenerTodosDe

ReversionService (v2)
valida y crea el despliegue

EnrutamientoService
publicar, retirar

RetencionArtefactos
escucha DespliegueTerminado

PasoRecepcion · PasoConstruccion
PasoEjecucion · PasoEnrutamiento
PasoOperacion
Chain of Responsibility

TransicionesDespliegue
State

RecetaStack (Strategy)
Dockerfile · Node · Python · Go · Estática
decide sobre un mapa de archivos

PoliticaDespliegue
PoliticaRetencion · LimitesContenedor

ColaConstruccionPuerto

ClonadorRepositorioPuerto

ConstructorImagenPuerto

LectorFuente

RepositorioDespliegues
RepositorioArtefactos

ContenedorPuerto

VerificacionEntornoPuerto

EnrutamientoPuerto

Reloj

ColaBullMq

ClonadorGit

ConstructorDocker (dockerode)

LectorFuenteLocal · LectorFuenteGitHub (M3)

RepositorioDesplieguesPrisma

ContenedorDocker (dockerode)

VerificacionHttp

EnrutamientoTraefikArchivo

Redis

PostgreSQL

Docker Engine

GitHub

Traefik

Figura 4. C4 Nivel 3 — Componentes del motor de despliegue (M4 M5 M6), hexagonal

El pipeline como cadena de pasos

PipelineDespliegue es el corazón del trabajador. Recibe un TrabajoDespliegue { despliegueId, plan } de la cola y recorre una lista ordenada de pasos. Cada paso conoce su etapa y su estado, y el pipeline hace lo común: valida la transición, marca la etapa en curso, ejecuta, la marca completada y vacía la bitácora.

plan construccion o reversion

DespliegueTerminado

en orden

PipelineDespliegue

+ejecutar(trabajo) : void

-recorrer(contexto, pasos) : void

-fallar(contexto, error) : void

-cancelar(contexto) : void

«abstract»

PasoPipeline

+etapa Etapa

+estado EstadoDespliegue

+ejecutar(contexto) : void

+compensar(contexto) : void

+liberar(contexto) : void

PasoRecepcion

PasoConstruccion

PasoEjecucion

PasoEnrutamiento

PasoOperacion

«dominio»

TransicionesDespliegue

+puedeTransicionar(de, a) : boolean

+transicionar(de, a) : EstadoDespliegue

DeteccionStackService

+detectar(fuente, rutaDockerfile) : ResultadoDeteccion

«abstract»

RecetaStack

+reconoce(archivos) : boolean

+describir(archivos) : DescripcionReceta

+dockerfile(archivos) : string

+puertoSugerido(archivos) : number

RecetaDockerfile

RecetaNode

RecetaPython

RecetaGo

RecetaEstatica

«abstract»

OyenteDespliegue

+alTerminar(evento) : void

RetencionArtefactos

AvisoDespliegueOyente

«puerto»

ClonadorRepositorioPuerto

«puerto»

ConstructorImagenPuerto

«puerto»

ContenedorPuerto

+crear(espec) : ContenedorCreado

+detener(id) : void

+eliminarImagen(imagen) : void

«puerto»

VerificacionEntornoPuerto

+saludable(objetivo) : ResultadoSalud

«puerto»

EnrutamientoPuerto

+publicar(ruta) : url

+retirar(subdominio) : void

«puerto»

RepositorioDespliegues

ClonadorGit

ConstructorDocker

ContenedorDocker

VerificacionHttp

EnrutamientoTraefikArchivo

Figura 5. Clases del motor de despliegue (M4 Construcción, M5 Orquestación, M6 Enrutamiento)

Máquina de estados del despliegue

Los estados viven en una sola tabla pura, TransicionesDespliegue. Ningún servicio cambia un estado sin pasar por transicionar(de, a), que lanza TransicionInvalida si el cambio no está permitido:

const SIGUIENTES: Readonly<Record<EstadoDespliegue, readonly EstadoDespliegue[]>> = {
  encolado: ["construyendo", "cancelado"],
  construyendo: ["aprovisionando", "fallido", "cancelado"],
  revirtiendo: ["aprovisionando", "fallido", "cancelado"],
  aprovisionando: ["publicando", "fallido"],
  publicando: ["saludable", "fallido"],
  saludable: ["detenido"],
  detenido: ["aprovisionando"],
  fallido: [],
  cancelado: [],
};

crearDespliegue (alta, manual, reintento, redespliegue, variables)

revertir a un artefacto disponible (no consume construcciones)

el trabajador toma el trabajo

el cliente cancela

artefacto #n registrado (digest y tamaño)

clon falla, stack no reconocido, docker build falla o pasa de 10 min

el cliente cancela

la imagen del artefacto sigue en el nodo

la imagen ya fue retirada

contenedor con límites del plan responde a la salud (máx. 60 s)

el contenedor no arranca o no responde

Traefik apunta al contenedor nuevo y se detiene el anterior

falla el enrutamiento

el cliente detiene o reinicia (cola operación), suscripción Suspendida o cuenta suspendida

el cliente reinicia (mismo contenedor, sin reconstruir ni consumir construcciones)

Encolado

Revirtiendo

Construyendo

Cancelado

Aprovisionando

Fallido

Publicando

Saludable

Detenido

Etapa Recepción pendiente.
Reintentar y redesplegar crean
un despliegue nuevo (#n+1).

Etapas Recepción y Construcción:
git clone, detección de stack,
docker build con Dockerfile o receta.

Recepción y Construcción
quedan «omitida».
Reusa la imagen deploya/p:n.

Etapa Ejecución: red del proyecto,
--cpus y --memory de cuotaDe,
sin privilegios.

Etapa Enrutamiento: ruta dinámica
de Traefik; conmutación sin corte.

Etapa Operación. Proyecto.despliegueActivo
cambia aquí; el anterior se detiene
y la retención deja 5 artefactos.

La versión activa sigue
sirviendo tráfico.
Guarda codigoSalida y motivo.

Figura 6. Estados del despliegue — núcleo v4.1 con reversión (no son los de suscripción)

Detección de stack

Sí

No

Sí

No

Sí

No

Sí

No

Sí

No

LectorFuente
GitHub en 11a · disco en el trabajador

Leer solo los archivos que piden las recetas
Dockerfile, package.json, requirements.txt,
pyproject.toml, go.mod, index.html

¿Dockerfile en
rutaDockerfile?

RecetaDockerfile
usa el del repo
puerto = primer EXPOSE o 8080

¿package.json
con script start?

RecetaNode
node:22-alpine · npm ci · build si existe
npm start · PORT=8080

¿requirements.txt o
pyproject.toml, y main.py o app.py?

RecetaPython
python:3.12-slim · pip install
gunicorn si está, si no python main.py

¿go.mod?

RecetaGo
golang:1.23 → distroless
binario estático · PORT=8080

¿index.html
en la raíz?

RecetaEstatica
nginx-unprivileged · :8080

StackNoReconocido
11e: falta Dockerfile y no se reconoce el stack

Generar Dockerfile.deploya en el clon
bitácora: Stack detectado · receta Deploya

ResultadoDeteccion
receta, descripcion,
puertoSugerido, evidencia

Figura 7. Actividad — detección de stack (M4-03, Strategy de recetas en orden fijo)

Las recetas son puras: deciden sobre un mapa de archivos ya leído. Por eso la misma detección sirve en el alta (M3 lee por la API de GitHub con LectorFuenteGitHub) y en el trabajador (lee del clon con LectorFuenteLocal). Agregar un stack es crear una clase RecetaX extends RecetaStack y registrarla en la lista: abierto a extensión, cerrado a modificación (ADR 0003).

Diagramas UML

Clases · Identidad y notificaciones

IdentidadService depende de nueve abstracciones y de ninguna implementación. CorreoConRegistro es un Decorator: envuelve cualquier CorreoPuerto para registrar el último envío (lo usa Admin · Instalación) sin que el adaptador SMTP lo sepa. PlantillaCorreo es un Template Method: componer() arma el mensaje con el kit visual y cada plantilla solo define asunto, título, párrafo y botón.

envuelve

usa

IdentidadController

SesionController

RecuperacionController

CuentaController

SesionGuard

RolGuard

IdentidadService

+registrar(solicitud) : CuentaRegistrada

+verificar(token) : CuentaVerificada

+reenviarVerificacion(solicitud) : void

SesionService

+iniciar(credenciales) : SesionIniciada

+usuarioDe(token) : UsuarioSesion

+cerrar(token) : void

RecuperacionService

+solicitar(correo) : void

+restablecer(solicitud) : void

CuentaService

+perfil(usuarioId) : PerfilCuenta

+renombrar(usuarioId, nombre) : PerfilCuenta

+cambiarContrasena(usuarioId, sesion, cambio) : void

+cerrarTodas(usuarioId) : void

«dominio»

PoliticaContrasena

+validar(clave) : ResultadoPolitica

«dominio»

PoliticaReenvio

+segundosRestantes(creadoUltimo, ahora) : number

«objeto valor»

NombreCuenta

«puerto»

RepositorioUsuarios

«puerto»

RepositorioSesiones

«puerto»

RepositorioTokensCuenta

«puerto»

HashContrasena

+calcular(clave) : string

+coincide(clave, hash) : boolean

«puerto»

GeneradorToken

«puerto»

AsignacionSandboxPuerto

«puerto»

Reloj

+ahora() : Date

«puerto»

CorreoPuerto

+enviar(destinatario, plantilla, datos) : void

«template method»

PlantillaCorreo

+componer(datos) : MensajeCorreo

#asunto(datos) : string

#titulo(datos) : string

#parrafo(datos) : string

#textoBoton(datos) : string

CorreoSmtpAdaptador

CorreoConsolaAdaptador

«decorator»

CorreoConRegistro

CorreoSegunInstalacion

PlantillaVerificacion

PlantillaRecuperacion

HashContrasenaScrypt

RepositorioUsuariosPrisma

AsignacionSandboxSuscripciones

Figura 8. Clases de Identidad (M1) y Notificaciones (M10)

Clases · Suscripciones y administración

«facade»

SuscripcionesService

+catalogo() : PlanCatalogo[]

+asignarSandbox(usuarioId) : void

+cuotaDe(usuarioId) : Cuota

ContratacionService

+miSuscripcion(usuarioId) : VistaSuscripcion

+cotizar(usuarioId, solicitud) : Cotizacion

+contratar(usuarioId, solicitud) : ResultadoContratacion

+programarDescenso(usuarioId, codigoPlan) : VistaSuscripcion

HistorialPagosService

+deUsuario(usuarioId) : FilaPago[]

+pdfDe(usuarioId, pagoId) : ArchivoComprobante

CicloSuscripcionService

+ejecutar() : ResultadoCiclo

TareaCicloDiaria

«dominio»

PoliticaCiclo

+siguiente(suscripcion, ahora) : SaltoCiclo

+cambioPorSalto(suscripcion, salto, ahora) : CambioSuscripcion

«puerto»

PasarelaPago

+cobrar(cargo) : ResultadoCobro

PasarelaSimulada

«puerto · Strategy»

CatalogoPlanes

+enVenta() : PlanCatalogo[]

+planInicial() : Plan

+exigirCobro() : void

CatalogoNube

CatalogoAutoalojado

«puerto · Strategy»

PoliticaCuotas

CuotasDelPlan

LimitesDeInstalacion

«puerto»

ComprobantePdfPuerto

ComprobantePdfKit

«puerto»

RepositorioSuscripciones

«puerto»

RepositorioPagos

«puerto»

RepositorioPlanes

«puerto · M5»

ContenedoresClientePuerto

«puerto · M10»

AvisosPuerto

SuspensionService

+suspender(adminId, usuarioId, solicitud) : void

ReactivacionCuentaService

+reactivar(adminId, usuarioId, solicitud) : ResultadoReactivacion

«puerto»

CuentasSuspensionPuerto

+cuenta(usuarioId) : CuentaObjetivo

+suspender(suspension) : void

+reactivar(reactivacion) : void

Figura 9. Clases — Suscripciones y pagos (M2) y Administración (M9)

Clases · Proyectos y observabilidad

ProyectosController

ProyectosService

+validarRepositorio(consulta) : ValidacionRepositorio

+listar(usuarioId) : ListaProyectos

+crear(usuarioId, alta) : ProyectoCreado

+configurar(usuarioId, proyectoId, cambios) : Proyecto

+eliminar(usuarioId, proyectoId, confirmacion) : void

VariablesProyectoService

+guardarVariables(usuarioId, proyectoId, pedido) : VariablesGuardadas

+listarVariables(usuarioId, proyectoId) : VariablePublica[]

+mostrarVariable(usuarioId, proyectoId, clave) : string

«puerto»

ProveedorFuente

+validar(consulta) : ValidacionRepositorio

+lector(consulta) : LectorFuente

«puerto»

RepositorioProyectos

+guardar(proyecto) : Proyecto

+porId(id) : Proyecto

+deUsuario(usuarioId) : Proyecto[]

+existeSubdominio(subdominio) : boolean

«puerto»

CuotaProyectosPuerto

+cuotaDe(usuarioId) : CuotaProyectos

«puerto»

CifradorVariables

+cifrar(valor) : string

+descifrar(valorCifrado) : string

FuenteGitHubPublica

CifradorAesGcm

CuotaProyectosSuscripciones

RepositorioProyectosPrisma

«M4»

DeteccionStackService

«M4»

ConstruccionService

+crearDespliegue(pedido) : Despliegue

«M5»

BloqueosService

ResumenProyectoService

+resumen(proyectoId, usuarioId) : ResumenProyecto

ConsumoService

+de(usuarioId) : ConsumoPeriodoDto

+diario(usuarioId) : ConsumoDiarioDto

LecturaMetricasService

+leer(proyectoId, usuarioId, rango) : RespuestaMetricas

TiemposService

+leer(proyectoId, usuarioId, ultimos) : RespuestaTiempos

BitacoraEjecucionService

+leer(proyectoId, usuarioId, desde) : RespuestaBitacora

«puerto»

SalidaContenedorPuerto

Figura 10. Clases — Proyectos (M3) y Observabilidad (M7)

Secuencia · Recorrido completo

Del registro a la aplicación en línea, cruzando todos los módulos del núcleo.

Traefikworker M4 M5 M6M4 Construcción (api)GitHubM3 ProyectosM2 SuscripcionesMailpitM10 CorreoPuertoM1 IdentidadwebEddy · pantallas 01, 02, 03Javier · pantalla 06Eduardo · pantallas 10b, 11a, 11dDerek · motor, pantallas 10 y 12loop[cada 3 s]ClienteRegistro (01)1POST /identidad/registro2asignarSandbox(usuarioId)3enviar(verificacion, enlace 24 h)4SMTP5abre el correo (localhost:8025)6enlace de verificación (02)7POST /identidad/verificar8Iniciar sesión (03)9POST /identidad/sesion10cookie de sesión (SesionGuard)11Planes12GET /suscripciones/planes134 planes · Sandbox es el plan actual14Nuevo proyecto: URL, rama, nombre15POST /proyectos/validar16API pública: repo, rama, último commit, Dockerfile17DeteccionStackService.detectar(LectorFuenteGitHub) · desde A218Dockerfile detectado · puerto de EXPOSE19Revisar → Desplegar20POST /proyectos21cuotaDe · 0 de 1 proyectos22crearDespliegue(proyectoId, alta)23despliegue24TrabajoDespliegue por Redis25clonar → detectar → docker build → artefacto26contenedor 0.25 vCPU · 256 MB → salud OK27ruta hola-deploya.localhost → contenedor28GET /despliegues/:id y /bitacora?desde=n29saludable · url30http://hola-deploya.localhost31la app responde32
Figura 11. Recorrido de punta a punta del Avance 1 — de registrarse a ver la app Saludable

Secuencia · Contratación de un plan

PostgreSQLM10 NotificacionesM5 OrquestacionServicio de cuotasAdaptador de pasarela (simulada)Servicio de pagosServicio de suscripcionesServicio de catalogoControlador de suscripcionesInterfaz webalt[Cargo aprobado][Cargo rechazado]ClienteSelecciona plan y vigencia (30/365 dias)POST /suscripciones (planId, vigencia)obtenerPlan(planId)SELECT planplan (precio, recursos, vigencia)plan validoprocesarCargo(cliente, plan, vigencia)cobrar(monto, metodo)resultado (aprobado | rechazado)registrar transaccion (aprobada)pago aprobadocrearSuscripcion(cliente, plan, vigencia)INSERT suscripcion (estado = Activa)suscripcion creadaaplicarLimites(suscripcion, plan)configurarLimites(cpu, memoria, proyectos, ...)limites aplicadoscuota activaenviarConfirmacionContratacion(cliente, plan)notificacion encoladasuscripcion activa201 Creada (suscripcion, comprobante)Confirmacion de contratacionregistrar transaccion (rechazada)pago rechazado402 Pago rechazadoMuestra motivo del rechazo, no se crea la cuota
Figura 12. Secuencia — contratación de un plan con la pasarela simulada (M2)

Secuencia · Despliegue

EnrutamientoTraefikArchivoVerificacionHttpContenedorDockerOrquestacionServiceConstructorDockerDeteccionStackServiceClonadorGitPipelineDespliegue (worker)ColaBullMq (Redis)PostgreSQLSuscripcionesServiceConstruccionService (api)web (pantallas 11d, 12)loop[cada 3 s mientras terminado = false]1 · Recepción2 · Construcciónloop[cada línea de docker build]3 · Ejecución4 · Enrutamiento5 · Operaciónalt[responde][no responde en 60 s]alt[build correcto][build falla o pasa de 10 min]ClienteDesplegar1POST /proyectos/:id/despliegues2cuotaDe(usuarioId)3Despliegue4encolar(TrabajoDespliegue{despliegueId, plan: construccion})5201 {id, numero, estado: encolado}6GET /despliegues/:id y /bitacora?desde=n7estado, etapas, líneas n > desde8riel de etapas + líneas nuevas9trabajo10construyendo · recepción en curso11clonar(url, rama, destino) --depth 112commit {sha, mensaje, autor}13detectar(LectorFuenteLocal)14receta dockerfile o node/python/go/estatica15commit, receta, líneas de bitácora16construir(directorio, etiqueta deploya/p:n, 10 min, alLinea)17alLinea(texto)18LineaBitacora n, marca, etapa construccion19{digest, tamanoBytes}20Artefacto21aprovisionar(contexto)22cuotaDe(usuarioId)23crear({nombre, imagen, red del proyecto, cpus, memoriaMb, variables})24{id, host}25saludable({host, puerto, ruta /, 60 s})26ok, 200 en 38 ms27publicando28publicar({subdominio, host, puerto})29url http(s)://p.dominio30saludable · Proyecto.despliegueActivo =31detener(contenedor anterior)32publica DespliegueTerminado (retención de artefactos)33falla34eliminar(contenedor nuevo)35fallido · la versión activa no cambia36ConstruccionFallida(codigoSalida)37fallido · codigoSalida · motivo · construcción fallida38
Figura 13. Secuencia de un despliegue por construcción — Recepción → Construcción → Ejecución → Enrutamiento → Operación

Secuencia · Reversión sin reconstruir

EnrutamientoTraefikArchivoVerificacionHttpContenedorDockerOrquestacionServicePipelineDespliegue (worker)ColaBullMq (Redis)PostgreSQLReversionService (api)web (pantalla 14)No se clona ni se construye. No consume construcciones del mes.alt[responde][no responde]alt[no disponible, ya activo o hay un despliegue en curso][disponible]ClienteRevertir a la versión1POST /proyectos/:id/reversiones {artefactoId}2artefacto3409 artefacto-no-disponible · artefacto-ya-activo · despliegue-en-curso4Despliegue5encolar(TrabajoDespliegue{despliegueId, plan: reversion})6201 {id, numero: 16, estado: revirtiendo}7trabajo8aprovisionando · ejecución en curso9aprovisionar(contexto con la imagen deploya/p:13)10crear(imagen deploya/p:13, límites vigentes, variables vigentes)11saludable(host, puerto, 60 s)12publicar(subdominio → contenedor13saludable · Proyecto.despliegueActivo =14detener(contenedor de15eliminar(contenedor16fallido ·17
Figura 14. Secuencia de reversión — vuelve a un artefacto anterior sin reconstruir (M5-04)

Estados · Suscripción

PoliticaCiclo.siguiente(suscripcion, ahora) es una función pura: recibe la fecha como parámetro, así que el ciclo de 7, 5 y 30 días se prueba con fechas literales y sin esperar. Cada corrida mueve una suscripción un estado como máximo, lo que la hace idempotente.

contratacion con pago aprobado
(Sandbox: Activa sin vencimiento)

faltan 7 dias o menos
(DIAS_POR_VENCER)

se cumple la fecha de vigencia
(un solo salto por corrida)

renovacion manual aprobada

se cumple la fecha
de vigencia sin renovar

descenso programado a Sandbox
al vencer (sin cobro)

descenso programado a un plan
de pago al vencer (sin cobro)

renovacion aprobada
durante la gracia

5 dias en Vencida
(DIAS_GRACIA)

renovacion aprobada
(M5 reanuda los contenedores)

30 dias en Suspendida
(DIAS_HASTA_CANCELAR)

contratacion aprobada
(M5 reanuda los contenedores)

Activa

PorVencer

Vencida

Suspendida

Cancelada

Todos los entornos operan
con la cuota contratada

Entornos en linea;
nuevos despliegues bloqueados

Contenedores detenidos (M5);
datos y configuracion conservados

Datos conservados;
solo cambia el estado

Tarea diaria 00:05 UTC (BullMQ)
o POST /admin/ciclo/ejecutar;
idempotente el mismo dia

Figura 15. Estados de la suscripción — ciclo §4.4 (no son los de despliegue)

Actividad · Registro y verificación

No

Sí

Sí

No

Sí

No

Inicio

Usuario abre
Crear cuenta

Ingresar correo, contraseña
y confirmación

Enviar solicitud
de registro

¿Correo válido y contraseña
cumple la política?

Mostrar errores
de validación (400)

Verificar si el
correo ya existe

¿Correo registrado?

Informar que la cuenta ya existe (409):
iniciar sesión o recuperar contraseña

Fin

Crear cuenta pendiente
con hash de la contraseña

Pedir Sandbox a M2
(asignarSandbox)

Generar token de verificación
(se guarda su huella; vence en 24 h)

Enviar correo
de verificación (M10)

¿Correo enviado?

Mostrar confirmación:
revisar correo

Fin

Cuenta creada con aviso
(correoEnviado: false)

Fin

Figura 16. Actividad — registro de cuenta (M1, M10)

Actividad · Contratación y activación

No

Si

Inicio

Cliente consulta
catalogo de planes

Selecciona plan
y vigencia (30/365 dias)

Confirma cargo simulado

Pasarela simulada
procesa el cobro

Cargo aprobado?

Registrar transaccion
rechazada

Mostrar motivo
del rechazo

Fin

Registrar transaccion
aprobada

Crear suscripcion
estado = Activa

Aplicar limites del plan
al contenedor (M5)

Registrar en historial
de pagos

Enviar confirmacion
de contratacion

Fin

Figura 17. Actividad — contratación y activación de un plan (M2)

SOLID aplicado

Cada principio con la regla que sigue el equipo y dónde se puede comprobar en el código.

PrincipioRegla en DeployaEvidencia en el código
S · Responsabilidad únicaEl controlador adapta HTTP, el servicio orquesta, la política decide, el adaptador habla con el exteriorPoliticaCiclo saca el ciclo de SuscripcionesService; PoliticaRetencion (decide) y RetencionArtefactos (ejecuta) están separadas; un controlador por recurso en M9
O · Abierto/cerradoSe extiende con una implementación nueva, no con if/switch por tipoRecetas de stack (RecetaNode, RecetaPython…), planes del pipeline, ManejadorAccion por acción (DetenerManejador, ReiniciarManejador, EliminarManejador), modo de instalación (ADR 0008)
L · Sustitución de LiskovStub, doble y adaptador real cumplen el mismo contrato y lanzan los mismos errores de dominiocontratos-stubs.spec.ts; CertificadoAusente devuelve null igual que el real sin TLS; RecetaDockerfile es una receta más, sin casos especiales
I · Segregación de interfacesPuertos estrechos pensados desde quien los usa~70 puertos; VerificacionEntornoPuerto tiene un solo método; ProyectosLecturaPuerto y DetalleProyectoPuerto son vistas distintas del mismo repositorio
D · Inversión de dependenciasServicios reciben puertos por constructor; nada de new de Docker, Prisma, SMTP ni Date.now() en el dominioIdentidadService recibe 9 puertos; el tiempo entra por Reloj; los bindings viven en AdaptersModule y en cada *.module.ts

Clean code

Patrones de diseño

PatrónDóndeClasesPor qué
Ports & Adapters (hexagonal)Todos los módulos~70 puertos abstract classEl dominio se prueba sin infraestructura y el proveedor se cambia por configuración
Repository (Fowler)Persistencia por agregadoRepositorioUsuarios, RepositorioDespliegues, RepositorioPagos… con versión Prisma y en memoriaEl servicio no conoce SQL ni Prisma
Adapter (GoF)ExteriorContenedorDocker, EnrutamientoTraefikArchivo, FuenteGitHubPublica, CorreoSmtpAdaptadorTraducir una API externa al contrato del puerto
StrategyPagos, recetas, planes del pipeline, modo de instalaciónPasarelaPago, RecetaStack, PlanesPipeline, CatalogoPlanes, PoliticaCuotasVariar el algoritmo sin tocar a quien lo usa
State (tabla pura)Despliegue y suscripciónTransicionesDespliegue, PoliticaCicloUna sola fuente de verdad de las transiciones, probada sin base
Chain of ResponsibilityMotorPasoPipeline y sus cinco pasosCada etapa es una clase con su compensación
Command + productor/consumidorColasTrabajoDespliegue, acciones de operaciónConstruir fuera del ciclo HTTP; jobId = despliegueId lo hace idempotente
ObserverFin de un despliegueOyenteDespliegue ← RetencionArtefactos, AvisoDespliegueOyenteEl pipeline no depende del correo ni de la retención
DecoratorCorreoCorreoConRegistro envuelve un CorreoPuertoRegistrar envíos sin tocar el adaptador SMTP
Template MethodPlantillas de correoPlantillaCorreo.componer()Mismo kit visual; cada correo define solo su texto
Null ObjectCertificado sin TLSCertificadoAusenteLa vista no pregunta si hay TLS
FacadeFronteras entre módulosSuscripcionesService.cuotaDe, asignarSandboxUna sola puerta por módulo
CompositeSalud de la instalaciónSaludInstalacion y sus comprobacionesUn estado general a partir de varias comprobaciones
Guard / Decorator de NestAutorizaciónSesionGuard, RolGuard, @Roles(), @UsuarioActual()La autorización no ensucia los controladores
Container / PresentationalWebHooks (useDespliegue) y componentes sin fetchLa lógica de la web se prueba con node --test
Strategy (recursos de datos)M5 · datosTipoRecursoDatos: postgres, redis, s3Cada tipo sabe su imagen, su puerto, su memoria, cómo prepararse y qué variables inyecta; un tipo nuevo es una clase más (OCP)

Modelo de datos

Modelo entidad-relación

Dieciocho tablas en PostgreSQL. El modelo se agrupa por el módulo dueño de cada tabla; las relaciones que cruzan módulos (Usuario → Proyecto, Plan → Suscripcion) están firmadas en docs/contratos/datos-nucleo.md.

USUARIOstringidPKstringcorreoUKstringnombrestringhashContrasenaRolrolcliente | administradorEstadoCuentaestadoCuentapendiente | activa | suspendidastringmotivoSuspensiondatetimecontrasenaCambiadaEnnull = nunca (SP-03)datetimecreadoTOKEN_CUENTAstringidPKstringusuarioIdFKTipoTokenCuentatipoverificacion 24 h | recuperacion 30 minstringhashTokenUKdatetimeexpiradatetimeusadoEnun solo usoSESIONstringidPKstringusuarioIdFKstringhashTokenUKdatetimeultimaActividadexpira a los 7 días sin actividaddatetimerevocadaEnSUSCRIPCIONstringidPKstringusuarioIdFK,UKuna por cuentastringplanIdFKEstadoSuscripcionestadoactiva | por-vencer | vencida | suspendida | canceladadatetimeestadoDesdeintvigenciaDias30 | 365 | nulldatetimevencenull en SandboxstringplanSiguienteIdFKPAGOstringidPKstringusuarioIdFKstringsuscripcionIdFKstringplanIdFKConceptoPagoconceptodecimalmontoEstadoPagoestadoaprobado | rechazadostringtarjetaUltimos4stringnumeroComprobanteUKPROYECTOstringidPKstringusuarioIdFKstringnombrestringsubdominioUKinmutablestringurlRepositoriostringramastringrutaDockerfilestringcomandoInicionull = CMD de la imagenintpuertoInternoRecetaConstruccionrecetadockerfile | node | python | go | estaticastringdespliegueActivoIdFK,UKACCION_ADMINISTRATIVAstringidPKstringadminIdFKstringusuarioAfectadoIdFKstringaccionsuspender-cuentastringmotivostringdetallePLANstringidPKstringcodigoUKsandbox | starter | pro | businessdecimalprecio30decimalprecio365intmaxProyectosdecimalcpusintmemoriaMbintconstruccionesMesintmaxRecursosDatospor cuentaintdiscoDatosMbpor recurso: se mide y se avisaVARIABLE_ENTORNOstringidPKstringproyectoIdFKstringclaveUK con proyectoIdstringvalorCifradoAES-256-GCMDESPLIEGUEstringidPKstringproyectoIdFKintnumeroUK con proyectoIdEstadoDespliegueestadoDisparadorDesplieguedisparadorstringcommitShastringartefactoIdFKstringcontenedorIdstringurldecimalcpusintmemoriaMbintcodigoSalidastringmotivoFallodatetimecreadodatetimeterminadoARTEFACTOstringidPKstringproyectoIdFKintnumeroUK con proyectoIdstringimagendeploya/subdominio:nstringdigestbiginttamanoBytesstringcommitShaRecetaConstruccionrecetabooleandisponibleretención: últimos 5ETAPA_DESPLIEGUEstringdespliegueIdPK,FKEtapaetapaPKrecepcion..operacionEstadoEtapaestadopendiente | en-curso | completada | fallida | omitidadatetimeiniciadadatetimeterminadaLINEA_BITACORAstringdespliegueIdPK,FKintnPKconsecutivo; desde= filtra n mayordatetimemarcaEtapaetapaNivelBitacoranivelinfo | aviso | errorstringtextoRECURSO_DATOSstringidPKstringproyectoIdFKdueñoTipoRecursoDatostipopostgres | redis | s3EstadoRecursoDatosestadocreando | disponible | detenido | errorstringcontenedordeploya-datos-<subdominio>-<tipo>stringvolumenstringcontrasenaCifradaAES-256-GCMintusoDiscoMbúltima medicióndatetimemedidoEnRECURSO_DATOS_COMPARTIDOstringrecursoIdPK,FKstringproyectoIdPK,FKmismo usuariodatetimecreadoCONFIGURACION_INSTALACIONstringidPKuna fila: instalacionstringhashCodigosha256 del código; null si ya hay administradordatetimecodigoVence30 minPoliticaRegistropoliticaRegistroabierto | invitacion | cerradodecimalcpuPorProyectointmemoriaMbPorProyectointproyectosPorUsuarionull = sin límitedatetimeultimaLimpiezaEST-01b: última limpieza diariaintfilasLimpiadasfilas que borró esa corridarecibeabretienerealizaposeeejecuta (admin)recibeplan actualdescenso programadocobrageneraconfiguratieneversionaactivoconstruido o reusado porcinco etapasproducees dueño dese comparteusa compartidoERD del núcleo v4.1 — fuente del schema Prisma (docs/contratos/datos-nucleo.md)
Figura 18. ERD del núcleo v4.1 — fuente del schema Prisma (docs/contratos/datos-nucleo.md)

Decisiones de diseño de la base

DecisiónPor qué
Claves primarias uuid generadas por la aplicaciónNo revelan volumen ni orden; se pueden crear antes de insertar (el despliegue y su trabajo comparten id)
Claves naturales únicas aparte del idUsuario.correo, Plan.codigo, Proyecto.subdominio, Pago.numeroComprobante: el seed es idempotente por clave natural
Únicas compuestas donde el dominio lo pide(proyectoId, numero) en Despliegue y Artefacto; (usuarioId, nombre) en Proyecto; (proyectoId, clave) en VariableEntorno
Claves compuestas sin id artificialEtapaDespliegue (despliegueId, etapa), LineaBitacora (despliegueId, n), MuestraMetrica (despliegueId, tomada): son series que se leen siempre por su despliegue
Dinero en numeric(10,2) y moneda char(3)Nada de flotantes para importes
Enums de PostgreSQL para los estadosLa base rechaza un estado que no existe; los valores coinciden con el contrato (por-vencer, en-curso)
Borrado en cascada desde el agregado raízEliminar un proyecto borra variables, despliegues, etapas, bitácora, artefactos y muestras; eliminar un usuario borra todo lo suyo
SetNull en referencias débilesProyecto.despliegueActivoId y Despliegue.artefactoId no bloquean el borrado del otro lado
Índices por la consulta que los usa(estado, vence) para el ciclo diario; (usuarioId, creado) para el historial de pagos; (proyectoId, creado) para el historial de despliegues; tomada para purgar métricas
Secretos nunca en claroContraseñas con scrypt; tokens solo como SHA-256; variables y SMTP con AES-256-GCM
Una fila de configuraciónConfiguracionInstalacion.id = "instalacion" garantiza una sola configuración en autoalojado

Migraciones: siete migraciones de Prisma versionadas en apps/api/prisma/migrations (núcleo del 29 de septiembre, más métricas, configuración de la instalación, fecha de cambio de contraseña, invitaciones y correo de la instalación). prisma migrate deploy las aplica en orden y el servicio migracion de compose las corre antes que la API.

Diccionario de datos

Generado desde apps/api/prisma/schema.prisma cada vez que se construye este documento, así que no puede desfasarse del modelo real. Claves: PK primaria, FK foránea, UQ única.

M1 Identidad (Eddy)

Usuario

ColumnaTipoNuloClaveDescripción
idtextNoPKPor defecto: uuid().
correotextNoUQnormalizado en minúsculas.
nombrevarchar(64)No——
hashContrasenatextNo——
rolenum RolNo—Por defecto: cliente.
estadoCuentaenum EstadoCuentaNo—Por defecto: pendiente.
motivoSuspensiontextSí—lo muestra 03b; lo escribe M9.
contrasenaCambiadaEntimestamp(3)Sí—SP-03 · M1 la escribe al cambiar (M1-07) y al restablecer (M1-05); null = nunca.
creadotimestamp(3)No—Por defecto: now().
actualizadotimestamp(3)No—Se actualiza en cada escritura.

TokenCuenta

ColumnaTipoNuloClaveDescripción
idtextNoPKPor defecto: uuid().
usuarioIdtextNoFK—
tipoenum TipoTokenCuentaNo——
hashTokentextNoUQsha256 del token del enlace.
expiratimestamp(3)No—verificación 24 h, recuperación 30 min.
usadoEntimestamp(3)Sí—un solo uso.
creadotimestamp(3)No—Por defecto: now().

Índice: (usuarioId, tipo) · Claves foráneas: usuarioId → Usuario.id (al borrar: Cascade)

Sesion

ColumnaTipoNuloClaveDescripción
idtextNoPKPor defecto: uuid().
usuarioIdtextNoFK—
hashTokentextNoUQsha256 del valor de la cookie.
creadatimestamp(3)No—Por defecto: now().
ultimaActividadtimestamp(3)No—expira a los 7 días sin actividad. Por defecto: now().
revocadaEntimestamp(3)Sí——
agenteUsuariotextSí——

Índice: (usuarioId) · Claves foráneas: usuarioId → Usuario.id (al borrar: Cascade)

M2 Suscripciones (Javier)

Plan

ColumnaTipoNuloClaveDescripción
idtextNoPKPor defecto: uuid().
codigotextNoUQsandbox | starter | pro | business.
nombretextNo——
descripciontextNo——
precio30decimal(10, 2)No——
precio365decimal(10, 2)Sí—null: no se vende anual (Sandbox)
maxProyectosintegerNo——
cpusdecimal(4, 2)No—vCPU por proyecto.
memoriaMbintegerNo—por proyecto.
construccionesMesintegerNo——
ordenintegerNo——
activobooleanNo—Por defecto: true.

Suscripcion

ColumnaTipoNuloClaveDescripción
idtextNoPKPor defecto: uuid().
usuarioIdtextNoFK · UQuna suscripción por cuenta; cambia de plan y de estado.
planIdtextNoFK—
estadoenum EstadoSuscripcionNo—Por defecto: activa.
estadoDesdetimestamp(3)No—gracia de 5 días y 30 días suspendida. Por defecto: now().
vigenciaDiasintegerSí—30 | 365; null en Sandbox.
iniciotimestamp(3)No—Por defecto: now().
vencetimestamp(3)Sí—null: no vence (Sandbox)
planSiguienteIdtextSíFKdescenso programado al terminar la vigencia.
creadotimestamp(3)No—Por defecto: now().
actualizadotimestamp(3)No—Se actualiza en cada escritura.

Índice: (estado, vence) · Claves foráneas: usuarioId → Usuario.id (al borrar: Cascade); planId → Plan.id; planSiguienteId → Plan.id

Pago

ColumnaTipoNuloClaveDescripción
idtextNoPKPor defecto: uuid().
usuarioIdtextNoFK—
suscripcionIdtextNoFK—
planIdtextNoFK—
conceptoenum ConceptoPagoNo——
vigenciaDiasintegerNo——
montodecimal(10, 2)No——
monedachar(3)No—Por defecto: "USD".
estadoenum EstadoPagoNo——
motivoRechazotextSí——
tarjetaUltimos4char(4)No——
numeroComprobantetextSíUQsolo aprobados, p. ej. DPY-2026-000123.
creadotimestamp(3)No—Por defecto: now().

Índice: (usuarioId, creado) · Claves foráneas: usuarioId → Usuario.id (al borrar: Cascade); suscripcionId → Suscripcion.id (al borrar: Cascade); planId → Plan.id

M9 Administración (Javier)

AccionAdministrativa

ColumnaTipoNuloClaveDescripción
idtextNoPKPor defecto: uuid().
adminIdtextNoFK—
usuarioAfectadoIdtextNoFK—
acciontextNo—"suspender-cuenta" (núcleo); "reactivar-cuenta" (SP-05)
motivotextNo——
detalletextSí——
creadotimestamp(3)No—Por defecto: now().

Índice: (usuarioAfectadoId) · Claves foráneas: adminId → Usuario.id; usuarioAfectadoId → Usuario.id (al borrar: Cascade)

M3 Proyectos (Eduardo)

Proyecto

ColumnaTipoNuloClaveDescripción
idtextNoPKPor defecto: uuid().
usuarioIdtextNoFK—
nombrevarchar(64)No——
subdominiovarchar(63)NoUQetiqueta DNS; no cambia al renombrar.
urlRepositoriotextNo——
ramatextNo—Por defecto: "main".
rutaDockerfiletextNo—Por defecto: "Dockerfile".
puertoInternointegerNo—Por defecto: 8080.
recetaenum RecetaConstruccionNo—la última que detectó M4. Por defecto: dockerfile.
despliegueActivoIdtextSíFK · UQel que atiende tráfico; lo escribe M5.
creadotimestamp(3)No—Por defecto: now().
actualizadotimestamp(3)No—Se actualiza en cada escritura.

Única: (usuarioId, nombre) · Claves foráneas: usuarioId → Usuario.id (al borrar: Cascade); despliegueActivoId → Despliegue.id (al borrar: SetNull)

VariableEntorno

ColumnaTipoNuloClaveDescripción
idtextNoPKPor defecto: uuid().
proyectoIdtextNoFK—
clavevarchar(128)No——
valorCifradotextNo——
creadotimestamp(3)No—Por defecto: now().
actualizadotimestamp(3)No—Se actualiza en cada escritura.

Única: (proyectoId, clave) · Claves foráneas: proyectoId → Proyecto.id (al borrar: Cascade)

M4 · M5 · M6 Motor (Derek)

Artefacto

ColumnaTipoNuloClaveDescripción
idtextNoPKPor defecto: uuid().
proyectoIdtextNoFK—
numerointegerNo—= número del despliegue que lo construyó (#n)
imagentextNo—deploya/:.
digesttextNo—sha256:….
tamanoBytesbigintNo——
commitShavarchar(40)No——
recetaenum RecetaConstruccionNo——
disponiblebooleanNo—false cuando la retención borra la imagen. Por defecto: true.
creadotimestamp(3)No—Por defecto: now().

Única: (proyectoId, numero) · Claves foráneas: proyectoId → Proyecto.id (al borrar: Cascade)

Despliegue

ColumnaTipoNuloClaveDescripción
idtextNoPKPor defecto: uuid().
proyectoIdtextNoFK—
numerointegerNo—#n por proyecto, consecutivo.
estadoenum EstadoDespliegueNo—Por defecto: encolado.
disparadorenum DisparadorDespliegueNo—Por defecto: manual.
ramatextNo——
commitShavarchar(40)Sí——
commitMensajetextSí——
commitAutortextSí——
artefactoIdtextSíFKel que construyó, o el que reusa una reversión.
contenedorIdtextSí——
urltextSí——
cpusdecimal(4, 2)Sí—límites aplicados (pantalla 13)
memoriaMbintegerSí——
codigoSalidaintegerSí——
motivoFallotextSí——
creadotimestamp(3)No—Por defecto: now().
iniciadotimestamp(3)Sí——
terminadotimestamp(3)Sí——
actualizadotimestamp(3)No—Se actualiza en cada escritura.

Única: (proyectoId, numero) · Índice: (proyectoId, creado) · Claves foráneas: proyectoId → Proyecto.id (al borrar: Cascade); artefactoId → Artefacto.id (al borrar: SetNull)

EtapaDespliegue

ColumnaTipoNuloClaveDescripción
despliegueIdtextNoFK—
etapaenum EtapaNo——
estadoenum EstadoEtapaNo—Por defecto: pendiente.
iniciadatimestamp(3)Sí——
terminadatimestamp(3)Sí——

Clave primaria compuesta: (despliegueId, etapa) · Claves foráneas: despliegueId → Despliegue.id (al borrar: Cascade)

LineaBitacora

ColumnaTipoNuloClaveDescripción
despliegueIdtextNoFK—
nintegerNo—1, 2, 3… por despliegue; ?desde= filtra n > desde.
marcatimestamp(3)No——
etapaenum EtapaNo——
nivelenum NivelBitacoraNo—Por defecto: info.
textotextNo——

Clave primaria compuesta: (despliegueId, n) · Claves foráneas: despliegueId → Despliegue.id (al borrar: Cascade)

MuestraMetrica

ColumnaTipoNuloClaveDescripción
despliegueIdtextNoFK—
tomadatimestamp(3)No——
cpudecimal(6, 3)No——
memoriaMbintegerNo——

Clave primaria compuesta: (despliegueId, tomada) · Índice: (tomada) · Claves foráneas: despliegueId → Despliegue.id (al borrar: Cascade)

ConfiguracionInstalacion

ColumnaTipoNuloClaveDescripción
idtextNoPKPor defecto: "instalacion".
hashCodigotextSí——
codigoVencetimestamp(3)Sí——
politicaRegistroenum PoliticaRegistroNo—Por defecto: invitacion.
cpuPorProyectodecimal(4, 2)No—Por defecto: 1.
memoriaMbPorProyectointegerNo—Por defecto: 1024.
proyectosPorUsuariointegerSí——
smtpHosttextSí——
smtpPuertointegerSí——
smtpUsuariotextSí——
smtpClaveCifradatextSí—AES-GCM con CLAVE_CIFRADO_VARIABLES; nunca en claro.
smtpRemitentetextSí——
sinCorreobooleanNo—Por defecto: false.
actualizadotimestamp(3)No—Se actualiza en cada escritura.

Invitacion

ColumnaTipoNuloClaveDescripción
idtextNoPKPor defecto: uuid().
correotextNo——
hashTokentextNoUQ—
vencetimestamp(3)No——
usadatimestamp(3)Sí——
revocadatimestamp(3)Sí——
creadaPortextNo—adminId.
creadotimestamp(3)No—Por defecto: now().

Índice: (vence)

M5 · Recursos de datos (Derek)

RecursoDatos

ColumnaTipoNuloClaveDescripción
idtextNoPKPor defecto: uuid().
proyectoIdtextNoFKProyecto dueño
tipoTipoRecursoDatosNo—postgres, redis o s3
estadoEstadoRecursoDatosNo—Por defecto: creando
detalletextSí—Motivo del estado error
contenedortextNo—deploya-datos-<subdominio>-<tipo>; también es el host en la red
volumentextNo—Volumen Docker con el mismo nombre
contrasenaCifradatextNo—AES-256-GCM con CLAVE_CIFRADO_VARIABLES; nunca en claro
usoDiscoMbintegerSí—Última medición del volumen
medidoEntimestampSí—Cuándo se midió
creadotimestampNo—Por defecto: now()

Única: (proyectoId, tipo) · Claves foráneas: proyectoId → Proyecto.id (al borrar: Cascade; el trabajador borra antes contenedor y volumen)

RecursoDatosCompartido

ColumnaTipoNuloClaveDescripción
recursoIdtextNoPK, FKRecurso compartido
proyectoIdtextNoPK, FKOtro proyecto del mismo usuario
creadotimestampNo—Por defecto: now()

Claves foráneas: recursoId → RecursoDatos.id y proyectoId → Proyecto.id (al borrar: Cascade). Además, Plan suma maxRecursosDatos y discoDatosMb, y Proyecto suma comandoInicio.

Dominios enumerados

EnumValores en la base
Rolcliente · administrador
EstadoCuentapendiente · activa · suspendida
TipoTokenCuentaverificacion · recuperacion
EstadoSuscripcionactiva · por-vencer · vencida · suspendida · cancelada
ConceptoPagocontratacion · renovacion · cambio-plan
EstadoPagoaprobado · rechazado
RecetaConstrucciondockerfile · node · python · go · estatica
EstadoDespliegueencolado · construyendo · aprovisionando · publicando · saludable · fallido · cancelado · detenido · revirtiendo
DisparadorDesplieguealta · manual · reintento · redespliegue · reversion · variables
Etaparecepcion · construccion · ejecucion · enrutamiento · operacion
EstadoEtapapendiente · en-curso · completada · fallida · omitida
NivelBitacorainfo · aviso · error
PoliticaRegistroabierto · invitacion · cerrado
TipoRecursoDatospostgres · redis · s3
EstadoRecursoDatoscreando · disponible · detenido · error (≠ despliegue y ≠ suscripción)

Seguridad por diseño

Amenaza (STRIDE)Dónde podría ocurrirControl
SuplantaciónRobo de sesiónCookie HttpOnly + SameSite=Lax + Secure; en la base solo la huella SHA-256 del token; cambiar o restablecer la contraseña revoca sesiones
SuplantaciónFuerza bruta de credencialesPolítica de 12 caracteres; límite de intentos por IP en el código de configuración (y en el inicio de sesión con EST-01a)
TamperingVariables del cliente alteradas en la baseAES-256-GCM: un valor alterado falla la autenticación y el despliegue falla sin tocar la versión activa
RepudioAcciones del administradorAccionAdministrativa registra quién, a quién, qué, motivo y cuándo
Information disclosureEnumerar cuentas o recursosRespuestas neutras en registro y recuperación; 404 (no 403) para recursos ajenos; la bitácora nunca muestra valores de variables
Denial of serviceUn cliente acapara el nodo--cpus y --memory del plan; construcciones por mes; concurrencia 1 en el trabajador; tiempo máximo de 10 min por construcción
Elevation of privilegeEscapar del contenedor o llegar a DockerContenedores sin privilegios, CapDrop: ALL, no-new-privileges, red propia; solo el trabajador monta docker.sock; nombres reservados (api, www) prohibidos como subdominio
Information disclosureBases de datos de un cliente alcanzables por otroSin puertos publicados; el recurso vive en la red del proyecto dueño y solo se comparte con proyectos del mismo usuario; contraseña aleatoria de 32 caracteres, cifrada
Elevation of privilegeContenedores de datos con más permisos que una appImágenes fijas de Deploya, CapDrop ALL más solo las capacidades que exige el arranque, no-new-privileges y límite de memoria

Infraestructura y despliegue

Diagrama de despliegue

EC2 m7i-flex.large · 8 GB

Landing · ADR 0011

red de cada proyecto

docker compose

clona

ContenedorPuerto

RecursosDatosPuerto

verificación y avisos

Navegador

Cloudflare · solo DNS
raíz y www → CloudFront · app, api y * → IP elástica

CloudFront + ACM
HSTS · nosniff

S3 privado · landing Astro

Traefik v3 · TLS comodín *.<dominio> por DNS-01

web · Next.js
app.<dominio>

api · NestJS
api.<dominio>

worker
construye y orquesta · docker.sock

PostgreSQL 16
plataforma

Redis 7
colas BullMQ

app del cliente
límites del plan · CapDrop ALL

Postgres 17 + pgvector · Redis · S3
volumen persistente

EBS gp3 80 GB cifrado · snapshot diario DLM, 7 días

GitHub · repos públicos

Resend · correo

Despliegue — Deploya en AWS (piloto) y en local
Figura 19. Despliegue — Deploya en AWS (piloto) y en local

Entornos

EntornoCómo se levantaMotorCorreoDominio
Pruebas unitarias y CIpnpm testEn memoriaConsola—
Desarrollodocker compose upDocker realMailpithttp://<proyecto>.localhost
AWS (piloto)terraform apply y scripts/desplegar-vps.shDocker realResendhttps://<proyecto>.deploya.larasolutions.dev con comodín
Autoalojadoscripts/instalar.shDocker realEl que configure el administradorEl dominio de quien instala

Diseño de la interfaz

La web sigue el design system Deploya v4.1 (docs/diseno/), el mismo con el que está hecho este documento. Sus reglas tienen consecuencias de diseño concretas:

PrincipioQué significa en la interfaz
Sobrio en la forma, vivo en el estadoLa personalidad está en cómo se ve lo que pasa: el riel de cinco etapas y el cursor de la bitácora
Claro por defecto, oscuro siempre disponibleToggle en el encabezado; cada pantalla se revisa en ambos
Neutros cálidos y una sola tintaPapel en claro y grafito en oscuro; nada de paletas por módulo
Señal = lo que está en cursoEl azul solo marca la etapa actual, Construyendo, lo no leído y el foco
Bordes antes que sombrasLa sombra se reserva para diálogos y menús
Geist para hablar, Geist Mono para medirHashes, dominios, variables, puertos y tiempos van en monoespaciada
Informa, no decoraSin gradientes ni emoji; ilustraciones solo en las pantallas de acceso

Los tokens se definen una vez en apps/web/src/app/globals.css en OKLCH (colores perceptualmente uniformes) y los componentes viven en apps/web/src/components, con un catálogo vivo en /sistema. La interfaz tiene 45 fichas de pantalla (una por artboard del canvas), y cada estado lleva palabra e icono, nunca solo color.

TokenUsoMuestra
foreground / primaryTinta y botón principalTinta
signalSolo lo que está en cursoConstruyendo
okSaludable, ActivaSaludable
warnPor vencer, Vencida, límite cercaPor vencer
badFallido, Suspendida, erroresFallido

Decisiones de arquitectura

Cada decisión grande tiene su registro con contexto, alternativas y consecuencias (formato de Michael Nygard) en docs/adr/.

ADRDecisiónAlternativa descartada principal
0001Correo por un adaptador SMTP configurable; Mailpit solo en desarrolloUn SDK por proveedor
0002Cola BullMQ y un trabajador aparte con la misma imagenConstruir dentro de la API
0003Dockerfile del repositorio o receta por stack detectadoBuildpacks (Nixpacks, Paketo)
0004Artefactos inmutables y reversión sin reconstruir, retención de 5Registro de imágenes aparte
0005Traefik v3 con proveedor de archivo y una red por proyectoEtiquetas de Docker
0006Polling cada 3 s en vez de WebSocket o SSEWebSocket
0007Certificado comodín por DNS-01HTTP-01 por proyecto
0008Código abierto (AGPL-3.0) y autoalojado por Strategy de puertosUn fork para autoalojado
—Monolito modular hexagonal y TypeScript de punta a puntaMicroservicios
0009Piloto en una EC2 de AWS creada con TerraformVPS sin infraestructura como código
0010Escala con ECS sobre EC2 y construcciones en Fargate Spot (propuesto)EKS y todo en Fargate
0011Landing en S3 + CloudFront y la web en app.Servir la landing desde la EC2
0012Recursos de datos como contenedores con volumen en el nodoRDS y ElastiCache por proyecto

Calidad y pruebas

1 250pruebas de la API en 166 suites
323pruebas de lógica de la web
97.1 %líneas cubiertas en la API
45changes de OpenSpec archivados
NivelHerramientaQué cubre
Unitarias de dominio y serviciosJest + @nestjs/testingUn it(...) por escenario del spec, con puertos dobles y Reloj falso
Contratos de adaptadoresJestStub y real con los mismos métodos (contratos-stubs.spec.ts)
Lógica de la webnode --testFormato, validaciones, fusión de bitácora, acciones por fila
Smoke de composeGitHub Actions + e2e/smoke.mjsLa plataforma completa arranca y responde
Recorrido e2ee2e/recorrido.mjs (QA-01, se suma en esta entrega)Registro → verificación → sesión → plan → proyecto → despliegue, y sus negativos
Seguridad del repositoriogitleaks, shellcheckSin secretos en el historial; scripts del instalador sin errores

Cada PR corre el job Build y pruebas (pruebas, build y validación de diagramas), Secretos, Scripts y Smoke compose. Nada entra a main sin pasar por PR y por la revisión del dueño del módulo (CODEOWNERS).