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
On this page

Architecture

This guide describes how the Pamodzi e-Services framework is put together: its components, the three roles an installation can run in, the path a request takes, where each kind of data is kept and the environments the platform runs in.

Components

ComponentTechnologyTalks to
National public portal (public-portal-ui)React single-page applicationThe API of the installation it is served from
Back office (back-office-ui)React single-page applicationThe API of its own installation
Platform API (zcm-backend)Node.js, Express, Sequelize, PostgreSQL with PostGISIts database and storage; ZamPay, ZamPass, SMS and e-mail gateways, NGDR, register sources (all through an outbound allow-list); peer installations over /fed/v1
Background workersThe same code base, job runner and queuesThe same database
POS app (GSB-POS)AndroidThe API of its installation
Signing servicePart of the platformSeals output documents and audit anchors

Every installation is one copy of the same image. What differs between installations is configuration: environment variables and Helm values, never code branches.

The three platform roles

The setting PLATFORM_ROLE decides what an installation does.

RoleWhat it isHolds
combinedHub and node in one installation. The default, and how every installation runs todayEverything
hubThe national portal. There is exactly one. It must scale horizontallyCitizen accounts, the catalogue copy, the node registry, index data about applications and payments
nodeOne MDA's own back office, API and databaseThat MDA's services, cases, documents, payments and registers

Each installation has a permanent name (NODE_KEY, for example hub or an MDA's short key) and its own ES256 signing key, published at /.well-known/jwks.json.

  • A node trusts its hub from configuration (HUB_URL, HUB_NODE_KEY, optionally pinned keys).
  • The hub trusts the nodes in its registry (System administration → Federation). Every change to the registry

    needs a second administrator and is audited. Suspending a node takes effect at once.

The federation contract

Hub and nodes talk through the versioned /fed/v1 contract. Its OpenAPI file and JSON Schemas live in zcm-backend/docs/federation/, and the code reads the same files.

  • Every message carries a detached JWS signature in the X-GSB-Fed-Signature header, made with ES256 over the

    canonical JSON body.

  • The signature names the sender (iss), the receiver (aud), the method and path, and a single-use id (jti). A

    message older than 5 minutes is refused, and a second delivery of the same message is refused as a replay.

  • Answers are signed too, so a caller can tell a real answer from a forged one.
  • The transport is mutual TLS with certificates from the federation certificate authority.
  • Writes carry an Idempotency-Key, so a retried message is applied once.

In combined mode the same calls are signed and handed to the receiving code in-process (the loopback adapter). The same checks run, without a network hop. A change to a federation message is a contract change: the schema is updated, kept backward compatible (or hub and nodes ship together), and tested with two real instances.

Request paths

The paths below describe a service that lives on a node. In combined mode the same steps happen inside one installation.

Applying

  1. The citizen finds the service on the hub. The hub's catalogue is a copy of the node's signed service manifest,

    received by federation sync.

  2. The citizen signs in on the hub and fills the mirrored form. Drafts are saved on the hub with a HUB- reference.
  3. Uploaded files are staged on the hub with the same checks as any upload: type by content, damage, password

    protection, antivirus.

  4. On submit the hub checks the form's rules and required documents, then relays the application to the node

    (submissions.create) with a citizen assertion: a signed statement of who the citizen is and how strongly their identity was proved.

  5. The node verifies the assertion, fetches the staged files through short-lived signed links, creates the case and

    answers with its case number.

  6. Once the node acknowledges, the hub deletes the staged files. If the node is unreachable, the hub queues the

    application and retries; after 7 days it expires, its staged files are deleted and the citizen is told.

  7. From then on the node sends status events (case.submitted, case.status_changed, action.required). The hub

    keeps only the index data needed for "My applications".

Paying

  1. The node raises the invoice on its own ZamPay merchant and revenue code, and tells the hub (payment.invoiced).
  2. The citizen sees the amount due on the hub. To pay, the hub asks the node, which answers with a short-lived signed

    checkout link that leads to the node's own ZamPay checkout.

  3. ZamPay calls the node back. The node finalises the payment through its single finalisation path and sends

    payment.settled to the hub.

  4. The hub never holds a node's ZamPay keys, never marks a node's invoice paid and never keeps a copy of a receipt.

Verifying a document

  1. Anyone opens the verification page on the hub, usually by scanning the QR code on a document.
  2. The hub looks the code up in its own register first. A code that carries a node key goes to that node

    (verify.document). Other questions go to the nodes that serve the issuer, then to legacy verification services.

  3. The answer says whether the document is genuine and whether it is still valid. Verification must never overstate

    validity: expired, suspended and revoked documents say so.

The public verification page looks up codes only, never document numbers, and is rate-limited against guessing. Partner systems verify by document number through the Partner API with the permits:verify scope.

Where data lives

DataKept onNotes
Service design: forms, processes, fees, documents, templatesThe owning nodeThe hub never becomes the place where a node's service is designed
Catalogue copyHubReceived by federation sync; only the passport and form needed to apply
Citizen account and sign-inHubNodes receive a pairwise subject and, when the rules allow, the NRC with its verification flag
Application answers, officer notes, uploaded documentsNodeStaged on the hub only until the node acknowledges
Application index: numbers, status wordsHubFor "My applications"
Invoices and receiptsNodeThe hub keeps an index of amounts due, for display only
Issued documentsNodeThe hub relays verification and never keeps a copy
National register recordsThe register's home installationRead elsewhere only under a data-sharing agreement
Audit trailEach installation, for its own actionsBoth sides of a federated call audit it with the same federation id

The rule is short: data stays with its owner. The hub keeps only the index data it needs: numbers, status words, amounts due and document labels.

Environments

EnvironmentWhereNotes
DevelopmentA developer's machineLocal PostgreSQL 18 with PostGIS. Integration tests create a fresh database per test file. Local tools are never pointed at a remote database
Testeservices.pamodzi.co.zm (public portal) and admin.pamodzi.co.zm (back office)Shared test deployment for demonstrations and product-owner review. Deployed only when the product owner asks. Never real citizen data. Uses the GSB test tenants of ZamPay and ZamPass
StagingGovernment servers (GSB RKE2 cluster)Same infrastructure as production. Images are promoted by digest through the image registry
ProductionGovernment servers (GSB RKE2 cluster)Changes reach it only by promotion from staging, with approvals. No direct deploys
  • Configuration differs per environment only through environment variables and Helm values.
  • Test and staging use provider sandboxes or stubs until real credentials are issued.
  • Production refuses development keys, such as the local signing key or the development certificate authority, and

    refuses to start when a required secret is missing or holds a placeholder value.

Next steps