Files
recordalexia/docs/adr/adr-004-madurez-operativa-saas.md
Jaume Garriga Maestre 3dfe0218a4 docs: manual con PWA y recuperación de contraseña + ADR-004
Manual: instalación como app (añadir a pantalla de inicio), recuperación
de contraseña con captura real, login actualizado y FAQ ampliado (olvido
de contraseña, borrado de datos). PDF regenerado.
ADR-004: decisiones de madurez operativa SaaS (rate-limiter propio, código
de recuperación hasheado y anti-enumeración, seeder fail-safe, RGPD
verificable, PWA sin stores, producción de superficie mínima).
2026-07-13 22:41:47 +02:00

4.9 KiB
Raw Permalink Blame History

ADR-004: Madurez operativa para la instancia pública (SaaS Fase 1)

Metadata Valor
Estado Aceptado (implementado; despliegue pendiente)
Fecha 2026-07-13
Autor(es) Jaume Garriga Maestre
Supersede —

Contexto

Se decide ofrecer recordaLexia como servicio hospedado para familias sin conocimientos técnicos (abrir URL, registrarse, «añadir a pantalla de inicio»), en lugar de distribuir software instalable. El multi-tenant ya existía (ADR-001); lo que faltaba era madurez operativa para exponer públicamente una app que guarda datos de menores: recuperación de contraseña, freno de fuerza bruta, RGPD mínimo, PWA y despliegue reproducible con backups. Este ADR fija las decisiones técnicas de esa transformación (cambio SDD saas-fase-1; artefactos en engram).

Decisiones y trade-offs

1. Rate-limiter propio en memoria (no bucket4j/Redis)

LoginAttemptService: mapa concurrente por clave (login:email:ip, unlock:sesión, forgot:email:ip), 5 fallos → bloqueo 30 s con backoff x2 (tope 1 h), comprobado antes de evaluar credenciales (429 + Retry-After).

  • Optimizamos: cero dependencias e infraestructura; testeable con el Clock inyectable del proyecto. El PIN de 4 dígitos pasa de fuerza-brutable en minutos a inviable (días).
  • Sacrificamos: el estado vive en memoria de UNA instancia.
  • Asunción que lo invalida: si algún día hay múltiples réplicas del backend, migrar el estado a Redis o similar.

2. Recuperación por código de un solo uso, hasheado, anti-enumeración

Tabla password_reset_token (Liquibase 005): solo el SHA-256 del código (el valor real viaja únicamente en el enlace del email), caducidad 30 min, un solo uso, y emitir uno nuevo invalida los anteriores. La solicitud responde siempre 202 con el mismo cuerpo y el email sale @Async (ni el contenido ni el tiempo de respuesta delatan si una cuenta existe). Restablecer cierra todas las sesiones de la familia. Sin SMTP configurado, MailService cae a modo log (dev/CI funcionan sin correo).

  • Optimizamos: patrón OWASP; un volcado de BD no permite tomar cuentas.
  • Sacrificamos: el enlace del email es el único factor — aceptable porque el email ya es el factor de recuperación universal en consumo.

3. Seeder demo fail-safe

@ConditionalOnProperty(... matchIfMissing = false): sin la propiedad, no se siembra. El compose local y los tests que lo necesitan la activan explícitamente.

  • Optimizamos: un despliegue olvidadizo deja la instancia pública SIN la familia demo y su PIN público. En seguridad, el olvido debe cerrar, no abrir.
  • Sacrificamos: el bootRun local necesita la variable para tener demo.

4. RGPD mínimo verificable

Consentimiento persistido (family.privacy_accepted_at, casilla no premarcada), política de privacidad pública ES/CA, y borrado de cuenta real (POST /api/account/delete, confirmado con contraseña) que elimina todos los datos de la familia — con test que cuenta filas.

5. PWA en lugar de apps nativas

@angular/pwa con el service worker limitado a assets (el /api nunca se cachea: el día del niño siempre fresco). Instalación vía «añadir a pantalla de inicio»: experiencia de app sin stores, sin cuentas de desarrollador ni revisiones.

  • Asunción que lo invalida: si hiciera falta notificación push nativa fiable u offline profundo, reevaluar Capacitor.

6. Producción con superficie mínima en la VM existente

docker-compose.prod.yml: Postgres y backend sin puertos publicados; el frontend solo en 127.0.0.1:8089; el nginx nativo de la VM es el único punto público (TLS con certbot, cabeceras de seguridad, X-Forwarded-For para el rate-limiter). Backups: pg_dump diario con retención y copia off-VM (rclone), con la regla operativa de que un restore no ensayado no cuenta como backup (runbook docs/operacion-saas.md).

Consecuencias

  • (+) Una familia se da de alta y usa la app sin instalar nada; el onboarding lo resuelve la provisión automática (ADR-003).
  • (+) 44 tests backend (incl. migración contra Postgres real) fijan el comportamiento de seguridad: anti-enumeración, caducidad, un solo uso, 429.
  • (−) El operador (una persona) asume backups, certificados y SMTP → mitigado con runbook, cron y monitorización ya existente en la VM.
  • (→) CI/CD con Gitea Actions queda como cambio hermano (saas-cicd).

Criterios de éxito

  • Flujo completo de recuperación verificado E2E (incl. sesiones invalidadas).
  • Fuerza bruta frenada con test (login y PIN).
  • Instancia sin demo por defecto (test).
  • PWA instalable con service worker activo.
  • Despliegue público con restore ensayado (pendiente: SMTP + DNS del usuario).

Referencias

  • Artefactos SDD en engram: sdd/saas-fase-1/{proposal,spec,design,tasks,apply-progress}
  • ADR-001 (auth propia), ADR-003 (provisión inicial)
  • backend/.../security/, deploy/, docs/operacion-saas.md