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:
- 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 bundledmodule, except
samplemodules (such as the SDK example) whenNODE_ENV=production;nonemeans 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.
- Installation. An administrator switches the module on under System administration → Modules.
- Ministry assignment. Scope
all(every ministry the module is available to), orassignedto 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
impactfunction) and requires the module id to be typed in.
- Every change is audited (
module.*), recorded in the change history and sent as the webhookplatform.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:
| Where | What happens |
|---|---|
| API routes | 404 { code: 'MODULE_DISABLED', module }, every method, every caller, anonymous included |
| Policy | The module's actions are refused at step 0 |
| Jobs | Runs are recorded as skipped; run() is never called |
| Services | A service with a capability the module owns is hidden from the catalogue; a new application gets 409 SERVICE_MODULE_DISABLED |
| Back office | Menu entries disappear; a bookmarked page shows "This function is not enabled" (administrators get a link to the Modules page) |
| Portal | Its generic blocks disappear; its services show as not available |
| POS | Its 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 asmodule.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.
ModuleProviderloadsGET /api/modules/me. A module renders only when the server lists it, anda 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,legacyServiceIdsandi18nNamespaces. Routes, My Workplace sections, home-page scenarios, service handlers, action types and payment kinds need the product owner's decision and an entry inportalNeutrality.allow.js;portalNeutrality.test.jsfails otherwise. Do not add to the allow-list yourself. Portal texts go inpublic/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 andheartbeat 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:
| Setting | Hub | Node |
|---|---|---|
MODULES_AVAILABLE | the hub's modules plus those of hosted and external MDAs | the 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_BOOTSTRAP | modules switched on at first start | the 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