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) certificate2026_e-integritate.pfx. Do not reuse the GLog audit certificate here.
1. How forwarding works
Section titled “1. How forwarding works”The forward path
Section titled “The forward path”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 throwsMNotifyClient.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.
When a notification is forwarded
Section titled “When a notification is forwarded”forward(job) returns early (does nothing) unless all of these hold:
-
gnotify.mnotify.enabled = true(master switch, defaulttrue); -
the emitting
Sistemis found byjob.systemId(); -
either the system’s per-
SistemtogglemnotifyEnabledis on or the notification carries the per-event hintjob.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.
mTLS transport
Section titled “mTLS transport”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 aHttpComponentsClientHttpRequestFactory, 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.
Payload shape
Section titled “Payload shape”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/bodyare pulled from the already-renderedjob.continut()(JSON withsubject/body/textkeys); non-JSON content is used verbatim as the body.- The recipient
typeis inferred fromjob.destinatar(): an e-mail pattern →EMAIL, a 13-digit value →IDNP, otherwiseEMAIL(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.mnotifyEnabledThe 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:
TipEvenimentNotif | Meaning |
|---|---|
MNOTIFY_SENT | forwarded (or, in mock mode, formed and recorded) |
MNOTIFY_FAILED | forward 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.payloadis@Lob. On PostgreSQL a@Lob Stringmaps to a Large Object: thepayloadcolumn physically stores the LO OID, and the text lives inpg_largeobject. Querying the column directly shows a number, not JSON (raw text islo_get(oid)). This is harmless — Hibernate/JDBC materializes the LOB back to aStringon read, so the REST API (and therefore the Status notificare UI) shows the JSON correctly. Consistent with the project’s deferred-JSONB decision (@Lobtext now, physicaljsonba documented hardening step).
5. Configuration
Section titled “5. Configuration”All keys under prefix gnotify.mnotify.* (see MNotifyProperties). Defaults shown; each is
env-overridable in the usual Spring relaxed-binding way.
| Property | Env var | Default | Meaning |
|---|---|---|---|
gnotify.mnotify.enabled | GNOTIFY_MNOTIFY_ENABLED | true | Master switch. false disables forwarding entirely. |
gnotify.mnotify.mode | GNOTIFY_MNOTIFY_MODE | mock | mock = build + record only, no network; default = real mTLS POST. |
gnotify.mnotify.url | GNOTIFY_MNOTIFY_URL | https://mnotify.staging.egov.md:8443 | MNotify base URL. |
gnotify.mnotify.endpoints.notifications | GNOTIFY_MNOTIFY_ENDPOINTS_NOTIFICATIONS | /api/Notification | POST endpoint for a notification. |
gnotify.mnotify.endpoints.templates | GNOTIFY_MNOTIFY_ENDPOINTS_TEMPLATES | /api/Template | Template endpoint (reserved; not used by forwarding). |
gnotify.mnotify.keystore.path | GNOTIFY_MNOTIFY_KEYSTORE_PATH | certs/2026_e-integritate.pfx | PKCS12 client identity (filesystem, then classpath). |
gnotify.mnotify.keystore.alias | GNOTIFY_MNOTIFY_KEYSTORE_ALIAS | test-integritate.ani.pki.gov.md | Key alias inside the PKCS12. |
gnotify.mnotify.keystore.password | GNOTIFY_MNOTIFY_KEYSTORE_PASSWORD | 123456 | Keystore password. |
gnotify.mnotify.connection-request-timeout | GNOTIFY_MNOTIFY_CONNECTION_REQUEST_TIMEOUT | 5 (seconds) | Connection-request timeout. |
gnotify.mnotify.response-timeout | GNOTIFY_MNOTIFY_RESPONSE_TIMEOUT | 8 (seconds) | Response timeout. |
The certificate
Section titled “The certificate”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:
# Government mTLS certs (MNotify etc.) — baked into the jar, never committedsrc/main/resources/certs/*.pfx6. Testing it
Section titled “6. Testing it”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/notifyContent-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.
Deployed
Section titled “Deployed”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.
Going live
Section titled “Going live”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.
Regeneration safety
Section titled “Regeneration safety”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).