Ir al contenido

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.

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):

Usuario web

well.emahealth.io

PWA / Service Worker

React SPA

Cloudflare Pages

Hono API

Cloudflare Worker

Crons

weekly summary · streak at risk

Supabase Postgres

proyecto emawell

schema public

pillars · daily_logs · streaks ·

achievements · subscriptions · …

schema fhir

fhir_resources JSONB

Stripe

checkout + webhooks

Loops.so

email transaccional + nurture

Claude API

onboarding IA · sugerencias · solo Pro

Web Push

VAPID

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.

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 deploy

El 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.

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.

Loops.soSupabase (public + fhir)fhir-observationsachievement-enginestreak-engineHono WorkerReact SPAUsuarioLoops.soSupabase (public + fhir)fhir-observationsachievement-enginestreak-engineHono WorkerReact SPAUsuarioAbre /checkin1Marca pilares done · valor · energía · mood2POST /api/checkins3Inserta daily_log + entries (schema public)4Genera FHIR Observations5Inserta en schema fhir6Recalcula racha (general + por pilar)7Update streaks8Detecta achievements desbloqueados9Insert achievements unlocked10Trigger nurture si aplica11200 + insight + nueva racha12Pantalla post-check-in13

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).

Dos crons corren periódicamente para mantener el ciclo de comunicación con el usuario fuera del flujo de UI:

CronCuándo correQué hace
Weekly summaryDomingo 00:00 UTCEmail de resumen semanal vía Loops.so
Streak at riskDiario 20:00 UTCEmail 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.