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.
Comandos del ciclo de desarrollo
Sección titulada «Comandos del ciclo de desarrollo»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:
cd emaclinicnpm installnpm 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 --noEmitnpm test # Vitest (520 unit + integration tests)npm run build # Vite build (SPA) + Wrangler build (Worker)npm run deploy # build + wrangler deployHosting y servicios externos
Sección titulada «Hosting y servicios externos»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.
| Componente | Plataforma | Función |
|---|---|---|
| API | Cloudflare Workers (worker emaclinic) | Backend Hono y handlers de 53 rutas |
| SPA | Cloudflare Pages con assets binding integrado | React 19 con SPA estática |
| FHIR | Aidbox compartido en https://vablraitnz.edge.aidbox.app/fhir | Datos clínicos canónicos con ORGBAC server-side |
| BD operacional | Supabase emahub (proyecto bzikofdvfvchntvfzyni, región us-east-1) | Tablas operacionales con RLS por tenant |
Secrets requeridos
Sección titulada «Secrets requeridos»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.
Acceso a Supabase
Sección titulada «Acceso a Supabase»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:
| Secret | Para qué |
|---|---|
SUPABASE_URL | https://bzikofdvfvchntvfzyni.supabase.co |
SUPABASE_ANON_KEY | JWT pública (RLS-safe) |
SUPABASE_SERVICE_ROLE_KEY | Server-side (auth + cross-tenant queries puntuales) |
Aidbox y ORGBAC
Sección titulada «Aidbox y ORGBAC»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:
| Secret | Para qué |
|---|---|
AIDBOX_BASE_URL | URL del servidor Aidbox |
AIDBOX_TOKEN_URL | Endpoint OAuth token |
AIDBOX_SECRETS_ENCRYPTION_KEY_V1 | Master key AES-GCM (32 bytes base64) para decryptar tenant_aidbox_credentials |
Comunicación inter-service
Sección titulada «Comunicación inter-service»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:
| Secret | Para qué |
|---|---|
PLATFORM_API_KEY | Shared secret para /api/platform/* (provisioning desde emavault) |
EMI_API_KEY | Bearer para emi.emahealth.io (proxy /api/emi/chat) |
EMI_BASE_URL | URL del servicio EMI (default https://emi.emahealth.io) |
Integraciones externas
Sección titulada «Integraciones externas»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:
| Secret | Para qué |
|---|---|
IMED_API_URL | Bonos de segmento privado de salud (en Chile, IMED) — fallback global |
IMED_API_KEY | API key del cliente |
LOOPS_API_URL | Telemedicina vía proxy a LOOPS (decisión F174) |
LOOPS_API_KEY | API key LOOPS |
Variables de entorno locales
Sección titulada «Variables de entorno locales»Para desarrollo local el worker espera un archivo .dev.vars en la
raíz del repositorio que no debe commitearse al repo:
SUPABASE_URL=https://bzikofdvfvchntvfzyni.supabase.coSUPABASE_ANON_KEY=...SUPABASE_SERVICE_ROLE_KEY=...AIDBOX_BASE_URL=https://vablraitnz.edge.aidbox.app/fhirAIDBOX_TOKEN_URL=https://vablraitnz.edge.aidbox.app/auth/tokenAIDBOX_SECRETS_ENCRYPTION_KEY_V1=...PLATFORM_API_KEY=...EMI_API_KEY=...EMI_BASE_URL=https://emi.emahealth.ioPara 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.
Custom domains
Sección titulada «Custom domains»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:
npx wrangler domains add <slug>.emahealth.ioLa 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.
Migraciones de Supabase
Sección titulada «Migraciones de Supabase»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).
CI y deploy
Sección titulada «CI y deploy»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.
Endpoints públicos sin auth
Sección titulada «Endpoints públicos sin auth»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):
| Path | Por qué |
|---|---|
GET /api/health | Health check |
GET /api/config | Bootstrap del SPA (devuelve SUPABASE_URL y SUPABASE_ANON_KEY para que el cliente inicialice Supabase Auth) |
GET /api/auth-config/:slug | Configuració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/signin | OTP request del paciente |
Todo lo demás pasa por authMiddleware y tenantMiddleware.
Errores HTTP comunes
Sección titulada «Errores HTTP comunes»La tabla siguiente resume los códigos de error más frecuentes y sus causas típicas:
| Código | Causa típica |
|---|---|
400 | Body inválido o falta X-Tenant-Slug cuando aplica |
401 | Bearer JWT inválido o expirado |
403 | RBAC: rol del usuario no está en requireRole(...), o tenant inactivo |
404 | Recurso FHIR o row de Supabase no existe |
409 | Conflicto FHIR (versión optimistic lock) o duplicado |
422 | Validación FHIR fallida (perfil incumple) |
500 | Error interno (revisar logs Cloudflare Workers) |
503 | Aidbox, Supabase o EMI no responden |
Observabilidad
Sección titulada «Observabilidad»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.