Ir al contenido

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.

EntornoWorkerURLAidbox
Dev localwrangler devhttp://localhost:8787mock o Aidbox staging
Staginglab-staging.workers.dev(interno)Aidbox staging
Producciónemalab.emahealth.io{slug}.lab.emahealth.ioAidbox prod
Ventana de terminal
cd /home/cristianruiz/Dev/emahealth/emalab
npm install
npm run dev # Vite dev (frontend) :5173
npm run dev:worker # Wrangler dev (backend Hono) :8787
npm run build # Vite build → dist/
npm run deploy # build + wrangler deploy
npm run typecheck # tsc --noEmit
npm run guards # Wave 3 regression guards (scripts/guards.sh)
npm run smoke:emi # Post-deploy EMI smoke
npm run test # Vitest run (todas las suites)
npm run test:unit # Suite unit
npm run test:integration # Suite integration (timeout 15s)
npm run test:regression # Suite regression
npm run test:perf # Suite performance (timeout 30s)
npm run test:security # Suite security (timeout 20s)
npm run test:coverage # con coverage

Cada tenant tiene su subdominio bajo lab.emahealth.io:

TenantURL
Lab Auroraaurora.lab.emahealth.io
Lab Andesandes.lab.emahealth.io
Lab Sursur.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).

Agrupados por propósito. Configurar en CF dashboard o wrangler secret put NAME por entorno.

SecretPropósito
SUPABASE_URLURL del proyecto emahub
SUPABASE_SERVICE_ROLE_KEYService role (uso interno middleware)
SUPABASE_ANON_KEYAnon key servida via /api/config
JWT_SECRETValidación JWT Supabase
SecretPropósito
AIDBOX_BASE_URLURL del Aidbox prod
AIDBOX_PER_TENANT_KEYClave AES-GCM para decrypt tenant_aidbox_credentials (S9)
SecretPropósito
LOOPS_API_KEYLoops.so transactional emails
LOOPS_DLQ_TRANSACTIONAL_IDTemplate DLQ alert
LOOPS_DLQ_RECIPIENTEmail destinatario DLQ
EMI_BASE_URLDefault https://emi.emahealth.io (override en dev)
EMALAB_API_KEYServer-side proxy a EMI (NUNCA en frontend)
EMI_WORKER_URLEndpoint legacy evaluateEMI
PLATFORM_API_KEYValidación X-Platform-Key (provisioning desde Vault)
HUB_JWT_SECRETFirma de tokens Hub on-prem

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

# 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)
]
Ventana de terminal
# Aplicar migraciones a hub
cd supabase/
psql postgresql://postgres@db.bzikofdvfvchntvfzyni.supabase.co/postgres \
-f migrations/022_routing_engine.sql
# Verificar estado
SELECT 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:

  1. Numerar secuencial (023_*.sql).
  2. Documentarse en docs/decisions.md con L###.
  3. Tener test de regression que verifique RLS + indexes.
  4. NO romper RLS — verificar con el patrón existente.
Ventana de terminal
# Cargar seed FHIR en Aidbox (organizations, locations, practitioners,
# patients, activity definitions, demo data)
bash scripts/seed-aidbox.sh

Carga:

  • 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
Ventana de terminal
# Cliente saca imagen y la corre en site
docker pull ghcr.io/emahealth/emalab-hub:0.2.0
# Configurar token y endpoints
docker 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.0

4 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).
HTTPSignificadoSolución
401JWT expiró o inválidoRe-login
403RBAC: rol no autorizado para endpointRevisar RBAC_MATRIX.md
404Recurso no existe en este tenantVerificar tenant activo en localStorage
409Duplicate (ej: barcode ya usado, accession identifier conflict)Generar uno nuevo
422Validation error (ej: stage 1 sin tech_section_auth)Revisar mensaje específico
429Rate limit (5/min portal, 30/min resolve-tenant)Esperar y reintentar
501R2 binding no habilitado (ej: bucket de reportes)Crear bucket + descomentar binding
503Aidbox caído o credenciales inválidasVerificar tenant_aidbox_credentials
  • tenantId es required en fhirFetch(env, path, opts, tenantId) — typecheck enforces. Si pasás undefined, el helper rechaza al inicio.
  • Nuevo endpoint mutante debe tener requireRole(...) ANTES del app.route en src/index.ts. Si solo agregás requireRole dentro 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_id desde emashared.tenants — no desde el JWT (que no existe en cron). Pass via options al processRetryQueue.
  • buildEmaFhirClient opt-in por handler (L102) — NO mover a middleware global. Adopción staged.
  • Hub tokens activoshub_registry.jwt_token activo. Rotación via POST /api/hub/renew-token.
  • No hay GitHub Actions para deploy (preferencia EMA: deploys manuales con npm run deploy o 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 era sprint/<id>-<scope> con squash-merge via PR.
AidboxEMA Lab workerEMA Vault workerVault staffAidboxEMA Lab workerEMA Vault workerVault staffTenant operativo — admin recibe email y completa onboardingPOST /tenants {slug, country, products: ['lab']}1POST /api/platform/tenantsX-Platform-Key2INSERT emashared.tenants3INSERT emashared.tenant_products (lab)4Crear OAuth client en Aidbox+ encrypt AES-GCM5INSERT tenant_aidbox_credentials6POST /Organization (root tenant org)7200 {tenant_id, slug}8POST /admins {tenant_id, email}9POST /api/platform/tenants/:id/admins10INSERT user + user_roles (lab_admin)11Loops invite email12Configurar custom domain {slug}.lab.emahealth.io13