Diagramas del sistema¶
Diagramas Mermaid de los flujos principales del sistema. Renderizados nativamente por el plugin mkdocs-mermaid2-plugin.
Topología de despliegue (modo producción)¶
flowchart LR
Internet((Internet))
subgraph CF["Cloudflare Edge"]
DNS["DNS
custodiam.es"]
Tunnel["Tunnel
cloudflared"]
Pages["Pages
app.custodiam.es"]
BookPages["Pages
docs.custodiam.es"]
end
subgraph Host["PC anfitrión (modo prod)"]
Compose["Docker Compose
--profile tunnel"]
subgraph Stack["Stack de servicios"]
API_Container["custodiam-api
:8000"]
KC_Container["Keycloak 26
:8080"]
DB_Container[("PostgreSQL 15")]
NTFY_Container["ntfy
:80"]
end
end
Internet -->|HTTPS| DNS
DNS -->|api.custodiam.es
auth.custodiam.es
ntfy.custodiam.es| Tunnel
DNS -->|app.custodiam.es| Pages
DNS -->|docs.custodiam.es| BookPages
Tunnel -.cloudflared connect.-> API_Container
Tunnel -.cloudflared connect.-> KC_Container
Tunnel -.cloudflared connect.-> NTFY_Container
API_Container --> DB_Container
KC_Container --> DB_Container
API_Container -.admin API.-> KC_Container
Flujo OAuth2 + PKCE (móvil)¶
sequenceDiagram
participant U as Usuario
participant App as Flutter App
(Android/iOS)
participant KC as Keycloak
participant API as custodiam-api
U->>App: Toca "Iniciar sesión"
App->>App: Genera code_verifier + code_challenge
(_pendingGrant en memoria)
App->>KC: Abre Custom Tab con
authorize?code_challenge=...
KC-->>U: Pantalla de login
U->>KC: Credenciales (usuario + password)
KC-->>App: Redirige a
es.custodiam://callback?code=...
Note over App: Deep link captura el code
App->>KC: POST /token
code + code_verifier
KC-->>App: access_token + refresh_token
App->>App: Guarda en flutter_secure_storage
App->>API: GET /me
Authorization: Bearer
API->>API: Valida JWT localmente con PyJWT
(usa JWKS cacheado)
API-->>App: 200 OK + datos del voluntario
Flujo OAuth2 + PKCE (web)¶
sequenceDiagram
participant U as Usuario
participant Web as PWA Web
(Flutter Web)
participant SS as sessionStorage
participant KC as Keycloak
participant API as custodiam-api
U->>Web: Click "Iniciar sesión"
Web->>Web: Genera code_verifier + code_challenge
Web->>SS: Persiste code_verifier
(clave: custodiam.oauth.code_verifier)
Web->>KC: window.location = authorize?code_challenge=...
(webOnlyWindowName: '_self')
Note over Web: ⚠️ Recarga completa del navegador
KC-->>U: Pantalla de login
U->>KC: Credenciales
KC-->>Web: Redirige a
https://app.custodiam.es/callback?code=...
Note over Web: GoRouter (PathUrlStrategy) matcha /callback
Web->>SS: Lee code_verifier
Web->>KC: POST /token
code + code_verifier
KC-->>Web: access_token + refresh_token
Web->>SS: Limpia code_verifier
Guarda tokens en sessionStorage
Web->>API: GET /me
Authorization: Bearer
API-->>Web: 200 OK + datos del voluntario
Asimetría móvil/web — ADR-023
La diferencia clave entre móvil y web: en móvil la app sobrevive la redirección al IdP (deep link vuelve a la misma instancia con el _pendingGrant intacto en memoria); en web, la navegación a Keycloak recarga la PWA completa, perdiendo el estado en memoria. Por eso web necesita persistir el code_verifier en sessionStorage antes de redirigir. Se implementa con dos AuthService distintos seleccionados vía kIsWeb. Detalle completo en [ADR-023 del repo privado].
Flujo de notificación de emergencia¶
sequenceDiagram
participant Coord as Coordinador
(usuario)
participant App as App Coord
participant API as custodiam-api
participant FCM as Firebase FCM
participant NTFY as ntfy
participant Volunt as App Voluntario
(N dispositivos)
Coord->>App: Crea convocatoria de emergencia
App->>API: POST /servicios
tipo=emergencia, voluntarios_filtrados
API->>API: Filtra voluntarios disponibles + permisos
API->>FCM: Envía push a tokens FCM
(canal principal)
alt FCM responde 200
FCM-->>Volunt: Push notification
(notificación + datos del servicio)
Note over Volunt: Voluntario toca → abre app en pantalla del servicio
else FCM falla o timeout > 5s
API->>NTFY: Publica al canal /custodiam-emergencia
(fallback ADR-005)
NTFY-->>Volunt: Push via ntfy
(canal secundario)
Note over API: Log de fallo de FCM
+ telemetría para análisis
end
Volunt->>API: PATCH /servicios/{id}/voluntarios/{me}
{respuesta: "acepto" | "rechazo"}
API-->>App: Notifica al coordinador
(via FCM tambien)
Despliegue del book de documentación¶
flowchart LR
Dev["Dev (commit + push)"]
GH["GitHub Actions
workflow deploy.yml"]
Pages["GitHub Pages
branch gh-pages"]
DNS["Cloudflare DNS
CNAME docs → custodiam.github.io
(DNS only, sin proxy)"]
User((Usuario))
Dev -->|push a main| GH
GH -->|uv sync + mkdocs build| GH
GH -->|peaceiris/actions-gh-pages@v4| Pages
User -->|https://docs.custodiam.es| DNS
DNS -->|resuelve a IP de GitHub Pages| Pages
User -.fallback.->|custodiam.github.io/custodiam-book/| Pages
Resiliencia vendor-lock-free
El sitio sigue siendo accesible en https://custodiam.github.io/custodiam-book/ aunque Cloudflare desaparezca: solo se perdería el dominio "bonito" docs.custodiam.es. Cloudflare se reserva como CDN opcional futuro (toggle Proxied reversible). Decisión en [ADR-027].
Referencias¶
- Stack técnico — tecnologías concretas con versiones.
- ADRs — decisiones arquitectónicas que sostienen estos diagramas.