Ir al contenido

Overview y stack

EMA Lab es el sistema de información clínica para laboratorios (LIS) del ecosistema EMA Health. Cubre el ciclo IVLIS completo —desde la admisión y la toma de muestra hasta la entrega del reporte firmado— y opera como plataforma multi-tenant y multi-país por diseño, con primera implementación regulatoria activa en Chile. La arquitectura combina FHIR R4 nativo en Aidbox para los datos clínicos canónicos, Supabase para la operación, y un Hub on-premise opcional para la conectividad con analizadores físicos.

La versión actual es 0.2.0 con Wave 3 v2.0 y el sprint S9 cerrados. La operación está cubierta por 809 tests pasando sin omisiones, RLS con cobertura del 100% bajo mutation testing (decisión L087), y los hallazgos OWASP críticos y altos resueltos (L088 y L089). El producto está listo para piloto controlado con un cliente real.

ItemValor
Versión actual0.2.0 (Wave 3 v2.0 + S9 ORGBAC)
Sprints cerradosFases 1a/1b/2/3/4 más S1 a S9
Decisiones formalesL001 a L108 (ver docs/decisions.md)
ADRs22 archivos en docs/adr/
Tests809 passing, 0 skipped
RLS coverage100% bajo mutation testing (L087)
OWASP CRITICAL/HIGHCerrados (L088 y L089)
EstadoListo para piloto controlado
Repoemahealth/emalaboratory
Workspace/home/cristianruiz/Dev/emahealth/emalab

El stack se decidió privilegiando dos cosas: el ecosistema TypeScript end-to-end y la operación sobre el edge de Cloudflare. La segunda decisión condicionó muchas otras: descartó runtimes pesados, forzó el uso de bibliotecas compatibles con Workers, y empujó a delegar la base de datos clínica a Aidbox y la operacional a Supabase en lugar de auto-hostearlas. Una particularidad del producto es la decisión deliberada de no usar el CLI de shadcn y construir wrappers Radix manuales (L027), porque el CLI sobrescribe las variables CSS del design system v1.0 y rompe los cuatro themes soportados.

CapaTecnologíaNotas
FrontendReact 19 con React Router 7SPA con tabs persistentes en URL
BuildVite 6TypeScript estricto
UITailwind 4 con wrappers Radix manualessin shadcn CLI por L027
Design systemDS v1.0 con tres capas de tokenscuatro themes (light, dimmed, dark, well), accent EMA blue #1E40AF
BackendHonoCloudflare Workers, ESM
Auth y BDSupabase emahubschemas emalab y emashared
FHIRAidboxper-tenant ORGBAC desde S9
FHIR clientemafhir 0.6.0opt-in createEmaClient con meta-injection
QCWestgard, Bull’s y moving averagesengine puro src/lib/engines/qc.ts
ValidationCuatro etapas (técnica, EMI, clínica, firma)ADR-012
ConectoresHL7v2 inline parser y FHIR SubscriptionsADR-009, ADR-010
Hub on-premDocker con cuatro modos de conexiónADR-016, ADR-017
TestsVitest 6 con suites separadasunit, integration, regression, perf, security
StorageR2 binding DOCS_BUCKETreportes firmados, etiquetas, payloads conectores
Facturaciónworker emadte con X-API-Key per-tenantADR-015
EMIproxy /api/emi/{chat,access}server-side, evita filtración de key
Linter y TStsconfig estricto sobre 38 módulos APIsin any injustificado
Package managernpmregla global EMA

El producto cubre doce áreas funcionales que en conjunto definen el ciclo IVLIS completo. La tabla siguiente sintetiza qué provee cada una:

ÁreaFuncionalidad
Catálogo50 tests LOINC seed, 5 perfiles, 6 specimen types, 11 containers
ÓrdenesServiceRequest per test (ADR-003), check-in lote, barcode Code 128B con Luhn
WorklistsCola técnica por sección, ruteo automático
ValidaciónPipeline de cuatro etapas con auth por sección, EMI evaluator y gates
QCQC lots, targets, Westgard 7-T (L024), moving averages, Bull’s
MicrobiologíaCulture tracking de siete etapas, antibiogramas SIR, MALDI-TOF Bruker, hemocultivo positivo crítico (L051)
MolecularPlataforma genérica, parser registry, pending review, critical targets
StewardshipAntibiograma CLSI M39, dual-write FHIR DetectedIssue, multi-firma
ReportesDiagnosticReport firmado con QR y endpoint público de verificación sin PII
FacturaciónPrice lists por cliente y emisión vía emadte
InventarioReagent catalog, lot tracking FIFO, auto-consumption opcional
ConectoresHL7v2 inbound con hash at rest SHA-256 (L089) y outbound con retry
Portal pacienteIdentidad nacional más email para token de 1h, lectura de DR finales

El producto reconoce seis roles internos del laboratorio más un guard especial de plataforma. Cada rol agrupa permisos coherentes con una responsabilidad real del cargo:

RolDescripción
lab_adminAdministración total del tenant
lab_directorDirector técnico — firma clínica y define políticas (TAT, SLA, ruteo, exclusiones)
lab_techTM o BQF — ingreso de resultados, validación técnica, QC, microbiología y molecular
lab_receptionAdmisión, órdenes, check-in, facturación
lab_phlebotomistToma de muestras, check-in, etiquetado
lab_viewerSolo lectura sobre informes

El guard platform_admin no es un rol JWT tradicional sino un mecanismo de acceso vía header X-Platform-Key reservado para operaciones de provisioning desde EMA Vault. No puede invocarse desde el frontend ni desde un usuario humano.

Las decisiones registradas suman 108 a la fecha. Las más estructurales se concentran en multi-tenancy (L002, L102), calidad asegurada (L024, L049, L051, L052), seguridad (L087, L089), y EMI (L107, L108). La tabla siguiente resume las más relevantes para entender el resto de la documentación técnica:

DecisiónResumen
L002JWT multi-tenant: tenant_id en JWT, tenantMiddleware resuelve user_roles
L024Westgard 7-T strict monotonic (no tolera repetidos exactos)
L025DR finalization, endpoint público de verificación sin PII, QR real con la librería qrcode
L026Facturación per-tenant: billing_config con siete columnas y X-API-Key symmetric
L027Sin shadcn CLI; wrappers Radix manuales para preservar los tokens del DS
L049Stewardship CLSI M39: engine puro, XLSX writer, R2 y DocumentReference
L051Hemocultivo positivo dispara Task FHIR critical-value-notification
L052Catálogo global de microorganismos SNOMED con tenant_id nullable
L071Dual-write stewardship: Supabase primary y Aidbox DetectedIssue canonical
L079Retry sweep para FHIR emission status (tres columnas _status, _error, _at)
L087RBAC matrix completa con mutation testing (100% de cobertura)
L089Inbound API key con SHA-256 hash at rest y constant-time compare
L102buildEmaFhirClient opt-in por handler; middleware no global
L107 y L108EMI proxy per-app, tenant_emi_access como defensa en profundidad, drawer escondido hasta Wave 5

Los ADRs formales viven en emalab/docs/adr/.

EMA Lab nació de la experiencia del proyecto IVLIS de BUPA Chile sobre Dedalus SUNRISE y aplica deliberadamente las lecciones acumuladas allí. La separación del producto del resto del ecosistema clínico responde a cuatro razones concretas. La primera es que el ciclo IVLIS es complejo: admisión, toma, transporte, recepción, pre-analítica, análisis, validación, firma, comunicación crítica y stewardship, cada uno con sus reglas regulatorias propias y sus estados específicos. La segunda es que la calidad asegurada requiere infraestructura dedicada: QC interno con Westgard y Bull’s, antibiograma institucional con CLSI M39, no-conformidades con CLSI GP41 y dieciséis razones tipificadas.

La tercera es la integración asimétrica: los clientes externos hablan FHIR, los analizadores on-premise hablan ASTM o HL7v2, y el EHR del cliente puede hablar cualquiera de los dos —cada uno con sus propios cuellos de botella y patrones de fallo. Y la cuarta es el multi-site dentro del tenant: un solo laboratorio puede operar tres sedes con ruteo automático entre ellas, distinto del modelo multi-tenant que aplica entre laboratorios diferentes.

Cuatro antipatrones recurrentes vale la pena registrar explícitamente para que las decisiones acumuladas no se reviertan por accidente. No instalar el CLI de shadcn: sobrescribe los CSS variables del design system y rompe los cuatro themes; siempre construir wrappers Radix manuales (L027). No mover buildEmaFhirClient a middleware global: la adopción staged por handler permite mantener los handlers que todavía usan fhirFetch directo sin obligarlos a refactor inmediato (L102). No exponer la API key del worker en el frontend: siempre invocar EMI vía proxy server-side /api/emi/{chat,access} (L107). Y no crear endpoints públicos con PII: el endpoint de verificación /verify/:code retorna resumen del DR sin exponer datos personales del paciente (L025).