Skip to main content

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.