Skip to content

PetroNet System

Mongolia's integrated fuel monitoring and management platform

PetroNet collects the import, storage, distribution and retail of petroleum products across Mongolia into one data flow, watches it in real time, and gives the regulator, the fuel companies and the driver the same numbers. It is built for the Mineral Resources and Petroleum Authority (MRPAM) and replaces the current mpetro system — see the system requirements.

PetroNet System is not one application — it is an ecosystem. A driver, a station, a fuel company and the regulator are doing four different jobs, and each has a platform built for that job. Underneath, all of them share one set of data, one identity and one audit trail.

Монгол  ·  العربية  ·  中文  ·  English  ·  Français  ·  Русский  ·  Español

License Go Version Next.js


Contents


The problem

Mongolia has more than 200 fuel companies, over 110 storage depots and more than 1,500 filling stations. Their data lives in two systems that do not talk to each other, and most of it is typed in by hand, once or twice a week.

There is no single place that can answer, right now, how many litres of which grade are where. So national reserves are managed by estimate, and when supply tightens the only instruments left are blunt ones — odd-and-even plate days, a flat ₮50,000 cap per fill, queues.

That is not only a supply problem. It is an information problem, and information problems are the kind software can actually solve.

What the platform does

Public page
1 Chain of custody — import contract → customs → laboratory → terminal → transport → station tank → nozzle → customer. Every litre is traceable back to the batch it came from. /supply
2 Station POS — a driver layer that speaks to dispensers and tank gauges of any make, from a modern forecourt controller down to a pulse counter, and keeps selling when the network is gone. /stations
3 Vouchers — entitlements minted only from fuel that has physically arrived, then allocated by proximity, need and time waited. /vouchers
4 State oversight — stock, price, quality, tax and discrepancy on one board, over an audit trail that cannot be rewritten. /oversight

It runs in two modes on one set of rails. In a crisis it rations: limits and quotas change in minutes, priority classes keep their reserve, vouchers go out with a time window. The rest of the time it supervises: tax, price, quality and stock monitoring, automatic import-to-sale reconciliation, demand forecasting and strategic-reserve alerts.

The ecosystem

Platform For whom What it carries
Citizen platform Drivers and citizens The nearest station, its grades, how full it is, the daily entitlement and the voucher. Identity comes from eID
Company portal Fuel companies Depot and station registry, tank balances, and the flow for filing and correcting each reporting period
Regulator's command centre MRPAM and inspectors The national daily aggregate, days of stock, coverage gaps, reconciliation discrepancies and alerts
Station POS and edge agent Filling stations Nozzles, tanks, shifts, payment and receipts, over a local database: the sale does not stop when the network does
Warehouse and analytics Analysts and management Fed daily from the operational systems, cleaned and modelled in business language, with BI and forecasting on top
Console and observability Operators Organisations, permissions and audit behind their own sign-in, beside metrics, alerts and backups

Under all of them sit one Go binary, one database and one identity: an ecosystem here means many workspaces, not many servers. Outward it connects to customs, the state registry, e-Barimt, eID and the tank gauges in the field.

The design decision

A voucher is not a promise. It is a reserved litre.

A voucher comes into existence only after fuel has physically entered a station's tank and the automatic tank gauge has confirmed the level rise.

Two things follow. The system can never promise more than it holds, so the queue has nothing to form around. And a station that does not report its deliveries generates no vouchers, so nobody is sent to it — compliance is enforced by the design rather than by an inspector.

Chain of custody

Stage What is recorded Where it comes from
Import Contract, supplier, grade, tonnage, Incoterms, price, expected date Importer portal / API
Border Declaration number, HS code, duties, crossing point Customs
Quality Octane, density, sulphur, water, laboratory certificate Accredited laboratory
Metrology Observed litres, temperature, density → litres at 15 °C ASTM D1250 / API MPMS 11.1
Terminal Tank, capacity, level, free capacity, strategic-reserve transfers Tank gauges
Transport Tanker, driver, loaded volume, destination, GPS track, e-seal Carrier module
Station Received volume, level rise, discrepancy, receiving staff Tank gauge + confirmation
Nozzle Totalizer, per-transaction litres and amount, shift readings Forecourt controller
Customer Entitlement, voucher, redemption, receipt, VAT PetroNet + e-Barimt

Each node reconciles against the one before it, so a discrepancy names when, where and in whose custody it appeared.

What already exists

Not a plan — a record of what is in the repository, with tests, run against a real PostgreSQL. The full list, and what comes next, is in the development plan.

  • Registry of depots and stations, plate numbers, status, XYP verification
  • Product reference with JODI classification, seven grades
  • Regulator permissions, with a row-level policy behind them
  • Policy as data — limits, tolerances, deadlines, changeable without a release
  • Station technical inventory (classes A–D)
  • Reporting periods, submissions, lines and conclusions
  • Validation rules — balance, continuity, capacity, deviation, metrology
  • 15 °C volume correction (ASTM D1250 / API MPMS 11.1)
  • Report versioning and a hash chain over it
  • Excel template export and import
  • Review workflow — approve, return, four-eyes
  • Movements, with plate number, closing state and discrepancy
  • National daily aggregate, coverage, days of stock remaining
  • Missing-data detection and a coverage-gap report
  • Reconciliation ΔA–ΔE
  • Seven reports as Excel and CSV, scheduled and emailed
  • Open data: the daily national aggregate at /api/v1/petro/public/daily
  • The company reporting screen and the regulator screen

How it is built

PetroNet is a Level 2 distribution of the Gerege Nexus platform. There is no core code in this repository — one line of go.mod is the whole of it. What lives here is the fuel business logic (modules/petro/) and the map, operator screens and public pages built for it (frontend/).

The module registers its own routes, menus, permissions and migrations through the public pkg/nexus contract and compiles into a single Go binary. Identity, tenancy, RBAC, SSO, reporting and the audit trail come from the platform and are not rewritten here.

The deployment authenticates people itself: its own sign-in, its own OIDC issuer, its own database. Citizens are identified through eID Mongolia rather than a password this system would have to keep.

Repository layout

main.go                   Registers the petro module and starts the platform host
modules/petro/            The fuel module: registry, reports, oversight, vouchers
  migrations/             This module's SQL, one history
cmd/petro-import/         Importer for the existing mpetro data
catalog/                  App catalogue, manifests and version chronicle
frontend/                 Next.js web client — public site, map, operator screens
deploy/                   Dockerfile, compose stack, monitoring, backup scripts
nginx/                    The six vhosts this deployment answers on
docs/                     This documentation, in seven languages

Getting started

Prerequisites: Go 1.26+, Node.js 20+, PostgreSQL 16+ (or Docker).

# Everything at once
docker compose -f deploy/docker-compose.yml up -d

# Or the API on its own
go run .

# And the web client
cd frontend && npm ci && npm run dev

The web client answers on http://localhost:3000.

A deployment with no organisation yet sends every visitor to /setup. The token that wizard asks for is written to the API log once, at boot:

docker logs petronet2_backend 2>&1 | grep -i "setup token"

Configuration

The complete list is in .env.example. The values that decide how a deployment behaves:

Variable Description
PUBLIC_ORIGIN Where this instance answers. Defines CORS, the OIDC issuer and the eID callback in one place
PETRONET_POSTGRES_PASSWORD This stack's own database
SSO_DEFAULT_CLIENT_SECRET The platform refuses to start in production without it
BRAND_* The deployment's name, description, colours and icons
SERVICE_URL_* The addresses of the console, warehouse, backups, monitoring and documentation. Only the ones set are drawn on the front page
EID_RP_UUID / EID_RP_SECRET The eID relying-party pair. Without them eID sign-in is unavailable
CONTROL_PLANE_HOST The hostname the operator console answers on, and nowhere else
PROMETHEUS_URL Where the console reads platform health from

Deployment

The production host carries /opt/petronet-v2/ — src/ (a git checkout of this repository), .env (chmod 600) and brand/. Nobody runs git pull there: the only path to production is git → CI → the self-hosted runner on the host:

commit → PR → main → CI (ci.yml) ✓ → Deploy (deploy.yml)
  → self-hosted runner: deploy/ci-deploy.sh <sha>   # git reset --hard <sha>
  → ./deploy.sh                                     # build, migrate, verify, rollback

deploy.sh builds both the backend and the web image from this repository, so the API, the map and the operator screens always ship at one revision.

Six hostnames stand beside each other: the platform (petronet.mn), the operator console (admin.), monitoring (monitor.), the warehouse map (dwh.), this documentation (docs.) and the backup notes (backups.). What each is, and the traps in the nginx configuration, are in this deployment's document.

Testing

go vet ./... && go test -race ./...     # Go: unit and PostgreSQL integration
cd frontend && npm test && npm run build

CI runs both on every push and pull request, and builds both Docker images.

Security

  • Session tokens are random 256-bit values; only their SHA-256 digest is stored.
  • Passwords are hashed with bcrypt, and sign-in attempts are rate limited.
  • Tenant data is isolated by a database role, a tenant context and row-level security on the declared tables. The regulator's reach is a policy in SQL, not a check in a handler.
  • Report versions are chained by hash, so an approved submission cannot be edited without the chain saying so.
  • The operator console has its own identity, cookie, audit trail and database role, and answers only on CONTROL_PLANE_HOST.

Report vulnerabilities as described in SECURITY.md.

Documentation index

Document Description
Documentation hub Every document and translation
System requirements What the customer asked for
Development plan What is built, what comes next, and the acceptance criteria
Benchmarks How other countries solved this, and what failed
This deployment Hostnames, ports, backups — only this host
Architecture The planes, the schemas, how data is isolated
Writing a module The pkg/nexus contract
Operations Deployment, monitoring, backup and restore
Runbooks When something breaks
Translation The language policy and the generator
Contributing · Security · Code of conduct Project conduct

License

Copyright (c) 2026 Gerege Systems Development Team, Gerege Nomadica Foundation. Distributed under the Apache 2.0 License — see LICENSE.

Flag icons by Flaticon (attribution).