Skip to content

MNotify forwarding

This document covers the MNotify forwarding capability: gNotify sends a copy of each delivered notification to the Government of Moldova’s own notification platform, MNotify (interop.gov.md), over mutual TLS. It is best-effort, off by default in what it actually transmits (mock mode), and fully auditable from the Status notificare screen.

MNotify is the government’s central “message to a citizen/business” service (subject + body, multiple channels, recipients addressed by e-mail or IDNP). Forwarding lets a gNotify send also land in the citizen’s MNotify inbox, without the producing system integrating with MNotify itself.

The certificate is the e-integritate cert, not glog’s misda.pfx. MNotify is a different government service than GLog; it authenticates the client with the eIntegritate (ANI) certificate 2026_e-integritate.pfx. Do not reuse the GLog audit certificate here.


DeliveryService.markSent(...) is where a channel delivery flips to SENT. Right next to the GLog audit call, it fires the MNotify forward:

DeliveryService.markSent(job)
-> mnotifyClient.forward(job) // @Async, best-effort, never throws

MNotifyClient.forward(NotificationJob) is asynchronous and best-effort — it never blocks the delivery and never propagates an exception (every failure is logged and recorded, nothing bubbles up). This mirrors GlogAuditClient, except the transport is a mutual-TLS RestClient instead of a plain one.

forward(job) returns early (does nothing) unless all of these hold:

  1. gnotify.mnotify.enabled = true (master switch, default true);

  2. the emitting Sistem is found by job.systemId();

  3. either the system’s per-Sistem toggle mnotifyEnabled is on or the notification carries the per-event hint job.forwardMnotify():

    if (!Boolean.TRUE.equals(sistem.getMnotifyEnabled()) && !job.forwardMnotify()) return;

So a system opts in globally (toggle on the Sisteme page) or a producer opts in per notification (the forwardMnotify hint). Either is sufficient.

MNotifyClientConfig builds the mnotifyRestClient bean — a faithful port of eIntegritate’s MnotifyAutoConfiguration:

  • the PKCS12 keystore is loaded as the client identity (loadKeyMaterial);
  • the server’s self-signed certificate is accepted (chain.length == 1);
  • hostname verification is disabled (NoopHostnameVerifier) — the staging endpoint’s cert does not match its host;
  • transport is Apache HttpClient 5 (httpclient5) wrapped in a HttpComponentsClientHttpRequestFactory, with connection-request and response timeouts from properties.

The mTLS client is only built when mode = default. In mock (or when enabled = false) the bean is a plain RestClient that is never used to send.

The keystore is loaded from the filesystem path first, then falls back to the classpath (classpath:certs/...), so the cert works both baked into the jar and mounted beside it.

buildPayload(job) produces the MNotify MnotifyCreateNotification shape:

{
"subject": { "ro": "…" }, // multilingual maps; RO filled from content
"body": { "ro": "…" },
"bodyShort": { "ro": "…" }, // body truncated to 160 chars
"recipients": [ { "value": "…", "type": "EMAIL" | "IDNP" } ]
}
  • subject/body are pulled from the already-rendered job.continut() (JSON with subject/body/text keys); non-JSON content is used verbatim as the body.
  • The recipient type is inferred from job.destinatar(): an e-mail pattern → EMAIL, a 13-digit value → IDNP, otherwise EMAIL (best-effort; MNotify validates server-side).

2. Per-Sistem toggle — “Trimite la MNotify”

Section titled “2. Per-Sistem toggle — “Trimite la MNotify””

A new boolean column Sistem.mnotifyEnabled (Liquibase changelog 20260813100000_add_sistem_mnotify_enabled.xml, append-only) turns forwarding on for all delivered notifications of that system.

It is exposed as a toggle switch and a column on the Sisteme list page (entities/sistem/list). Flipping it issues a partial update (PATCH/partialUpdate) with just { id, mnotifyEnabled }; the switch reverts on error:

// sistem.component.ts — toggleMnotify()
this.sistemService.partialUpdate({ id: sistem.id, mnotifyEnabled: enabled });

i18n key gnotifyApp.sistem.mnotifyEnabled = “Trimite la MNotify” (RO) / “Send to MNotify” (EN).


3. Per-notification hint — forwardMnotify

Section titled “3. Per-notification hint — forwardMnotify”

For producers that want to control forwarding per event (rather than turning it on for a whole system), a forwardMnotify flag threads end-to-end:

AdminNotifyRequest.forwardMnotify (POST /api/admin/notify body)
-> NotificationDispatchService.dispatch(..., forwardMnotify) // backward-compatible overload
-> NotificationJob.forwardMnotify() // carried on the RabbitMQ job
-> MNotifyClient.forward(): forwards when job.forwardMnotify() || sistem.mnotifyEnabled

The dispatch method has two overloads: the original signature (no flag) delegates to the new one with forwardMnotify = false, so existing callers are untouched. The flag rides on the stateless NotificationJob record over RabbitMQ, so the worker forwards without re-reading any storage.

This is what lets a producer like drumuri say “forward this event to MNotify” without enabling the toggle on its whole Sistem.


4. Visibility — “what we sent to MNotify”

Section titled “4. Visibility — “what we sent to MNotify””

Every forward attempt writes an append-only EvenimentNotificare (the same audit log as CREATED/QUEUED/SENT/FAILED), with a new event type:

TipEvenimentNotifMeaning
MNOTIFY_SENTforwarded (or, in mock mode, formed and recorded)
MNOTIFY_FAILEDforward attempt failed (transport / HTTP error)

The event payload is JSON:

{
"mode": "MOCK" | "DEFAULT",
"endpoint": "https://mnotify.staging.egov.md:8443/api/Notification",
"request": { /* the MnotifyCreateNotification payload we built */ },
"response": "…", // present on success
"error": "…" // present on failure (root-cause message)
}

MNotifyEventRecorder writes this row in its own transaction on the async thread (the recorder is a separate @Service from the @Async client precisely so the write commits independently). The event is attached to the notification that owns the delivery, with actor = "mnotify".

Mock mode records too. In mode = mock, forward() builds the payload and writes an MNOTIFY_SENT event with a response note “MOCK — payload NU a fost trimis către MNotify”, but never touches the network. This is how you demo/verify the exact payload before going live.

These events show up in the Status notificare screen’s event timeline — search by trackingId and you see the MNotify request and its response/error inline with the rest of the delivery history.

EvenimentNotificare.payload is @Lob. On PostgreSQL a @Lob String maps to a Large Object: the payload column physically stores the LO OID, and the text lives in pg_largeobject. Querying the column directly shows a number, not JSON (raw text is lo_get(oid)). This is harmless — Hibernate/JDBC materializes the LOB back to a String on read, so the REST API (and therefore the Status notificare UI) shows the JSON correctly. Consistent with the project’s deferred-JSONB decision (@Lob text now, physical jsonb a documented hardening step).


All keys under prefix gnotify.mnotify.* (see MNotifyProperties). Defaults shown; each is env-overridable in the usual Spring relaxed-binding way.

PropertyEnv varDefaultMeaning
gnotify.mnotify.enabledGNOTIFY_MNOTIFY_ENABLEDtrueMaster switch. false disables forwarding entirely.
gnotify.mnotify.modeGNOTIFY_MNOTIFY_MODEmockmock = build + record only, no network; default = real mTLS POST.
gnotify.mnotify.urlGNOTIFY_MNOTIFY_URLhttps://mnotify.staging.egov.md:8443MNotify base URL.
gnotify.mnotify.endpoints.notificationsGNOTIFY_MNOTIFY_ENDPOINTS_NOTIFICATIONS/api/NotificationPOST endpoint for a notification.
gnotify.mnotify.endpoints.templatesGNOTIFY_MNOTIFY_ENDPOINTS_TEMPLATES/api/TemplateTemplate endpoint (reserved; not used by forwarding).
gnotify.mnotify.keystore.pathGNOTIFY_MNOTIFY_KEYSTORE_PATHcerts/2026_e-integritate.pfxPKCS12 client identity (filesystem, then classpath).
gnotify.mnotify.keystore.aliasGNOTIFY_MNOTIFY_KEYSTORE_ALIAStest-integritate.ani.pki.gov.mdKey alias inside the PKCS12.
gnotify.mnotify.keystore.passwordGNOTIFY_MNOTIFY_KEYSTORE_PASSWORD123456Keystore password.
gnotify.mnotify.connection-request-timeoutGNOTIFY_MNOTIFY_CONNECTION_REQUEST_TIMEOUT5 (seconds)Connection-request timeout.
gnotify.mnotify.response-timeoutGNOTIFY_MNOTIFY_RESPONSE_TIMEOUT8 (seconds)Response timeout.

certs/2026_e-integritate.pfx (alias test-integritate.ani.pki.gov.md, password 123456) is the eIntegritate (ANI) client certificate — not GLog’s misda.pfx. It is gitignored and baked into the jar:

.gitignore
# Government mTLS certs (MNotify etc.) — baked into the jar, never committed
src/main/resources/certs/*.pfx

Auth for the admin API is JHipster JWT (admin / admin).

Send a notification with per-event forwarding via the notify console endpoint:

POST /api/admin/notify
Content-Type: application/json
{
"systemId": "billing-service",
"message": "Test MNotify forward",
"channels": [ { "channel": "email", "tier": "important", "recipient": "someone@example.md" } ],
"forwardMnotify": true
}

Then open Status notificare, search by the returned trackingId, and look for the MNOTIFY_SENT event. In the deployed demo (mock mode) its payload carries the built MnotifyCreateNotification request plus the response note “MOCK — payload NU a fost trimis către MNotify” — the exact bytes that would go on the wire in default mode.

Alternatively, turn on the per-Sistem toggle on the Sisteme page and every delivered notification of that system is forwarded, no per-request flag needed.

gNotify backend-2.12.0 at https://gnotify.gstack.esempla.systems runs in mock mode — it forms and records the MNotify payload but does not transmit.

Set GNOTIFY_MNOTIFY_MODE=default. That builds the real mTLS RestClient and starts POSTing to MNotify; the MNOTIFY_SENT / MNOTIFY_FAILED events then carry MNotify’s actual HTTP response (or the error). Everything else — toggle, hint, audit trail — is unchanged.


Everything lives in separately-named, hand-written classes so jhipster jdl never clobbers it: service/mnotify/{MNotifyProperties,MNotifyClientConfig,MNotifyClient,MNotifyEventRecorder}, the forwardMnotify plumbing in service/delivery/{NotificationDispatchService,NotificationJob} and web/rest/{AdminNotifyRequest,AdminNotifyResource}, plus the toggleMnotify handler in the standalone entities/sistem/list screen. Two things do touch generated artifacts and must be re-applied after a regeneration: the MNOTIFY_SENT/MNOTIFY_FAILED values on the TipEvenimentNotif enum, and the mnotifyEnabled field on Sistem (column added by the append-only Liquibase changelog 20260813100000_add_sistem_mnotify_enabled.xml).