03 — Arquitectura técnica
Topología por colegio
Internet
│
[nginx] ── TLS (Let's Encrypt vía Dokploy)
├── / → SPA estática (staff)
├── /api/* → [BFF NestJS]
└── /files/* → volumen de archivos (URLs firmadas)
│
[BFF NestJS] ← única cara pública del backend
│ red interna Docker
[Core NestJS] ← dominio, Prisma, workers pg-boss
│
[PostgreSQL] ← única pieza de infraestructura de datos
- BFF NestJS separado (decisión explícita): capa de anticorrupción hacia los clientes. Se mantiene delgado: auth/sesión, agregación de llamadas, shaping de respuestas por cliente (web vs móvil), rate limiting. No contiene dominio ni toca la BD.
- Core NestJS: dominio DDD soft + vertical slice, Prisma/PostgreSQL, casos de uso, colas. Solo accesible en la red interna de Docker — jamás expuesto a internet.
- Solo PostgreSQL: sin Redis ni brokers. Las colas corren sobre el mismo Postgres con pg-boss.
- La app Flutter consume el mismo BFF (
/api/v1) con contrato versionado; superficie móvil = endpoints de lectura + login/refresh + cambio de contraseña + registro de token FCM.
Autenticación
- Web (SPA): sesión vía BFF con cookies httpOnly same-origin (sin CORS).
- Móvil: JWT access + refresh token con rotación y revocación.
- Sin registro abierto; contraseña temporal con cambio obligatorio. Rate limiting en login. Contraseñas con Argon2/bcrypt.
Eventos y consistencia (regla de oro)
- Nada crítico depende del event emitter en memoria.
@nestjs/event-emittersolo para efectos internos no críticos, y los eventos de dominio se despachan después del commit (se coleccionan en el agregado, se emiten post-commit). Un listener roto o lento no cuelga ni revierte nada. - Todo efecto que no puede perderse va por outbox: sync Moodle, push FCM, email, generación de boletas. El evento se inserta en la tabla
outboxdentro de la misma transacción del cambio de dominio; un worker pg-boss lo procesa con reintentos e idempotencia. Si el proceso muere, nada queda colgado ni fuera de transacción.
Trabajos en cola (pg-boss)
- Boletas PDF (picos de fin de período), importaciones Excel, despacho de outbox (Moodle/FCM/email), reportes.
- Idempotencia por clave de trabajo; reintentos con backoff; dead-letter visible para admin.
Logger (helper obligatorio)
- Helper sobre pino. Reglas:
- Jamás lanza — try/catch interno; un log no puede dañar una ejecución.
- Masking por defecto (redact): cédulas, nombres, contraseñas, tokens, emails.
- Cada línea lleva:
correlationId(por request/sesión, propagado con AsyncLocalStorage incluso a workers), UUID de la entidad afectada, y el paso/acción. Se investiga cruzando UUID contra la BD; el log en sí no filtra datos sensibles (LOPDP).
Datos
- Prisma + migraciones versionadas; migración corre al boot del contenedor (entrypoint).
- UUID v7 públicos en todas las entidades.
- Índices diseñados con el schema (cédula, listados por paralelo/período, FKs); endpoints de listado revisados con
EXPLAIN; toda query nueva justifica su índice. - Paginación obligatoria en toda lista (API y UI). Optimistic locking (campo
version) en entidades editadas concurrentemente (notas, matrícula).
Frontend (SPA staff)
- Vite + React + MUI (Material): prioridad eficiencia y funcionalidad sobre estilo; animaciones sobrias.
- Tablas siempre paginadas (server-side), formularios con validación compartida (zod en BFF y front).
Estándares de ingeniería
- TDD en core: dominio y casos de uso test-first. Integración contra Postgres real (Testcontainers) — no mocks de Prisma.
- SOLID en back y front; vertical slice por feature en el core.
- Front: unit/component con Vitest + Testing Library; smoke visual con Playwright (flujos clave + screenshots para validar que se vea bien).
- Los tests de listados usan seeds/factories con volumen (500+ alumnos) para probar paginación, orden y filtros de verdad.
- E2E de humo sobre el stack dockerizado completo antes de cada release.
Almacenamiento de archivos
Volumen local del VPS (documentos de matrícula, adjuntos, boletas generadas), servido por nginx con URLs firmadas, incluido en backups. El storage se abstrae tras una interfaz (FileStorage) para poder migrar a S3-compatible sin tocar dominio.