Deploy y entornos
EMA Lab se despliega como un único worker de Cloudflare con dos backends de datos —Aidbox para FHIR clínico y Supabase para operacional— más un Hub Docker opcional que vive en cada laboratorio con analizadores físicos en site. Esta página recorre los entornos, los secrets que necesita el worker, las migraciones de base de datos y las consideraciones específicas del Hub on-premise.
Entornos
Sección titulada «Entornos»| Entorno | Worker | URL | Aidbox |
|---|---|---|---|
| Dev local | wrangler dev | http://localhost:8787 | mock o Aidbox staging |
| Staging | lab-staging.workers.dev | (interno) | Aidbox staging |
| Producción | emalab.emahealth.io | {slug}.lab.emahealth.io | Aidbox prod |
Comandos básicos
Sección titulada «Comandos básicos»cd /home/cristianruiz/Dev/emahealth/emalabnpm install
npm run dev # Vite dev (frontend) :5173npm run dev:worker # Wrangler dev (backend Hono) :8787
npm run build # Vite build → dist/npm run deploy # build + wrangler deploy
npm run typecheck # tsc --noEmitnpm run guards # Wave 3 regression guards (scripts/guards.sh)npm run smoke:emi # Post-deploy EMI smokenpm run test # Vitest run (todas las suites)npm run test:unit # Suite unitnpm run test:integration # Suite integration (timeout 15s)npm run test:regression # Suite regressionnpm run test:perf # Suite performance (timeout 30s)npm run test:security # Suite security (timeout 20s)npm run test:coverage # con coverageCustom domains (multi-tenant)
Sección titulada «Custom domains (multi-tenant)»Cada tenant tiene su subdominio bajo lab.emahealth.io:
| Tenant | URL |
|---|---|
| Lab Aurora | aurora.lab.emahealth.io |
| Lab Andes | andes.lab.emahealth.io |
| Lab Sur | sur.lab.emahealth.io |
Mapping vía wildcard en CF Workers + tenantMiddleware resuelve el
slug del subdominio. Provisioning automático desde EMA Vault vía
POST /api/platform/tenants (X-Platform-Key).
Secrets
Sección titulada «Secrets»Agrupados por propósito. Configurar en CF dashboard o
wrangler secret put NAME por entorno.
Auth + DB
Sección titulada «Auth + DB»| Secret | Propósito |
|---|---|
SUPABASE_URL | URL del proyecto emahub |
SUPABASE_SERVICE_ROLE_KEY | Service role (uso interno middleware) |
SUPABASE_ANON_KEY | Anon key servida via /api/config |
JWT_SECRET | Validación JWT Supabase |
FHIR (Aidbox)
Sección titulada «FHIR (Aidbox)»| Secret | Propósito |
|---|---|
AIDBOX_BASE_URL | URL del Aidbox prod |
AIDBOX_PER_TENANT_KEY | Clave AES-GCM para decrypt tenant_aidbox_credentials (S9) |
Conectores y servicios externos
Sección titulada «Conectores y servicios externos»| Secret | Propósito |
|---|---|
LOOPS_API_KEY | Loops.so transactional emails |
LOOPS_DLQ_TRANSACTIONAL_ID | Template DLQ alert |
LOOPS_DLQ_RECIPIENT | Email destinatario DLQ |
EMI_BASE_URL | Default https://emi.emahealth.io (override en dev) |
EMALAB_API_KEY | Server-side proxy a EMI (NUNCA en frontend) |
EMI_WORKER_URL | Endpoint legacy evaluateEMI |
PLATFORM_API_KEY | Validación X-Platform-Key (provisioning desde Vault) |
HUB_JWT_SECRET | Firma de tokens Hub on-prem |
Facturación electrónica
Sección titulada «Facturación electrónica»billing_config por tenant tiene dte_api_key. El secret global
DTE_BASE_URL apunta al worker emadte. El callback usa la API key
per-tenant (L026).
Bindings
Sección titulada «Bindings»# wrangler.toml (extracto)
[[r2_buckets]]binding = "DOCS_BUCKET"bucket_name = "emalab-docs" # nombre prod (ver L062 — un bucket por worker)preview_bucket_name = "emalab-docs-preview"
[[kv_namespaces]]binding = "RATE_LIMIT_KV"id = "..."
[triggers]crons = [ "*/5 * * * *", # retry-pending (FHIR + HL7v2 outbound) "*/15 * * * *", # fhir-emission-retry (stewardship dual-write) "0 3 1 1,4,7,10 *" # stewardship-quarterly (CLSI M39)]Migraciones Supabase
Sección titulada «Migraciones Supabase»# Aplicar migraciones a hubcd supabase/psql postgresql://postgres@db.bzikofdvfvchntvfzyni.supabase.co/postgres \ -f migrations/022_routing_engine.sql
# Verificar estadoSELECT version, applied_at FROM emalab.schema_migrations ORDER BY version;Migraciones 001-022 (skip 010 deprecated) ya aplicadas en prod
hub bzikofdvfvchntvfzyni (L103). Cualquier nueva migración debe:
- Numerar secuencial (
023_*.sql). - Documentarse en
docs/decisions.mdcon L###. - Tener test de regression que verifique RLS + indexes.
- NO romper RLS — verificar con el patrón existente.
Seed Aidbox
Sección titulada «Seed Aidbox»# Cargar seed FHIR en Aidbox (organizations, locations, practitioners,# patients, activity definitions, demo data)bash scripts/seed-aidbox.shCarga:
- 8 Organizations (3 clients + 5 departments)
- 9 Locations
- 6 Practitioners
- 20 Patients (con RUT + identifier MR)
- 160 ActivityDefinitions (LOINC, 7 secciones HEM/QUI/INM/COA/URO/TDM/MIS)
- 50 ServiceRequests · 35 Specimens · 55 Tasks · 150 Observations · 30 DRs
Hub Docker on-premise
Sección titulada «Hub Docker on-premise»# Cliente saca imagen y la corre en sitedocker pull ghcr.io/emahealth/emalab-hub:0.2.0
# Configurar token y endpointsdocker run -d \ --name emalab-hub \ --restart unless-stopped \ -e EMALAB_API_URL=https://aurora.lab.emahealth.io \ -e HUB_TOKEN=<token-from-platform-admin> \ -e ANALYZER_TCP_HOST=192.168.1.50 \ -e ANALYZER_TCP_PORT=9100 \ -e CONNECTION_MODE=hybrid \ -p 9100:9100 \ -v /var/lib/emalab-hub:/data \ ghcr.io/emahealth/emalab-hub:0.2.04 connection modes (ADR-017):
polling— Hub pide trabajo cada 30s (compatible firewalls estrictos).sse-keepalive— connection long-lived con keep-alive.hybrid— combina polling + sse según latencia.websocket— bidireccional (requiere WS en LAN cliente).
Errores HTTP comunes
Sección titulada «Errores HTTP comunes»| HTTP | Significado | Solución |
|---|---|---|
| 401 | JWT expiró o inválido | Re-login |
| 403 | RBAC: rol no autorizado para endpoint | Revisar RBAC_MATRIX.md |
| 404 | Recurso no existe en este tenant | Verificar tenant activo en localStorage |
| 409 | Duplicate (ej: barcode ya usado, accession identifier conflict) | Generar uno nuevo |
| 422 | Validation error (ej: stage 1 sin tech_section_auth) | Revisar mensaje específico |
| 429 | Rate limit (5/min portal, 30/min resolve-tenant) | Esperar y reintentar |
| 501 | R2 binding no habilitado (ej: bucket de reportes) | Crear bucket + descomentar binding |
| 503 | Aidbox caído o credenciales inválidas | Verificar tenant_aidbox_credentials |
Gotchas
Sección titulada «Gotchas»tenantIdes required enfhirFetch(env, path, opts, tenantId)— typecheck enforces. Si pasásundefined, el helper rechaza al inicio.- Nuevo endpoint mutante debe tener
requireRole(...)ANTES delapp.routeensrc/index.ts. Si solo agregásrequireRoledentro del handler, otros subpaths del módulo quedan expuestos. - R2 binding opcional con guard 501 — algunos handlers (reportes,
etiquetas, payloads connector) chequean
if (!env.DOCS_BUCKET) return 501. Permite deploy-first antes de que infra cree bucket. - Cron handlers leen
aidbox_tenant_iddesdeemashared.tenants— no desde el JWT (que no existe en cron). Pass viaoptionsalprocessRetryQueue. buildEmaFhirClientopt-in por handler (L102) — NO mover a middleware global. Adopción staged.- Hub tokens activos —
hub_registry.jwt_tokenactivo. Rotación viaPOST /api/hub/renew-token.
- No hay GitHub Actions para deploy (preferencia EMA: deploys
manuales con
npm run deployo CF git integration). - Tests corren localmente antes de merge. Si falla algún test:security o test:integration, no se mergea.
- Branches: trabajo directo en
main(solo-dev) post-S5. Antes erasprint/<id>-<scope>con squash-merge via PR.