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.
| Item | Valor |
|---|---|
| Versión actual | 0.2.0 (Wave 3 v2.0 + S9 ORGBAC) |
| Sprints cerrados | Fases 1a/1b/2/3/4 más S1 a S9 |
| Decisiones formales | L001 a L108 (ver docs/decisions.md) |
| ADRs | 22 archivos en docs/adr/ |
| Tests | 809 passing, 0 skipped |
| RLS coverage | 100% bajo mutation testing (L087) |
| OWASP CRITICAL/HIGH | Cerrados (L088 y L089) |
| Estado | Listo para piloto controlado |
| Repo | emahealth/emalaboratory |
| Workspace | /home/cristianruiz/Dev/emahealth/emalab |
Stack tecnológico
Sección titulada «Stack tecnológico»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.
| Capa | Tecnología | Notas |
|---|---|---|
| Frontend | React 19 con React Router 7 | SPA con tabs persistentes en URL |
| Build | Vite 6 | TypeScript estricto |
| UI | Tailwind 4 con wrappers Radix manuales | sin shadcn CLI por L027 |
| Design system | DS v1.0 con tres capas de tokens | cuatro themes (light, dimmed, dark, well), accent EMA blue #1E40AF |
| Backend | Hono | Cloudflare Workers, ESM |
| Auth y BD | Supabase emahub | schemas emalab y emashared |
| FHIR | Aidbox | per-tenant ORGBAC desde S9 |
| FHIR client | emafhir 0.6.0 | opt-in createEmaClient con meta-injection |
| QC | Westgard, Bull’s y moving averages | engine puro src/lib/engines/qc.ts |
| Validation | Cuatro etapas (técnica, EMI, clínica, firma) | ADR-012 |
| Conectores | HL7v2 inline parser y FHIR Subscriptions | ADR-009, ADR-010 |
| Hub on-prem | Docker con cuatro modos de conexión | ADR-016, ADR-017 |
| Tests | Vitest 6 con suites separadas | unit, integration, regression, perf, security |
| Storage | R2 binding DOCS_BUCKET | reportes firmados, etiquetas, payloads conectores |
| Facturación | worker emadte con X-API-Key per-tenant | ADR-015 |
| EMI | proxy /api/emi/{chat,access} | server-side, evita filtración de key |
| Linter y TS | tsconfig estricto sobre 38 módulos API | sin any injustificado |
| Package manager | npm | regla global EMA |
Dominio clínico
Sección titulada «Dominio clínico»El producto cubre doce áreas funcionales que en conjunto definen el ciclo IVLIS completo. La tabla siguiente sintetiza qué provee cada una:
| Área | Funcionalidad |
|---|---|
| Catálogo | 50 tests LOINC seed, 5 perfiles, 6 specimen types, 11 containers |
| Órdenes | ServiceRequest per test (ADR-003), check-in lote, barcode Code 128B con Luhn |
| Worklists | Cola técnica por sección, ruteo automático |
| Validación | Pipeline de cuatro etapas con auth por sección, EMI evaluator y gates |
| QC | QC lots, targets, Westgard 7-T (L024), moving averages, Bull’s |
| Microbiología | Culture tracking de siete etapas, antibiogramas SIR, MALDI-TOF Bruker, hemocultivo positivo crítico (L051) |
| Molecular | Plataforma genérica, parser registry, pending review, critical targets |
| Stewardship | Antibiograma CLSI M39, dual-write FHIR DetectedIssue, multi-firma |
| Reportes | DiagnosticReport firmado con QR y endpoint público de verificación sin PII |
| Facturación | Price lists por cliente y emisión vía emadte |
| Inventario | Reagent catalog, lot tracking FIFO, auto-consumption opcional |
| Conectores | HL7v2 inbound con hash at rest SHA-256 (L089) y outbound con retry |
| Portal paciente | Identidad nacional más email para token de 1h, lectura de DR finales |
Roles RBAC
Sección titulada «Roles RBAC»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:
| Rol | Descripción |
|---|---|
lab_admin | Administración total del tenant |
lab_director | Director técnico — firma clínica y define políticas (TAT, SLA, ruteo, exclusiones) |
lab_tech | TM o BQF — ingreso de resultados, validación técnica, QC, microbiología y molecular |
lab_reception | Admisión, órdenes, check-in, facturación |
lab_phlebotomist | Toma de muestras, check-in, etiquetado |
lab_viewer | Solo 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.
Decisiones críticas
Sección titulada «Decisiones críticas»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ón | Resumen |
|---|---|
| L002 | JWT multi-tenant: tenant_id en JWT, tenantMiddleware resuelve user_roles |
| L024 | Westgard 7-T strict monotonic (no tolera repetidos exactos) |
| L025 | DR finalization, endpoint público de verificación sin PII, QR real con la librería qrcode |
| L026 | Facturación per-tenant: billing_config con siete columnas y X-API-Key symmetric |
| L027 | Sin shadcn CLI; wrappers Radix manuales para preservar los tokens del DS |
| L049 | Stewardship CLSI M39: engine puro, XLSX writer, R2 y DocumentReference |
| L051 | Hemocultivo positivo dispara Task FHIR critical-value-notification |
| L052 | Catálogo global de microorganismos SNOMED con tenant_id nullable |
| L071 | Dual-write stewardship: Supabase primary y Aidbox DetectedIssue canonical |
| L079 | Retry sweep para FHIR emission status (tres columnas _status, _error, _at) |
| L087 | RBAC matrix completa con mutation testing (100% de cobertura) |
| L089 | Inbound API key con SHA-256 hash at rest y constant-time compare |
| L102 | buildEmaFhirClient opt-in por handler; middleware no global |
| L107 y L108 | EMI proxy per-app, tenant_emi_access como defensa en profundidad, drawer escondido hasta Wave 5 |
Los ADRs formales viven en emalab/docs/adr/.
Por qué Lab es su propio producto
Sección titulada «Por qué Lab es su propio producto»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.
Antipatrones registrados
Sección titulada «Antipatrones registrados»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).