Arquitectura
La arquitectura de EMA Well combina dos decisiones que la separan
del resto del ecosistema. La primera es operar en un proyecto
Supabase aislado: ningún dato individual de salud comparte
infraestructura con los productos institucionales. La segunda es
modelar las observaciones FHIR como JSONB en un schema dedicado
fhir, en lugar de delegar a una instancia de Aidbox: para el
volumen de datos por usuario, mantener todo dentro del mismo
Postgres simplifica las queries operacionales sin perder
portabilidad estándar. Sobre eso opera un Worker Hono compacto, una
SPA React con shadcn/ui y dos crons que mantienen el ciclo de vida
del usuario.
Vista general de componentes
Sección titulada «Vista general de componentes»El gráfico siguiente muestra cómo se conectan las piezas. La PWA es la capa que el usuario ve; la SPA React vive en Cloudflare Pages, la API en un worker Hono, y dos crons disparan tareas asíncronas (resumen semanal y alerta de racha en riesgo):
Tres servicios externos completan el sistema. Stripe maneja la
suscripción Pro: el checkout, los webhooks de pago y la
sincronización del tier en subscriptions. Loops.so gestiona el
correo transaccional (resumen semanal, nurturing post-check-in,
recuperación de cuenta) bajo el dominio mail.emahealth.io. Y
Claude API se invoca solo en el plan Pro, vía el agente especializado
de emaemi, para el onboarding personalizado y las sugerencias
diarias.
Estructura de carpetas
Sección titulada «Estructura de carpetas»El repositorio sigue una convención simple: la SPA en
src/client/, la API Hono en src/api/ agrupada por dominio
funcional, y la lógica de negocio en src/lib/ como motores puros
fácilmente testables. La separación deliberada entre handlers de
API y motores de dominio permite que la lógica de cálculo de racha
o detección de achievements se pruebe en isolación sin levantar
el stack completo.
emawell/├── src/│ ├── client/ # React SPA│ │ ├── pages/ # Routes principales (dashboard, checkin, programa, semana)│ │ ├── components/ # Componentes reusables (shadcn/ui base)│ │ ├── lib/ # Helpers cliente (api, auth, etc.)│ │ ├── hooks/ # React hooks custom│ │ └── styles/ # Tailwind + design tokens│ ├── api/ # Hono routes — 14 módulos│ │ ├── auth.ts│ │ ├── pilares.ts│ │ ├── checkins.ts│ │ ├── stats.ts│ │ ├── pagos.ts│ │ └── …│ └── lib/ # Business logic│ ├── streak-engine.ts # Cálculo de racha + freeze (Pro)│ ├── achievement-engine.ts # Detección de achievements unlock│ ├── fhir-observations.ts # Generación de FHIR Observation por check-in│ ├── payment-gateway.ts # Webhooks Stripe│ ├── loops.ts # Email transaccional + nurture│ └── constants.ts # LOINC codes, tipos de pilar, etc.├── docs/ # SPEC v0.1, TASK (8 sprints, 82 features), ADRs├── public/ # PWA manifest, service worker└── scripts/ # Build y deployEl routing de la API es modular por archivo bajo /api/*; el del
frontend usa React Router con páginas en src/client/pages/. Hay
catorce módulos de API, lo que mantiene el sistema lo
suficientemente pequeño como para que un developer pueda tener el
mapa completo en la cabeza.
Flujo de un check-in diario
Sección titulada «Flujo de un check-in diario»El check-in es el evento más frecuente del producto. Lo que el usuario percibe como un submit instantáneo dispara, en el lado del servidor, una secuencia coordinada de cinco operaciones que persisten datos en dos schemas, recalculan estado derivado, y disparan eventos de comunicación cuando corresponde.
La inserción en daily_log y entries mantiene la estructura
operacional optimizada para queries de UI (heatmaps, listados,
filtros). La generación paralela en el schema fhir produce los
recursos Observation en formato canónico LOINC, listos para
exportación. La separación entre ambos schemas significa que las
queries del producto son rápidas (sin pasar por JSONB) y la
exportación FHIR es estándar (sin transformación).
Crons programados
Sección titulada «Crons programados»Dos crons corren periódicamente para mantener el ciclo de comunicación con el usuario fuera del flujo de UI:
| Cron | Cuándo corre | Qué hace |
|---|---|---|
| Weekly summary | Domingo 00:00 UTC | Email de resumen semanal vía Loops.so |
| Streak at risk | Diario 20:00 UTC | Email o push si el usuario no hizo check-in ese día |
El resumen semanal solo se envía a usuarios con al menos dos check-ins esa semana —menos que eso indica desconexión y un correo con un heatmap vacío no aporta valor. La alerta de racha en riesgo respeta el patrón horario del usuario y solo dispara push si el usuario habilitó notificaciones explícitamente vía VAPID.