# 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 - [x] Flujo completo de recuperación verificado E2E (incl. sesiones invalidadas). - [x] Fuerza bruta frenada con test (login y PIN). - [x] Instancia sin demo por defecto (test). - [x] 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`