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

Geospatial data and NGDR

Many e-Services need a place: a plot, a licensed area, a borehole, a route. The platform has one generic set of geospatial capabilities that any service and any module can use. This guide covers how the platform reads the National Geospatial Data Repository (NGDR), how citizens and officers capture shapes, how shapes are checked, and how geodata is published to partners and to the public.

All geometry the platform stores and exchanges is WGS 84 longitude/latitude (EPSG:4326), in GeoJSON axis order. Transforms, areas and spatial relations are computed in PostGIS.

NGDR and the geodata provider

NGDR is the national repository of reference geodata. It publishes its layers through a GeoServer over OGC web services. (The separate ngdr web application is a Vite and OpenLayers front end that displays those GeoServer layers; it is not part of the e-Services platform.)

The platform reads NGDR through a geodata provider port in zcm-backend/lib/geo/providers/. The adapter is chosen with GEO_PROVIDER:

ValueMeaning
offThe default. No provider; stored layers keep working.
stubSynthetic test fixtures. A warning is logged if it is used in production.
ngdrThe NGDR adapter.

The NGDR adapter only reads. It uses WMS 1.3.0 and WFS 2.0.0 capabilities to list layers and WFS GetFeature (GeoJSON, EPSG:4326, paged) to fetch features. WMS map images are off by default (GEO_NGDR_WMS_IMAGES). It never writes to NGDR: there is no WFS-T and no database connection. The plan is for NGDR to harvest the platform's own partner feeds instead (see below).

Safety rules built into the adapter:

  • GEO_NGDR_OWS_URL must be https in production, with no credentials and no query string in the URL.
  • The host must be on the allow-list GEO_NGDR_HOSTS; outbound calls go only to that host.
  • An authorisation header, when GeoServer needs one, is read from an environment variable whose name is given in

    GEO_NGDR_AUTH_SECRET_NAME. The value is never logged.

  • A live call under the test runner throws; tests use the stub or a fake fetch.
  • The capabilities parser refuses DTDs and entity expansion.

The production NGDR endpoint is still to be confirmed with NGDR, so installations run with off or stub until it is.

Reference layers and refresh

Reference layers (districts, reserves, restricted areas, grids and so on) are stored in the core tables geo_layers and geo_features, with versioned features. A layer can come from a provider, from an administrator's upload, or from a module. The job geo-layer-refresh copies provider layers into these tables:

  • a new version is written only when something changed, and two versions are kept;
  • a failed, partial or empty answer, or one that drops more than half the features, keeps the stored copy;
  • the extension point geo.layerRefreshed lets a module that keeps its own copy (for example a grid of cells) move

    with the layer in the same transaction.

Administrators manage layers with POST /api/geo/layers and POST /api/geo/layers/:key/import (policy action geo.layers.manage), both audited.

The geometry field

The form designer has a generic "Location / area on map" field (form-js type: 'geometry'). Any service can use it; no module is needed. The applicant can draw, type coordinates, upload a file or use GPS. The answer is a GeoJSON Feature:

{
  "type": "Feature",
  "geometry": { "type": "Polygon", "coordinates": [[[28.28, -15.41], [28.29, -15.41], [28.29, -15.42], [28.28, -15.41]]] },
  "properties": { "srid": 4326, "inputCrs": 20935, "source": "draw", "areaHa": 12.5, "centroid": [28.285, -15.414] }
}

The server never trusts areaHa or centroid: it recomputes them. Limits: 5,000 vertices per geometry, 1 MB per answer, 500 features per file and 10 MB per upload. Uploads may be GeoJSON, KML, KMZ, GPX, CSV, WKT or a zipped shapefile; Arc 1950 UTM zones and WGS 84 UTM are among the supported coordinate systems.

Field properties set in the designer include geometryTypes (required), captureModes, coordinateSystems, minAreaHa, maxAreaHa, withinLayer (a reference layer key), snapLayer, basemapLayers and validators.

The geo API (/api/geo, signed in, policy action geo.use) helps the form while the person draws:

EndpointPurpose
GET /api/geo/crsSupported coordinate systems
POST /api/geo/parseParse an uploaded file or text into WGS 84 features; nothing is stored; rate limited
POST /api/geo/transformConvert coordinates between systems
POST /api/geo/checkCheck one geometry: validity, area, layer rules and validators (read-only)
GET /api/geo/validators?serviceId=The validators offered for a service
GET /api/geo/layers, GET /api/geo/layers/:key/features?bbox=Layers and features (citizens see public layers only)

Geo validators

Core checks every geometry answer at submission: structure, validity as PostGIS sees it, the exact area against the field's bounds, and withinLayer. Modules add their own rules through the extension point geo.validators:

'geo.validators': () => [{
  name: 'fisheries.insideWaterBody',          // prefixed with the module id
  label: 'Inside a gazetted water body',
  auto: false,                                // run only when the field lists it
  run: async ({ geometry, areaHa, transaction }) => {
    const inside = await checkInside(geometry, { transaction });
    return inside ? [] : [{ code: 'NOT_IN_WATER_BODY', level: 'error',
      message: 'The area must be inside a gazetted water body.' }];
  },
}],

A validator runs when the field names it in properties.validators, or when it is auto and applies to the service's capabilities. A problem with level: 'error' blocks submission; warning is shown but does not block. The same rules run live in POST /api/geo/check and at submission.

Module layers and the map workspace

A module that keeps shapes publishes them to every back-office map through the extension point geo.layers: a live, read-only layer with an attribute allow-list (core drops every other property) and a visibility of public, staff or mda. The core Map workspace in the back office lists these layers next to the reference layers, with filters, search, identify and export. A switched-off module's layers answer 404 MODULE_DISABLED. No personal data appears in a layer unless an attribute is marked restricted, and restricted attributes are shown only to callers who hold the layer's detail action.

Partner feeds

Partner systems, NGDR among them, read the platform's geodata as read-only GeoJSON feeds through the Partner API (OAuth 2 client credentials, scope geo:read):

GET /api/v1/geo/feeds                 -> { feeds: [{ key, title, description, href }] }
GET /api/v1/geo/feeds/:key?limit=1000&offset=0&bbox=27.5,-16,29,-15&updatedSince=2026-10-01T00:00:00Z
  • Feeds are contributed by modules through geo.feeds and listed only while the contributing module is on. Core

    never names a ministry.

  • Answers are application/geo+json with OGC API Features style paging: limit 1 to 2,000 (default 1,000),

    offset, bbox (min lon, min lat, max lon, max lat) and updatedSince; numberMatched, numberReturned and next / self links.

  • Answers carry an ETag, so a harvester polling an unchanged feed gets 304 and no body.
  • A feed carries no personal data. That is the contributing module's promise, checked by its tests.

Another platform installation (for example a ministry node) can overlay a feed on its own maps: its administrator adds a stored layer with the provider gsb-node, and the refresh job keeps a copy.

The national portal's generic page "Find a property or parcel" lets anyone search registers of shapes and see outlines on a map. It is anonymous by design and served at /api/public/geo-search:

EndpointPurpose
GET /api/public/geo-search/sourcesThe registers the public may search
GET /api/public/geo-search/:source/search?by=&q= (or lon, lat)Search by key, survey record or diagram number, or by a map click
GET /api/public/geo-search/:source/features?bbox=&zoom=Outlines in a map view: key and state only

Sources come from data, never from code: typed registers whose schema pack declares a geometry and a public search and that a second person approved for the public; module sources contributed through geo.publicSearch; and the parcel register (parcels.lookup) when a cadastre module is on.

What the public sees is limited by core whatever a source returns: key, label, status, state (current, retired, cancelled), areas, survey record, diagram number, the facts the pack lists, successors of a retired parcel, and a simplified outline. Owner names are hidden unless the source's disclosure is approved as masked, and then only as initials and surname. Identity numbers are never shown. Map views are refused below zoom 14, for boxes over 0.01 square degrees, and are capped at 300 outlines. Searches are throttled per address (GEO_SEARCH_PER_10_MIN, 120 by default) and answers carry an ETag.

On the national hub, nodes that advertise the geo-search capability add their sources as <key>@<nodeKey>, and the hub relays searches to them over the signed federation contract (see Federation: hub and nodes).

The verification page uses the same machinery: a document whose metadata names a parcel shows the parcel's outline, and a document whose parcel was subdivided, consolidated or cancelled is answered superseded, not valid.

For module authors, the geo extension points are listed in Extension points.