GN Gerege Nexus
GitHub Нэвтрэх

Changelog

All notable changes to open-gerege-nexus (Gerege Nexus) will be documented in this file.

Entries below the rebrand keep the names that were true when they shipped — the open-gerege-mn-erp repository, the ERP framing, and the openerp.gerege.mn deployment, which has since moved to nexus.gerege.mn. A changelog edited to match the present tense stops being a record.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.


[Unreleased]

[1.7.0] - 2026-08-15

Changed — Contacts left for the commerce distribution, and the sidebar was rearranged

Three changes with one shape: putting each thing where it belongs.

The contact register is a product's, not a platform's. Migration 00059 folded Contacts into the Directory this morning, on the argument that who an organisation is made of and who it deals with are one subject. That was half right, and the wrong half is the half that decides where code lives: departments and staff are something every organisation has; customers are something a business has. So io.gerege.nexus.contacts is an app again — built and shipped by commerce-gerege-nexus — and io.gerege.nexus.organisation is the organisation again at 3.0.0, under the name it had before the merge.

The sidebar says where things are. Installed apps sat under Modules while its page is at /settings/apps, which asked somebody to hold two answers for where the same screen lives; it is under Settings now. The organisation moved the other way, up into Modules, because it is a thing you look at and edit rather than a switch that changes how the platform behaves — and its two screens, departments and people, are indented beneath it instead of listed beside the App Store as though the three were unrelated destinations.

[1.6.0] - 2026-08-15

Added — A distribution can read its own catalogue

catalog.LoadFile and the pieces under it — LoadEntries, Assemble, LoadManifest, LoadChronicleFile, ReleaseNotesFor — moved from internal/platform/appcatalog into the contract package.

The App Store found this the way these things are always found. A test wanting to check its bundled catalogue against the modules compiled beside it could not read the catalogue: the loader was in internal/, which is the rule that makes distributions possible, working exactly as designed and against the product it was designed for. The alternative was a second implementation of "how a catalogue is read" — a second answer to whether a manifest is valid, agreeing with the first until the day a deployment and the store it publishes to disagreed about an app nobody had touched.

The platform's own loader is now three lines over the public one, keeping the one thing that is genuinely this repository's: the deprecated app-id renames, which no distribution should inherit.

[1.5.0] - 2026-08-15

Added — Apps can be published to named platforms instead of to all of them

An app now declares a visibility: public, which every platform may be offered, or private, which only the platforms the registry names may be. Empty means public, so every manifest written before this marshals to the bytes it always did — the signed catalogue stays byte-reproducible across the two repositories that build it.

It is enforced by the registry, not by the platform, and that is the whole design rather than a shortcut. A private app is kept from a platform by not being in the catalogue that platform is served. The alternative — ship every platform the same document and ask each to hide what it should not see — leaks the names of private apps to everyone holding the catalogue, and asks the party with the motive to look to be the party that decides.

The other half of this lives in appstore-gerege-nexus: which platform may see which private app, and GET /catalog answering per deployment. One trap to carry over — the ETag has to vary per deployment too, or a platform whose entitlement is withdrawn keeps its old catalogue on a 304. See docs/ECOSYSTEM_GIT_STRATEGY.md §3.1.

Fixed — Installed apps listed four that this binary cannot run

The settings screen read straight from app_installations, so it showed nine rows under its own banner saying the catalogue has five. State Services, Products, Inventory and Billing were all listed as installed and active — months after their code left for gov-gerege-nexus and commerce-gerege-nexus — each with a button offering to disable an app that was not running. Nothing about them worked: no routes mounted, no menu entry, and the compile-time check refuses them at install. The row was all that was left, and a row that says "Active" about an app with no code is worse than no row.

The store already had this rule (runnableHere, added when State Services left); the list of what a tenant has never got it. Both screens ask the same question now: if the catalogue knows the app, it has to be runnable here; if the catalogue has never heard of it, a compiled module is enough — a distribution's own module is real from the moment the binary starts and may reach a catalogue minutes later or never.

The rows stay in the database. The apps went to distributions this deployment may yet run, and deleting an installation because a screen cannot render it is the wrong way round.

Changed — Two cards became one app: Contacts moved inside the Directory

"Organisation & People" and "Contacts" were one subject cut in half — who this organisation is made of, and who it deals with. An administrator who installed one and not the other had half a directory, and nothing in the store said which half was missing. So io.gerege.nexus.contacts is gone and its register is part of io.gerege.nexus.organisation, now called Directory (mn: Бүртгэл) at version 2.0.0.

The contacts table, the API path /api/v1/contacts and the screen at /contacts are all unchanged. What moved is the app the register belongs to.

Changed — Two cards became one app: PDF E-Sign moved inside Documents

The store carried "Digital Documents & Signatures" and "PDF E-Sign" side by side. They answered one question — where are my documents and who has signed them — and nobody adopts a signature on its own; they adopt it because something has to be signed. So io.gerege.nexus.esign is gone and its rails are part of io.gerege.nexus.documents, now called Documents (mn: Баримт бичиг) at version 2.0.0.

Nothing about how a document is signed changed. The eID and HSM rails, the signature log, batch signing, stamp placement, the HSM connection and the two reports are the same code reading the same tables.

Changed — The image no longer knows its own name

lib/apiBase.ts took the deployment's address out of the build. This takes its identity out, which is the second half of the same argument: one image, a different .env, a hundred deployments is only true when neither is baked in.

Five values are read from the environment per request — BRAND_NAME, BRAND_SHORT_NAME, BRAND_DESCRIPTION, BRAND_LOGO_URL, BRAND_THEME_COLOR — and all of them are optional. Unset is Gerege Nexus, so nothing about nexus.gerege.mn changes. They reach the document title, the PWA manifest (which is what an installed copy keeps under its icon), the header, the sign-in and consent screens, the workarea footer, the operator console and the landing page's chrome.

See docs/ECOSYSTEM_GIT_STRATEGY.md §2.3 and §6.

Removed — State Services left, and is a product of its own

The second distribution split. apps/gov_services now lives in gov-gerege-nexus.

No composition image, and nexus.gerege.mn simply stops offering the app. That was not the plan an hour before it shipped; the plan was a repository whose only job is to build core-plus-verticals into one image for this deployment. Production answered the question instead. It carries two tenants; the app is installed on one of them and disabled there; gov_services, gov_applications and gov_appointments hold zero rows between them. A repository, a pipeline and their maintenance, to keep showing an app nobody has switched on. The ecosystem strategy tells distributions to choose the lower level when in doubt (§1), and the rule reads the same when it is pointed at us.

The distribution exists and is green, so the day somebody wants State Services it is a deployment rather than a project.

[1.4.0] - 2026-08-15

Removed — Commerce left, and is a product of its own

The third and last of the planned vertical splits. apps/products, apps/inventory and apps/billing now live in commerce-gerege-nexus.

As with State Services, no composition image: production carries two tenants, and products, stock_levels and billing_invoices hold zero rows between them. A deployment that wants commerce runs the distribution.

[1.3.0] - 2026-08-15

Added — the reporting contract and the document capability, both so a module elsewhere can use them

Changed

[1.2.0] - 2026-08-15

Added — a module can now state its own access policy, and book a meeting

Both are the same discovery from two directions: the platform held things a module should have held, and a module could not be moved until it did.

gov_services now depends on pkg/nexus and nothing else from this repository, which is what makes the next split possible.

Removed — the App Store left, and is a product of its own

The first distribution split. apps/appstore_registry, apps/publisher_studio and apps/store_review now live in appstore-gerege-nexus, which takes this platform as a dependency by tag and adds nothing but three modules and the line that registers them. Every other deployment stopped carrying them as dead weight the day they left.

[1.1.0] - 2026-08-14

Added — pkg/platform, so a distribution can start the platform it compiles against

The SDK let somebody write a module and gave them nowhere to run it: booting is internal/platform.NewServer, which the language closes to every other repository. docs/ECOSYSTEM_GIT_STRATEGY.md §2.5 sketches a distribution's main.go as a call to platform.Run(); this is that function.

Added — pkg/catalog, the app-store contract, and a clean appstore boundary

Preparation for the first distribution split (gerege-appstore). The three store modules — the registry, the publisher studio and the review queue — were measured against the rest of the platform first, and the boundary turned out to be almost clean already: nothing in the core imports them, and the only thing holding them here was the catalogue schema, which lived in internal/platform/appcatalog where no other repository can reach it.

The three store modules now import pkg/nexus, pkg/catalog and each other, and nothing else from this repository. What still ties them here is three lines in internal/apps/runtime.go — the lines that become a distribution's main.go.

[1.0.0] - 2026-08-14

Эхний тогтвортой хувилбар: backend/pkg/nexus нь semver амлалттай нийтийн API болж, экосистемийн салгалтын 0-р (нэршил) ба 1-р (SDK) алхам дуусав. Энэ хувилбараас эхлэн distribution repo нь цөмийг fork хийхгүйгээр dependency болгон авч чадна — хувилбар гаргах журам.

Added — a release process, and the tests that make its promise checkable

Step 2 of the ecosystem split. A tag is the only way another repository can depend on this one, so a tag has to mean something; this is what makes it mean something.

Added — pkg/nexus, the SDK that makes a product possible without a fork

Step 1 of the ecosystem split (docs/ECOSYSTEM_GIT_STRATEGY.md §6), and the precondition for every step after it. The module contract lived in backend/internal/module.go, and Go forbids another repository from importing anything under internal/ — so the only way to build a product on this platform was to fork it, and one fork per product means every fix is applied once per product for ever.

Implementations stay in internal/. The SDK is a contract, and a contract that also carried the machinery would drag the machinery into the semver promise.

The service half followed, and with it §6 step 1 is complete: a module in another repository can now be written, not merely declared.

The surface was measured rather than designed. Across the fourteen modules the whole demand on the platform was: write a JSON response (420 call sites), name the organisation and the caller (94), refuse on a missing permission (24), record what happened (41), and query the database. The report engine (40 symbols), the catalogue, the state rails and the SSO provider are each a subsystem or a specialised rail, and none belongs in the first version of a contract that cannot be narrowed later. settings, flags and emailverify are in docs/ECOSYSTEM_GIT_STRATEGY.md §2.1's sketch of this package and are not here, because no module imports them.

Added — egov, the front door to the state's systems

The last of the three naming corrections, and the only one that creates a module rather than renaming one. The pieces existed and were scattered: the ХУР registry lookups were two handlers in the platform's own route table, whether the eID, ДАН and ХУР rails were even configured was knowable only from the deployment's environment, and what had been looked up sat in the audit log with nothing pointing at it.

What deliberately did not move into the app: the eID and ДАН sign-in flows, which run before anybody is signed in, and a person's own list of linked identities with the button that unlinks one. The second is the same reasoning profile_handlers.go has carried since before this module existed — an app is installed per organisation and an administrator can remove one, and somebody's ability to detach their own national identity is not their employer's to take away. The connections screen links to /profile rather than owning it.

Deprecated — to be removed in the next release

Changed — developer_portal becomes sso_clients

The second of the three naming corrections. developer_portal named the wrong thing twice: there is a real developer portal in this ecosystem — developer.gerege.mn, backed by apps/publisher_studio, where a third party submits an app to the store — and an administrator looking for that and landing here had nothing in the name to tell them they were in the wrong product. What this app actually is has no developers in it: CRUD over the OAuth2 clients registered against this platform's own OIDC provider, run by whoever looks after an organisation's integrations.

Deprecated — to be removed in the next release

Changed — core becomes organisation, and stops being undeletable

The first of three naming corrections made before the platform is published as an SDK, where a name becomes part of an import path and stops being cheap to change. core was the name of the app holding departments and people and the name of the platform underneath every app; one of the two had to give it up, and it is not the platform.

Deprecated — to be removed in the next release

Each of these is marked // DEPRECATED: remove in vNEXT at its definition.

Added — Control Plane Catalog Management & Migration Deprecation (CP-46)

Added — The break-glass account, and the storage limit that refuses

The last two things the design document asked for and the phases had not delivered.

Added — Counting what each organisation used, from the database rather than from the metrics

CP-5, the last phase of the control plane. usage_events holds one row per organisation per metric per day, written by a job that runs nightly and again during the day, so the console is never showing yesterday's picture to somebody looking at it after lunch.

Added — A front page that answers "is the platform well", and the platform's first backup

CP-4. The console's home screen is now the deployment's health — requests, errors, latency, the government systems' lights, what is alerting, the disk, which background jobs have quietly stopped, what version is running — and the organisation list moved to its own page. Every panel links into Grafana, and none of it tries to replace Grafana.

Added — A platform that can be told to behave differently, without a deploy

CP-3, and the setting it exists for: this platform is private by default. Until now, whether a stranger who could authenticate somewhere else became a user here was decided by which environment variables happened to be set — EID_JIT_TENANT_SLUG in one file, SSO_CLIENT_TENANT in another, read by two packages, with no single place that answered "can somebody get in".

Fixed

Added — Operating an organisation from the console, without being able to take it

CP-2 gives the console the buttons CP-1 deliberately withheld: creating an organisation, closing one, deleting one, helping somebody back into their account, and — with a reason and a banner — looking at the platform as they see it. Everything in it is shaped by one rule: an operator should be able to do their job without being able to do quiet damage.

Added — A console for operating the platform, kept away from the platform

Somebody has to be able to see which organisations exist, which apps they run and what has been done to them — and until now that somebody used psql. This is the first phase of the operator console described in docs/CONTROL_PLANE_PLAN.md: the foundation, on which suspension, support and configuration are built next. Guide in docs/CONTROL_PLANE.md.

Fixed

Added — One organisation seeing another's report, with their permission

A coal mine contracts a hundred transport companies. Each keeps its trips in its own tenant; the mine wants one consolidated "Transport" report. That request runs against everything this platform is built to prevent, so the answer is not to weaken the isolation but to add a separate, permissioned path beside it. §3.5 of the design; guide in docs/REPORT_SHARING.md.

Added — Reports, as a platform layer rather than a screen

io.gerege.nexus.reports. Every app that keeps data now has reports; the module serving them knows about none of them. Guide in docs/REPORTS.md.

Added — Traces, and errors that group themselves

The third pillar and the tool beside it, both env-gated and both off by default. Guides in docs/MONITORING.md §11 and §12.

Added — A monitoring stack that reads the platform

deploy/docker-compose.monitoring.yml: Prometheus, Alertmanager, Loki, Alloy, Grafana, node_exporter, cAdvisor, postgres_exporter, redis_exporter. Guides in docs/MONITORING.md and, for every alert, docs/RUNBOOKS.md.

Added — The platform can now be measured

/metrics carried two series: a request count and a request duration. That is the R and the D of RED and nothing else — no saturation, no business volume, no sign that a call to ХУР or eID had gone slow, and no way to tell a breach of the in-flight ceiling from any other 503. Everything a dashboard would need was missing before the dashboards were, which is why this lands before the stack that reads it (design: docs/MONITORING_AND_REPORTING_PROPOSAL.md).

Added — Signing in with Google

A "Google-ээр нэвтрэх" button beside eID on the platform's own sign-in screen, off unless GOOGLE_LOGIN_CLIENT_ID is set. It is an addition, not the federation added a moment ago: SSO_CLIENT_ISSUER closes this deployment's own sign-in paths and hands the question of who somebody is to a provider, while this is one more of its own answers and closes nothing. On a deployment that does federate, the button is withdrawn along with the rest — a front door nobody manages is exactly what federating was meant to remove.

Google is an ordinary OpenID Connect provider, so there is no second implementation: the same discovery, PKCE, code exchange and RS256 id_token verification serve both, and both land on the same (issuer, subject) account resolution. What is written separately is only what differs — which cookie the flow parks in, and who is allowed through.

Added — A deployment can now be an SSO client, not only a provider

The platform has always been an OpenID Connect provider: it could hand identities out and never take one in, so a group running several deployments had one sign-in per deployment and no way to make one of them the source of truth. This is the other half. Setting SSO_CLIENT_ISSUER makes a deployment a relying party of the provider named there — including of another Gerege Nexus — and the two halves are independent: an instance can be a provider, a client, or both, which is what a regional deployment federating upward while still issuing identities to its own installed apps needs. Full guide in docs/SSO_FEDERATION.md.

Added — RP-initiated logout at the provider (/oauth2/logout)

The discovery document has advertised end_session_endpoint since it was written, and nothing served it: a relying party that ended its own session and sent the person here — which is what a conformant client does — landed on a 404 while staying signed in. That is worse than not advertising it, because the next click on "sign in" looks like the logout was ignored.

Fixed — Two defects the new tests turned up

Without this, single sign-on is not single. A relying party signing somebody in sends the browser to /oauth2/auth, which is a top-level navigation arriving from another site, and a Strict cookie is not sent on one — so the authorization endpoint saw no session and showed a login screen to somebody who had signed in a minute earlier. It costs nothing in CSRF terms, because the cookie was never the defence: Lax adds exactly one thing over Strict, a cross-site top-level GET, and every state-changing request goes through security.CSRFMiddleware, which demands positive evidence that a page of ours made it.

Fixed — A lockout that never let go, and three silent truncations

Removed — Code that had stopped being reachable

Changed — The platform's apps stop calling themselves examples

io.example.* was placeholder vocabulary from the first week — the reverse domain of nobody, borrowed the way example.com is borrowed — and it had been the primary key of every app in the store ever since. These are Gerege Nexus's own apps and they now say so: io.gerege.nexus.*.

Added — An organisation to be about

The module Odoo calls base, as a core app: the organisation itself, the people in it, and how it is arranged. The platform had tenants, users and memberships carrying only what signing somebody in needs — a slug, a name, an email. A document that has to print a registration number, an approval that has to name a department, a deadline counted in some timezone: none of those had anywhere to come from, so each app either invented its own or went without.

Added — The App Store moved to appstore.gerege.mn

The catalogue now comes from a registry of its own, and the apps in it can be published by people who do not work here.

Added — Preparing the App Store to live at appstore.gerege.mn

The catalogue is on its way out of this repository and into a registry of its own (docs/APPSTORE_SEPARATION_PLAN.md). Everything here works today in file mode, which stays the default and the whole story for a self-hosted deployment; the registry is opt-in and this platform never depends on it.

Added — Switching between the organisations you belong to

Removed — The Swift macOS client (desktop-mac/)

Removed — the Swift macOS shell, in favour of one shell for all platforms

Fixed — the Tauri shell's bridge was dead on arrival

Found by running the app and signing in — none of it was visible to cargo build, clippy -D warnings, cargo test, or the three-platform CI, all of which stayed green throughout.

Added — Tauri v2 desktop shell (desktop-tauri/)

Added — CI for both desktop shells

Added — Native Shell + Web Work Area

Fixed — Security: the macOS shell's JavaScript bridge

Added — Email verification as a platform capability

Added — PDF E-Sign v2: eID Mongolia qualified remote signing

Fixed — PDF E-Sign

Added

Fixed — CI/CD pipeline

Fixed — Security

Fixed — App store & modules

Fixed — Reliability


[0.1.0] - 2026-08-05

Added


Inspirations & Acknowledgements

Authors & Contributors