This is a follow-up to the previous post. Instead of a tour, this one applies a standard architecture-blueprint template to MoMo 5.16.0, using only what the shipped artifact proves. Where a section cannot be observed from a binary, I say so and mark the claim as inferred. That discipline is itself part of the exercise: a blueprint is only as good as its evidence labels.
Artifact under analysis:
com.mservice.momotransfer 5.16.0 (version code 51600), ~105 MB base APK, 84k classes, 14 mini-app bundles, 312 Hermes modules, 11 native libraries.1. Detection: what the artifact says the stack is
Everything below is observed, not assumed:
Layer | Technology | Evidence |
Host shell | Kotlin Multiplatform + Compose | vn.momo.compose.*, vn.momo.core.* packages |
Mini-apps | React Native 0.80.1, React 19.1, Hermes bytecode v96 | embedded package.json in cornerstone bundle |
Module system | Webpack Module Federation + Callstack Repack 5.1.2 | __federation_expose_index.chunk.bundle, Repack.ScriptManager |
JS-native bridge | Margelo Nitro (JSI) | libmomoapiproxy.so, nitroApiShared() |
HTTP transport | Ktor client | Ktor packages in core |
Local data | Realm KMP + SharedPreferences/DataStore | momoplatformstorage.di, realm-jni |
Native security | BShield + whitebox crypto + OpenSSL 3.6 | libmmwbapi, libmmguard, libtaibmcf |
Backend edge | api.momo.vn gateway + ~50 microservices | 234 mined routes |
Push/OTA | FCM + WorkManager + rigver-* CDN | service classes, env enum |
2. Architectural overview
MoMo is a hybrid: a host-shell micro-frontend platform wrapping a layered monolith core, with a separate native security plane that sits beside the application plane rather than inside it.
Three design principles are consistent everywhere:
- Thin shell, thick platform.
MainApplicationdoes almost nothing itself;MMSInitializer(androidx.startup) boots the security SDK first, then a KMP core configures transports and workers. Product code lives in remote bundles.
- Every seam is a real seam. The mini-app boundary, the JS-to-native bridge, the env selector, and the signing gateway are all genuine interfaces with multiple adapters (environments, transports, modules), not speculative abstractions.
- Trust is enforced below the language layer. Signing, pinning, root and emulator detection live in native code, invisible to anyone who only patches JS or smali.
3. Visualization
System context, container, and dependency views below are drawn from observed code paths, not from how such systems usually look.


4. Core architectural components
Host shell. Owns process lifecycle: configures per-process WebView dirs, applies the
CursorWindow 100 MB reflection patch, queues the 12-hour OTA worker and device-name worker. Deliberately shallow; its job is composition, not behavior.Mini-app runtime. The deepest module in the app. Small interface (
registerApp, ScriptManager, federation resolver) hiding enormous machinery: chunk loading, checksum verification, shared-library deduplication, OTA delta patching via libbspatch. One interface, two adapters (host bundle vs remote zips), and the remote adapter has a failover matrix of its own.JS bridge.
callPlatformApiSync/Async serializes request objects to JSON and hands them to nitroApiShared(). Two facts worth knowing: it logs every request and response to console ([MAX] prefixes) in production, and its dispatch is by method-ID string (api/P2P_FUND_TRANSFER_INIT), not URL path, so the JS layer is decoupled from REST routing.Core services.
vn.momo.core.*: PlatformApiManager dispatch, authentication{,v2} (PIN, SmartOTP, passkeys, biometric), environment config, AES helpers, Ktor transport, Realm storage. Layered internally: feature use-cases to repository interfaces to transport DTOs. The obfuscated-name map (SecurityCenterRepository to repository.b) confirms the layering survived R8.Security SDK.
com.momosecurity, 43 methods on MMSecuritySDKMain, remote-config driven. This is the anti-corruption layer for trust: every trust decision (is this device real, is this app tampered, may this endpoint be signed) funnels through one gateway class before becoming a header.OTA pipeline.
rigver-appversion (manifest), rigver-rollout (staged %), rigver-dynacfg-app (config), rigver-miniapp-load (zips). Triggered by FCM miniapp_version_check and a periodic WorkManager job. It is deployment architecture shipped inside the app.5. Layers and dependency rules

Observed dependency rules: features depend on the bridge interface, never on Ktor directly; domain use-cases depend on repository interfaces (constructor-injected, ~18 collaborators on
SecurityCenterRepository alone); transport reaches the security plane only at the signing gateway. One violation pattern exists by design: mini-app JS can request almost any route string, so endpoint authorization is enforced server-side and by the WlTrieLib signing whitelist, not by the bridge.6. Data architecture
- Domain DTOs: obfuscated but structured builders (
wb1,zb1,fc1,nb1) assemble request envelopes; every wire call carriesMsgType, device fields, timestamp, and a signature slot.
- Local persistence: Realm via KMP DI (
momoplatformstorage.di) for structured data; SharedPreferences/DataStore for flags; a 100 MBCursorWindowpatch suggests bulk cursor reads were hitting the 2 MB Android default.
- Secret material:
userSecretsupplies signing keys;JanusTokenStoreholds session tokens (encryption-at-rest internals still unverified, flagged as a gap). GeoComply and push tokens are server-issued, nothing sensitive embeds cleanly.
- Caching: config caches (
MmsConfigCache), bundle manifests with per-chunk checksums, failover endpoint lists as ordered two-element arrays.
- Validation: request envelope validation at builders; endpoint allowlisting at the signing gate; mini-app identity at
gateway/mini/v1/iam.
7. Cross-cutting concerns
- AuthN/Z: layered credentials (PIN, complex-PIN, biometric
bioId/bioIdComplex, SmartOTP, FIDO2 passkeys, Play Integrity). PIN change sends device IMEI plus an AES-CBC signature overimei|oldPasskeyed byuserSecret[:32]with an all-zero IV; deterministic by construction.
- Error handling: signing fails open (exception returns empty headers). Transport failover is data-driven via paired endpoint lists.
MmsConfigFetcherdegrades to cached config.
- Logging/monitoring: Sentry + Crashlytics + self-hosted Grafana (
NFC_GRAFANA_FLOWin eKYC), plusanorok/athanor/hermes-*event pipelines, plus the JS bridge console logging noted above.
- Config management: the
EnvironmentEndPointsenum is the compile-time spine;ix9.n().f737aselects at runtime;MmsConfig*andrigver-dynacfglayers add remote overrides;MaxApifeature flags gate mini-app features.
8. Service communication
Ktor for HTTPS JSON to
api.momo.vn with WBSIGN-family headers and mmguard pinning; gRPC to helios.mservice.io for realtime; MQTT (mqtt.momo.vn, mqttnoti:8084) for device/notification channels; WebSocket per-environment. Routes are explicitly versioned (ekyc/v3, hydra-user-setting/v2, telco/v2|v3, athena .../v2), and product surfaces evolve by adding sibling paths rather than breaking old ones. Resilience = failover pairs + signed-request retry semantics.9. Stack-specific patterns
- KMP/Compose host: androidx.startup initializer, DI by constructor injection, coroutine continuations (
*invokeSuspend) as the async unit, interface-impl split surviving obfuscation (dn3/an3).
- RN/Hermes fleet: Repack ScriptManager resolves remote chunks; Module Federation enhanced 0.7.1 shares libraries; per-screen bundle granularity keeps update payloads small.
- Native:
JNI_OnLoad+RegisterNativeseverywhere;mmwbapiblinds function pointers with masked constants so static analysis cannot resolve targets;libmmsstrips to single-letter exports.
10. Implementation patterns
Repository to use-case to service chains are the norm (the full
SecurityCenter PIN-change chain resolves across five classes and three packages). Builders assemble envelopes; continuations carry suspend bodies; lazy singletons (GeoComplyClient.getInstance) gate SDKs. The pattern is consistent enough that once you decode one feature's chain, you can predict the next one's shape.11. Testing architecture
Not observable: no test code ships in a release artifact. Inferred from build metadata: internal npm registry + GitLab deps imply standard CI; the
vn.momo.platform.test bundleId gate shows they maintain a test build flavor with telemetry disabled. Honest answer: unknown, flagged rather than invented.12. Deployment architecture

Environment matrix (DEV, UAT, STAGING, PROD) with STAGING running on production hosts under
staging- infixes; the binary ships answering PROD. This is staged rollout infrastructure where the client itself enforces checksums and ordering.13. Extension patterns
Adding a product feature means shipping a new remote: publish a zip to the mini-app CDN, register the app id in
rigver manifests, expose routes via gateway/mini IAM, consume shared libraries from the host federation. No app-store release needed; rollout percentage is controlled server-side. The seam is real: 14 remotes prove it. Third-party mini-apps get MoMo identity via oauth/login, so the platform extends outward too.14. Pattern examples (decompiled)
Signing gateway (fail-open by design):
try { Map signed = CoreBridgeKt.pzw(metadata, body, url, method); return withWbHeaders(signed); } catch (Exception e) { return emptyMap(); // request proceeds unsigned }
JS bridge dispatch (method-ID, not URL):
nitroApiShared().callApiSync(JSON.stringify({ methodId: "api/P2P_FUND_TRANSFER_INIT", params: {...} }))
15. Inferred decision records
- Federation over monolith (context: 14 product teams, weekly ship cadence; consequence: 312 bundles, duplicated shared libs, registry complexity; trade accepted for release independence)
- Native whitebox signing (context: JS/smali is attacker-readable; consequence: keys never materialize in managed code; cost: blinded pointers, no static audit path)
- Fail-open signing (context: payment availability; consequence: unsigned requests still attempt; risk accepted rather than blocking revenue paths)
- Deterministic AES signature over
imei|oldPass(context: server-verifiable without extra handshake; consequence: ciphertext equality leaks old-PIN equality; noted as a finding, not a design endorsement)
16. Governance
Enforced in-band:
WlTrieLib endpoint whitelist decides what may be signed; bundle manifests carry checksums; rollout percentages throttle exposure; bundleId != test gates production telemetry; route versioning isolates breaking change. Architecture is governed by config and whitelists, not by code review alone.17. Blueprint for new development
To add a feature the MoMo way: new remote mini-app (Hermes chunks + manifest), register in
rigver pipeline, call backend via bridge method-IDs through gateway/mini IAM, reuse host shared libs via federation, gate with MaxApi flags. Pitfalls to avoid: bypassing the signing gateway (requests will fail whitelist), shipping secrets in bundles (the GitLab token leak proves they extract), and assuming the bridge validates routes (it does not).Blueprint generated 2026-10-12 from static analysis of MoMo 5.16.0 (build 51600). Valid against this artifact only; mini-app remotes update continuously, so re-derive the remote-layer sections per release.