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

Policy, routes, data, jobs and settings

A generated module works, but it does very little. In this lesson you give it real behaviour while keeping to the platform's security and data rules: authorisation on the server, ministry scope, audited changes, migrations that always run, idempotent jobs and safe settings.

Policy actions

The platform has one policy decision point (PDP) in lib/policy. One function answers "may this user do this to this thing?", and every route names the action it needs. Hiding a button is never authorisation.

A module declares its actions in policy.js:

// modules/fisheries/policy.js - do not require lib/policy/rules.js here (it is still loading)
const { STAFF } = require('../../lib/roles');

const RULES = {
  'fisheries.read':  { area: 'fisheries', grants: [{ roles: STAFF }], deny: 'role',
                       label: 'See fisheries records' },
  'fisheries.write': { area: 'fisheries', grants: [{ roles: ['Case Officer', 'Supervisor', 'Executive'], scope: 'mda' }],
                       deny: 'role', label: 'Change fisheries records' },
};
module.exports = { RULES };

Rules for module actions:

  • Every action starts with <id>., and no core action may use that prefix. The registry tags each rule

    module: '<id>' and merges it into the rule set.

  • Every rule has a label, shown on the permission screens. The contract test requires it.
  • Grants work as in core: { roles }; { roles, scope: 'mda' } or 'mda-strict' (the resource's ministry must be the

    officer's); { owner: true } (the applicant's own record); { primary: ['admin'] }.

  • Administrators may grant module actions to custom roles. If you rename an action, ship a migration that carries the

    custom roles over.

The PDP runs its steps in a fixed order and the first failure decides: 0 module, 1 account, 2 capability, 3 scope, 4 classification, 5 access list, 5b separation of duties, 6 record state, 7 fields. Step 0 is what makes a switched-off module's actions disappear: a tagged action is refused with 404 MODULE_DISABLED while the module is off for the caller.

Ministry scope

Records carry their owning ministry in mdaId. Spread lib/moduleOwner.attributes(DataTypes) into your model and call ownerHooks(Model) in associate; mdaId is then filled from the officer's ministry on create. Lists filter by lib/roles.officerMdaScope(user), as the example module's controller shows. Scope data to the caller's ministry on the server, always.

Routes

// modules/fisheries/routes/index.js
const express = require('express');
const { isAuthenticated, can } = require('../../../middleware/auth');
const records = require('../controllers/records');

const router = express.Router();
router.get('/records', isAuthenticated, can('fisheries.read'), records.list);
router.post('/records', isAuthenticated, can('fisheries.write'), records.create);
module.exports = router;
  • Mounting. Each mount in the manifest becomes its own app.use(mount, moduleGate(id), router), so the gate never

    covers a core route. New code mounts under /api/modules/<id>.

  • The gate. While the module is off, for the installation or for the caller's ministry, the gate answers 404 with

    { message: 'This feature is not available on this system.', code: 'MODULE_DISABLED', module, requestId }, for every method and every caller, anonymous included.

  • Guards. Every route names one policy action. A route that genuinely cannot (a signed, expiring link, for

    example) must be listed with its reason in the route guard tests; the contract test fails otherwise.

  • Audit. A route that changes data writes a named audit record through

    services/auditService.createAuditRecord. An audit coverage test checks this.

  • Validate every input, and never put tokens, NRCs or other personal data in URLs.

Migrations: one stream

Module tables are created by migrations in the one migrations/ folder, named <YYYYMMDDHHMMSS>-<id>-<what>.js.

  • They always run, whether or not the module is on. Every installation has the same schema; switching a module on

    is a data change, never DDL.

  • The timestamp must be later than every existing migration. The generator picks one.
  • Core migrations never touch a module's tables. A module migration creates only tables listed in the manifest's

    tables. Module tables may reference core tables such as cases, payments, users and mdas.

  • Migrations are additive and reversible, and safe to re-run (check before you create). A destructive step needs

    the expand/contract marker that scripts/check-migrations.js looks for.

  • Keep models and migrations in step: npm run db:schema-diff compares them.

Because migrations always run, the generated example module's empty table exists in every installation, even where the module is never switched on.

Jobs

jobs: [{ name: 'fisheries-daily', cron: '30 5 * * *', tz: 'Africa/Lusaka', catchUp: false,
         run: () => require('./jobs/daily').run() }]

The worker schedules a module job only while the module is on, and re-schedules on a switch. A run that starts while the module is off is recorded with the outcome skipped, and run() is never called. Job names are unique across core and modules. Jobs must be idempotent: a manual run, a catch-up or a retry may run them twice, so use claims and unique keys.

Settings

settings: {
  reminderDay: { type: 'integer', label: 'Reminder day', min: 1, max: 28, default: 5 },
  channel:     { type: 'enum', values: ['sms', 'email'], default: 'sms' },
  revenueCode: { type: 'string', label: 'Revenue code', pattern: '^[0-9][0-9A-Za-z._-]{2,31}$' },
}
  • Types: string (pattern, maxLength, at most 2,000 characters), integer and number (min, max),

    boolean, enum (values).

  • Administrators edit them under System administration → Modules → Settings, for the installation or one MDA.

    The server validates them (400 SETTINGS_INVALID with field errors) and audits every change.

  • The effective value is the default, overridden by the installation value, overridden by the MDA value.
  • Read them with require('lib/modules').settingsFor('<id>', { mdaId }) in the backend, or useModules().settings('<id>')

    in the back office.

  • Never put secrets in settings. They are visible to administrators and stored as plain JSON. Secrets come from

    the environment or the secret store.

Keep ministry names, hosts and service ids in settings or data, never in code. That keeps check:mda green and lets the same module serve another installation.

Check your understanding

  • In what order does the PDP run its steps, and what does step 0 do for a module action?
  • Why is each module mount wrapped in its own gate instead of one gate for all of /api?
  • What two things must every write route of a module do?
  • Why do module migrations run even when the module is off?
  • What happens to a module job's scheduled run while the module is switched off?
  • Where should a module keep a revenue code, and where should it keep an API secret?

Sign in to record your progress.

Previous: Anatomy of a module and the generator Next: Plugging into core through extension points