Ir al contenido

Deploy y entornos

EMA Clinic se despliega como un único worker de Cloudflare que sirve tanto la API Hono como los assets estáticos del SPA, complementado por dos servicios externos: Aidbox para FHIR y Supabase para datos operacionales. La operación es deliberadamente simple: un comando empaqueta y publica todo, y el resto de los componentes (custom domains, migrations, secrets) se administran desde sus respectivos dashboards. Esta página cubre el ciclo completo de despliegue, los secrets requeridos, las migraciones de base de datos y las convenciones de CI.

Los comandos del ciclo cubren los cuatro estados habituales: desarrollo local, type-checking, testing y deploy. La separación entre npm run dev (Vite con proxy a la API) y npm run dev:worker (Wrangler con la API real) permite trabajar en frontend con la API mockeada cuando conviene, o en backend con la SPA hot-reload cuando el cambio es de schema:

Ventana de terminal
cd emaclinic
npm install
npm run dev # Vite dev server (SPA en :5173 + proxy API a :8787)
npm run dev:worker # Wrangler local (API en :8787)
npm run typecheck # tsc --noEmit
npm test # Vitest (520 unit + integration tests)
npm run build # Vite build (SPA) + Wrangler build (Worker)
npm run deploy # build + wrangler deploy

El producto se apoya en cuatro componentes, cada uno con responsabilidad acotada. La separación es deliberada: cada servicio opera en su propio plano y se mantiene aislado de los demás, lo que evita acoplamientos innecesarios y simplifica la rotación independiente de credenciales.

ComponentePlataformaFunción
APICloudflare Workers (worker emaclinic)Backend Hono y handlers de 53 rutas
SPACloudflare Pages con assets binding integradoReact 19 con SPA estática
FHIRAidbox compartido en https://vablraitnz.edge.aidbox.app/fhirDatos clínicos canónicos con ORGBAC server-side
BD operacionalSupabase emahub (proyecto bzikofdvfvchntvfzyni, región us-east-1)Tablas operacionales con RLS por tenant

El worker depende de un conjunto de secrets configurados via wrangler secret put <NOMBRE>. Los secrets se agrupan por función: acceso a Supabase, conexión con Aidbox y ORGBAC, comunicación inter-service y fallbacks de integraciones externas.

Tres secrets controlan el acceso al proyecto Supabase, cada uno con un alcance distinto. La anon key se usa en queries con RLS aplicado y es la que se sirve al cliente desde /api/config para que el SPA inicialice Supabase Auth. La service role key se reserva para operaciones server-side que requieren bypass controlado de RLS, típicamente queries cross-tenant durante provisioning o auditoría:

SecretPara qué
SUPABASE_URLhttps://bzikofdvfvchntvfzyni.supabase.co
SUPABASE_ANON_KEYJWT pública (RLS-safe)
SUPABASE_SERVICE_ROLE_KEYServer-side (auth + cross-tenant queries puntuales)

Tres secrets habilitan la conexión con Aidbox. Los dos primeros son URLs estáticas; el tercero es la master key AES-GCM con la que se desencriptan las credenciales OAuth per-tenant que viven en tenant_aidbox_credentials:

SecretPara qué
AIDBOX_BASE_URLURL del servidor Aidbox
AIDBOX_TOKEN_URLEndpoint OAuth token
AIDBOX_SECRETS_ENCRYPTION_KEY_V1Master key AES-GCM (32 bytes base64) para decryptar tenant_aidbox_credentials

El producto se comunica con dos servicios internos del ecosistema. La key de plataforma protege el endpoint /api/platform/* que usa EMA Vault para provisionar tenants; la key de EMI permite invocar al asistente conversacional vía proxy server-side:

SecretPara qué
PLATFORM_API_KEYShared secret para /api/platform/* (provisioning desde emavault)
EMI_API_KEYBearer para emi.emahealth.io (proxy /api/emi/chat)
EMI_BASE_URLURL del servicio EMI (default https://emi.emahealth.io)

Estas integraciones usan configuración per-tenant en tenant_config cuando es posible; los secrets siguientes son fallbacks globales para los tenants que no tienen configuración propia. En el plug-in chileno, la integración con bonos del segmento privado de salud opera vía el cliente IMED:

SecretPara qué
IMED_API_URLBonos de segmento privado de salud (en Chile, IMED) — fallback global
IMED_API_KEYAPI key del cliente
LOOPS_API_URLTelemedicina vía proxy a LOOPS (decisión F174)
LOOPS_API_KEYAPI key LOOPS

Para desarrollo local el worker espera un archivo .dev.vars en la raíz del repositorio que no debe commitearse al repo:

Ventana de terminal
SUPABASE_URL=https://bzikofdvfvchntvfzyni.supabase.co
SUPABASE_ANON_KEY=...
SUPABASE_SERVICE_ROLE_KEY=...
AIDBOX_BASE_URL=https://vablraitnz.edge.aidbox.app/fhir
AIDBOX_TOKEN_URL=https://vablraitnz.edge.aidbox.app/auth/token
AIDBOX_SECRETS_ENCRYPTION_KEY_V1=...
PLATFORM_API_KEY=...
EMI_API_KEY=...
EMI_BASE_URL=https://emi.emahealth.io

Para tests de integración locales además se necesita un tenant configurado en la base con sus credenciales encriptadas, ya que el flujo completo de FHIR depende de poder resolver el cliente per-tenant desde tenant_aidbox_credentials.

El tenant demo clinic.emahealth.io está mapeado manualmente desde el dashboard de Cloudflare Workers. Los tenants nuevos reciben un subdominio bajo el patrón <slug>.emahealth.io, y el provisioning desde EMA Vault llama internamente al comando:

Ventana de terminal
npx wrangler domains add <slug>.emahealth.io

La automatización completa del registro DNS está contemplada para Wave 4; hoy el comando se ejecuta como parte del script de provisioning que vive en EMA Vault.

Las migrations viven en supabase/migrations/ con prefijo numérico. Son SQL puro y se aplican via el MCP de Supabase con apply_migration (no execute_sql, porque apply_migration da idempotencia y trazabilidad de qué migración se ejecutó cuándo):

supabase/migrations/
├── 001_core.sql # tenants, user_roles, tenant_config, ...
├── 002_audit.sql # audit_logs
├── 003_pricing.sql # service_charge_items, agreements, ...
├── 004_scheduling.sql # schedule_patterns, exceptions, ...
├── 005_urgencia.sql # triage_esi_audit, vital_signs_reference_ranges
├── 006_emi_access.sql # tenant_emi_access (compartido emashared)
├── 007_patient_sequence.sql # patient_sequence per-tenant
└── ...

La política del proyecto exige que cada DDL quede registrado en una migration nueva. Después de aplicarla, correr get_advisors(type='security') y resolver los ERRORs antes de cerrar el sprint es regla de oro del Plan Consolidado (§12.5).

El producto opera con un modelo de CI/CD ligero que se apoya en Cloudflare más que en GitHub Actions. Cloudflare Pages tiene integración Git directa con el repo: cada push a main dispara build automático del SPA (Vite) en Pages, lo que mantiene el frontend siempre sincronizado. El Worker se deploya manualmente con npm run deploy desde local cuando hay cambios de backend o configuración.

Los tests de integración están gated por variables de entorno (TEST_AIDBOX_*, TEST_SUPABASE_*); si las variables no están presentes, los tests se omiten silenciosamente. Esto permite que npm test funcione tanto en local como en entornos sin acceso a servicios externos. El type check debe pasar siempre antes de cada deploy. No hay GitHub Actions —deliberadamente—: Cloudflare maneja el build del SPA y el resto del despliegue es manual, lo que mantiene cada release como una decisión consciente.

Cinco paths del Worker no requieren Bearer JWT. La razón en cada caso es operativa: o son endpoints de bootstrap (que el cliente usa antes de tener token), o son endpoints públicos por diseño (booking público, OTP del paciente):

PathPor qué
GET /api/healthHealth check
GET /api/configBootstrap del SPA (devuelve SUPABASE_URL y SUPABASE_ANON_KEY para que el cliente inicialice Supabase Auth)
GET /api/auth-config/:slugConfiguración de auth providers del tenant (lo necesita el login antes de saber el user)
POST /api/public/booking/:slug/*Booking público scoped por slug
POST /api/portal/auth/signinOTP request del paciente

Todo lo demás pasa por authMiddleware y tenantMiddleware.

La tabla siguiente resume los códigos de error más frecuentes y sus causas típicas:

CódigoCausa típica
400Body inválido o falta X-Tenant-Slug cuando aplica
401Bearer JWT inválido o expirado
403RBAC: rol del usuario no está en requireRole(...), o tenant inactivo
404Recurso FHIR o row de Supabase no existe
409Conflicto FHIR (versión optimistic lock) o duplicado
422Validación FHIR fallida (perfil incumple)
500Error interno (revisar logs Cloudflare Workers)
503Aidbox, Supabase o EMI no responden

El producto integra tres niveles de observabilidad. Cloudflare Workers Observability se habilita en wrangler.toml y captura logs de tracing del worker. El audit log en emaclinic.audit_logs registra toda mutación con tenant_id, user_id, action, resource_type y changes JSON, lo que permite reconstruir incidentes y responder a auditorías. Y un logger estructurado (con formato similar al de EMI) facilita correlacionar requests con sus efectos secundarios cuando un problema cruza varios servicios.