Architecture
HLMIS is a fork of a Rwanda-specific system (VLMIS), refactored so that external integrations — DHIS2, TRVST, and the choice of auth provider — become optional plug-ins instead of hard dependencies. Any organisation can deploy HLMIS with or without those systems.
The core has no knowledge of external systems
This is the single rule that drives most of the codebase's structure:
src/
├── core/ # Business logic — MUST NOT import from adapters or external systems
├── adapters/ # External integrations (dhis2, trvst, local-auth, postgres) behind interfaces
└── app/ # Wiring — turns adapters on/off based on configuration
If a file under src/core/ ever imports a DHIS2 client directly, that's a
sign it's in the wrong layer. Integrations sit behind interfaces, and the
app/ layer decides at runtime which implementation to wire in.
Two rules that explain almost every "why" in this codebase
1. Configuration table, not environment variables, for anything
integration-related. DHIS2 URLs, TRVST config, and (in principle) the
choice of auth provider live in a database configuration table, editable
at runtime via an admin API (RuntimeConfigService). Only genuine
infrastructure settings — DB host/port, NODE_ENV, JWT_SECRET — belong
in .env. When you're deciding where a new setting goes, this is the
question to ask.
One real exception worth knowing about: which auth provider is actually
active (LocalAuthProvider vs Dhis2AuthProvider) is decided once, at
application boot, by the ADAPTER_MODE environment variable — not by a
database config key, even though one used to exist and looked like it
should matter. See DHIS2 vs. Generic Mode for the
full story.
2. Database-driven permissions, not hard-coded roles. Permissions are
explicit capability strings (e.g. MANAGE_ORDERS, APPROVE_ORDER); a
Role holds an array of them. Every protected endpoint declares what it
needs via a @RequirePermissions(...) decorator, checked by a
PermissionsGuard. There is no RoleGuard(UserType.X)-style hard-coded
role check anywhere in the current system. See
Permissions for the full model.
Deployment mode: DHIS2 vs. generic
A single environment variable, ADAPTER_MODE, decides whether a deployment
runs in dhis2 mode (real DHIS2 integration wired in) or local/generic
mode (no DHIS2 dependency at all — the default). isDhis2Mode(), in
src/app/config/adapter-mode.ts, is the only place in the codebase that
reads this variable directly; everything else calls that function. See
DHIS2 vs. Generic Mode for more detail.