An official developer portal of the Government of the Republic of Zambia · Test environmentVerify a certificate
Pamodzi Developer HubBuild, integrate and learn the e-Services framework

Switching modules on and off, and deploying them

A module is useful only when an administrator can switch it on safely for the right ministries, and switch it off again without losing anything. In this lesson you learn the switch levels, the maker-checker process, what "off" means in every app, and the deployment settings that bound what administrators can do.

Three levels, all must say yes

A module is on for a caller only when every level agrees:

  1. Ceiling (deployment). Environment variables, set through Helm values:
    • MODULES_AVAILABLE: a comma list of module ids that may ever be switched on. Unset or empty means every bundled

      module, except sample modules (such as the SDK example) when NODE_ENV=production; none means none.

    • MODULES_BY_MDA: which ministry may have which module, for example

      "*:verifications;AAA,BBB:module-one,module-two". Unset means no per-ministry limit.

    • MODULES_FORCE_OFF: the operations kill switch, a comma list.
  2. Installation. An administrator switches the module on under System administration → Modules.
  3. Ministry assignment. Scope all (every ministry the module is available to), or assigned to chosen MDAs.

A module whose dependency is off is off too. A dependency cannot be switched off while a module that needs it is on.

The ceiling protects against mistakes: an administrator cannot switch a module on for a ministry that MODULES_BY_MDA does not list for it (409 MODULE_NOT_FOR_MDA), checked again when the second administrator approves. On the national hub, a ministry that runs its own node has no module on the hub at all (409 MODULE_MDA_ON_NODE): its modules run on its node. A MODULES_BY_MDA value that cannot be read fails closed and refuses the start.

Maker-checker

Modules are a sensitive setting, so switching them uses maker-checker:

POST /api/admin/modules/:id/requests  { action, reason, scope?, mdaIds?, confirmModuleId? }
PUT  /api/admin/modules/:id/mdas      (assignment)
PUT  /api/admin/modules/:id/settings  (settings)
  • A reason of at least 8 characters is required.
  • A second administrator approves the first switch-on and every switch-off. The proposer never approves their own

    request.

  • A switch-off shows an impact preview (including the module's own figures from its manifest's impact

    function) and requires the module id to be typed in.

  • Every change is audited (module.*), recorded in the change history and sent as the webhook

    platform.module-changed.

On a developer machine you may set ACCESS_MAKER_CHECKER=off to approve alone. Never do that on a shared environment.

Switching needs no restart. The module state is cached for 30 seconds and invalidated across API replicas with a PostgreSQL NOTIFY.

Off means absent

While a module is off for the caller:

WhereWhat happens
API routes404 { code: 'MODULE_DISABLED', module }, every method, every caller, anonymous included
PolicyThe module's actions are refused at step 0
JobsRuns are recorded as skipped; run() is never called
ServicesA service with a capability the module owns is hidden from the catalogue; a new application gets 409 SERVICE_MODULE_DISABLED
Back officeMenu entries disappear; a bookmarked page shows "This function is not enabled" (administrators get a link to the Modules page)
PortalIts generic blocks disappear; its services show as not available
POSIts capabilities are no longer sent to devices

Off never loses money or records

Two promises make switching safe:

  • Data stays. Tables and rows are kept. Switching the module on again restores everything. Because migrations

    always run, the schema is the same whether the module is on or off.

  • Money settles. A payment that was already started still finalises. The bypass points (payment.onFinalised,

    payment.finaliseExternalInvoice, billing.onAllocated, pos.offlineSale) reach the switched-off module, and each such run is audited as module.bypass.

Billing follows the same idea: a switched-off module's billing accounts are skipped by billing runs and refuse new orders, but money that arrives is still allocated.

The back office, the portal and POS

  • Back office. ModuleProvider loads GET /api/modules/me. A module renders only when the server lists it, and

    a menu entry shows only when the server's menu lists its key: the server's policy decides. Entries sit under Ministry functions · <MDA short name> by default, one group per module.

  • Portal. The public portal is ministry-neutral. Allowed by default are only generic, data-driven contributions:

    capabilities, statusWords, feeNotes, recordSources / recordDetails, legacyServiceIds and i18nNamespaces. Routes, My Workplace sections, home-page scenarios, service handlers, action types and payment kinds need the product owner's decision and an entry in portalNeutrality.allow.js; portalNeutrality.test.js fails otherwise. Do not add to the allow-list yourself. Portal texts go in public/locales/<lng>/<id>.json, English first, with ny, bem and toi drafts.

  • POS. There is one signed APK for every MDA. A module declares pos: { capabilities: [...] }; the sign-in and

    heartbeat answers carry the capabilities of the modules on for the device's MDA, and the APK hides flows that are not listed. A new native screen needs a new APK release.

Deployment

In a per-MDA deployment each ministry back office is a node. The estate manifest lists each MDA's modules, and the generated values set the ceiling per instance:

SettingHubNode
MODULES_AVAILABLEthe hub's modules plus those of hosted and external MDAsthe MDA's modules (none when it has none)
MODULES_BY_MDA*:<hub modules> plus <codes>:<modules> per hosted MDA<codes>:<modules> of the MDA
MODULES_BOOTSTRAPmodules switched on at first startthe MDA's modules

To give an MDA a new module: add it to the MDA's modules in the estate manifest, re-render and deploy. The module must be bundled in the image. Later changes are made in the back office, under maker-checker.

The deploy smoke test checks both sides of the gate: a route of an enabled module does not answer MODULE_DISABLED, and a route of a module outside the ceiling does.

Upgrading imported services

When a module needs an imported legacy service configured in a particular way (a capability, a re-sequenced process, a fee setting), ship that as modules/<id>/scripts/upgradeServices.js using the shared runner lib/modules/serviceUpgrade.js. It is a dry run by default (--apply writes), idempotent, refused while the module is off for the service's ministry, never overwrites a part an officer changed unless --force, and --revert restores exactly what was there. Never ship such configuration as an e2e seed.

Check your understanding

  • Name the three switch levels and the environment variables that form the ceiling.
  • Which switch changes need a second administrator, and what extra step does a switch-off require?
  • What does a citizen see when applying to a service whose module was just switched off?
  • Why can a payment still be finalised for a switched-off module, and how is that recorded?
  • Which portal contributions are allowed by default, and who decides the others?
  • How does an operator give an MDA's node a new module?

Sign in to record your progress.

Previous: Plugging into core through extension points Next: Testing and releasing a module