Arquitectura
La arquitectura de EMA Lab combina tres decisiones que se condicionan mutuamente: ejecutar todo el backend en el edge de Cloudflare, modelar los datos clínicos como recursos FHIR canónicos en Aidbox con aislamiento ORGBAC, y delegar el resto del estado operacional a Supabase con RLS estricta. Sobre eso opera un Hub Docker on-premise opcional para conectar analizadores físicos al cloud sin requerir internet directo en el sitio del laboratorio. Esta página recorre los componentes principales, la cadena de middleware, los conectores inbound y outbound, y los patrones cross-cutting que aparecen en todos los handlers.
Componentes principales
Sección titulada «Componentes principales»Layered structure (frontend)
Sección titulada «Layered structure (frontend)»src/client/├── design-system/ # Tokens CSS + componentes DS (IconBox, Avatar, etc.)│ ├── primitives.css # hex colors (NO referenciar desde UI)│ ├── themes/*.css # 4 themes con ~50 semantic tokens│ └── components/ # IconBox, Avatar, VitalPill, AlertCard, StatusDot, etc.├── components/│ ├── layout/ # AppLayout, Sidebar, Header│ └── ui/ # 10 wrappers Radix (L027) — Dialog, Popover, Select, Tabs, etc.├── pages/ # 35+ pages│ ├── admin/ # Admin dashboard, audit, FHIR emission status│ ├── orders/ # Lista + detalle + nueva orden│ ├── checkin/ # Recepción de muestras + lote│ ├── catalog/ # Test catalog, profiles, specimens│ ├── worklist/ # Cola técnica por sección│ ├── results/ # ManualEntry inline + EMI feedback│ ├── validation/ # Stage 1-4 gate + cascade│ ├── reports/ # Lista + detalle DR + verify QR│ ├── micro/ # MicroCultures + MicroCultureDetail + MicroConfig│ ├── inventory/ # InventoryStock + Consumption + Alerts│ ├── qc/ # QC lots, Westgard charts, Bull's│ ├── billing/ # Price lists, invoices, DTE│ ├── connectors/ # HL7v2 / FHIR Subs admin│ ├── nonconformities/ # CLSI GP41 16 reasons│ ├── portal/ # Paciente verify + results│ └── settings/ # Profile, organization, users, sections├── lib/│ ├── auth.ts # Supabase auth wrapper│ ├── api.ts # apiFetch helper│ ├── client-context.tsx # Active client filter│ ├── barcode.ts # Code 128B SVG (pure TS)│ └── qr.ts # Minimal QR Code SVG (demo-quality)└── styles/ └── globals.css # Tailwind v4 + DS tokensLayered structure (backend)
Sección titulada «Layered structure (backend)»src/├── api/ # 38 módulos Hono│ ├── auth.ts # /auth/me · /auth/resolve-tenant · /auth/callback│ ├── orders.ts # /api/orders/* (lab_admin, lab_reception, lab_tech)│ ├── samples.ts # /api/samples/* (+ lab_phlebotomist)│ ├── checkin.ts # /api/checkin/* (idem)│ ├── results.ts # /api/results/* (+ lab_director)│ ├── validation.ts # /api/validation/* (4 stages)│ ├── worklists.ts # /api/worklists/*│ ├── reports.ts # /api/reports/* (incluye lab_viewer)│ ├── micro.ts # /api/micro/* (cultures + antibiograma)│ ├── molecular.ts # /api/molecular/* (parser registry + sign)│ ├── stewardship.ts # /api/micro/stewardship/* (CLSI M39)│ ├── inventory.ts # /api/inventory/* (stock + auto-consume)│ ├── qc.ts # /api/qc/* (Westgard + Bull's)│ ├── billing.ts # /api/billing/* (price lists, DTE)│ ├── connectors.ts # /api/connectors/* (HL7v2 + FHIR Subs)│ ├── hub.ts # /api/hub/* (token-auth, on-prem)│ ├── platform.ts # /api/platform/* (X-Platform-Key)│ ├── portal.ts # /api/portal/* (token paciente)│ ├── emi.ts # /api/emi/{chat,access} proxy│ └── ... (catalog, sections, clients, users, config, tat, dashboard,│ nonconformities, sla, equipment, maintenance, routing,│ transport, exclusions, lab-sites, barcode-config,│ settings-{profile,users,organization,data})├── lib/│ ├── fhir-server.ts # fhirFetch helper con per-tenant ORGBAC│ ├── tenant-fhir.ts # buildEmaFhirClient (opt-in, L102)│ ├── supabase-server.ts # Server client per-request│ ├── micro.ts # Pure helpers — stage transitions, FHIR builders, timeouts│ ├── inventory.ts # FIFO selection, alerts, expiry│ ├── reports/finalize.ts# applyFinalizationPatch idempotente│ ├── stewardship/ # Engine M39 + writer + DocumentReference│ ├── molecular/ # Parser registry + critical targets│ ├── hl7v2/ # Inline parser + outbound builder│ ├── connectors/ # FHIR forward + retry queue│ └── engines/ # qc, emi-evaluator, autoverify, validation-cascade├── middleware/│ ├── auth.ts # Valida JWT Supabase│ ├── tenant.ts # Resuelve tenant_id desde user_roles (L002)│ ├── client-isolation.ts# Filtra clientes según user_client_access│ └── rate-limit.ts # KV-based rate limiting (L086)└── index.ts # Hono app entry · mount order · cron handlersORGBAC y multi-tenancy
Sección titulada «ORGBAC y multi-tenancy»Desde el sprint S9 (commit 82a07d4) todas las queries FHIR pasan
por la URL /Organization/{tenantId}/fhir/*, lo que permite a
Aidbox rechazar las queries cross-tenant server-side
independientemente del filtro client-side. La consecuencia
arquitectónica es relevante: un bug de developer que olvide aplicar
un filtro no compromete el aislamiento, porque la URL ya está scoped
a la Organization correcta antes de que la query llegue a Aidbox.
El aislamiento entre laboratorios opera en tres capas
independientes que se refuerzan mutuamente. La primera es el ORGBAC
server-side de Aidbox: la URL define el tenant y no se negocia. La
segunda es la RLS de Supabase, que aplica
tenant_id = auth.tenant_id sobre todas las tablas operacionales.
La tercera es el clientIsolationMiddleware, que filtra por
user_client_access para usuarios con visibilidad acotada a
ciertos clientes externos —típicamente un TM que solo procesa
muestras de un subconjunto de las clínicas que envían órdenes al
laboratorio.
Pipeline de validación en cuatro etapas
Sección titulada «Pipeline de validación en cuatro etapas»El pipeline de validación opera en cuatro etapas consecutivas que todos los resultados —incluso los que terminan en autoverify— deben atravesar. La primera etapa verifica la autorización del TM para operar sobre la sección donde el resultado entra; la segunda corre el evaluador EMI con las reglas configuradas; la tercera intenta liberar el resultado automáticamente si cumple las condiciones de autoverify del test; la cuarta requiere firma del director cuando ninguna etapa anterior liberó el resultado.
Conectores HL7v2 y FHIR
Sección titulada «Conectores HL7v2 y FHIR»El producto soporta cuatro tipos de comunicación con sistemas externos. Inbound HL7v2 recibe mensajes ORM con autenticación basada en API key con SHA-256 hash at rest; inbound FHIR recibe órdenes desde Aidbox externos vía Subscription estándar. Outbound HL7v2 emite ORU R01 al EHR del cliente cuando el reporte queda firmado, y outbound FHIR forward bundles cuando corresponda. Un cron de retry cada cinco minutos procesa la cola de pendientes para ambos protocolos outbound, lo que mantiene el sistema robusto frente a caídas momentáneas del lado del cliente.
Hub on-premise
Sección titulada «Hub on-premise»Algunos laboratorios operan analizadores físicos en sus sites que no tienen acceso directo a Internet, ya sea por política corporativa o por limitaciones de los protocolos legacy ASTM y HL7v2 que muchos equipos hablan nativamente. Para esos casos el producto provee un Hub Docker que se instala en la red local del site y actúa de intermediario, con cuatro modos de conexión (decisión ADR-017) —polling, SSE-keepalive, hybrid y websocket— que permiten operar bajo prácticamente cualquier configuración de firewall.
Cron jobs programados
Sección titulada «Cron jobs programados»Tres crons corren periódicamente para mantener el sistema operativo sin intervención manual:
| Cron | Schedule | Función |
|---|---|---|
retry-pending | */5 * * * * | Procesa connector_retry_queue (FHIR y HL7v2 outbound) |
stewardship-quarterly | 0 3 1 1,4,7,10 * | Engine CLSI M39 que produce reportes firmables (L049, L074) |
fhir-emission-retry | */15 * * * * | Re-emite los DetectedIssue de stewardship pendientes (L079) |
Patrones cross-cutting
Sección titulada «Patrones cross-cutting»Cinco principios atraviesan toda la arquitectura. El primero son los
builders puros en src/lib/**/*.ts: funciones sin acceso a base
de datos o FHIR que pueden testearse en isolación. El segundo es el
patrón de dual-write FHIR: Supabase como fuente primaria
operacional y Aidbox como fuente canónica, con tres columnas que
trackean el estado de emisión (fhir_emission_status,
fhir_emission_error, fhir_emission_at) más un retry sweep que
re-emite lo pendiente (L071).
El tercer principio es el provider interface con Mock, Http y
resolver: cada servicio externo opcional (FEA, IMED, Loops,
emadte) tiene una implementación mock para tests más una HTTP para
producción, y un resolver que decide cuál usar según la
configuración del tenant. El never-throw es regla: ningún provider
opcional debe poder romper el flujo principal. El cuarto es el
sprint entry audit, un bloque cero obligatorio en cada sprint
que valida los P0 del audit previo contra el código actual antes de
empezar trabajo nuevo. Y el quinto es el R2 binding unificado
con DOCS_BUCKET cross-repo (clinic más lab), que evita la
proliferación de bindings ad-hoc y simplifica la rotación de
credenciales.