MoMo architecture blueprint: a field manual recovered from the binary

Tags
Computer Science
Software Development
App Dev
AI summary
A full architecture-blueprint pass on MoMo 5.16.0: C4 views, layer and dependency rules, data and deployment architecture, cross-cutting concerns, implementation patterns, and inferred ADRs, all recovered from the shipped binary.
Published
October 12, 2026
Author
Peter of H
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:
  1. Thin shell, thick platform. MainApplication does 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.
  1. 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.
  1. 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.
notion image
notion image

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

notion image
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 carries MsgType, device fields, timestamp, and a signature slot.
  • Local persistence: Realm via KMP DI (momoplatformstorage.di) for structured data; SharedPreferences/DataStore for flags; a 100 MB CursorWindow patch suggests bulk cursor reads were hitting the 2 MB Android default.
  • Secret material: userSecret supplies signing keys; JanusTokenStore holds 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 over imei|oldPass keyed by userSecret[: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. MmsConfigFetcher degrades to cached config.
  • Logging/monitoring: Sentry + Crashlytics + self-hosted Grafana (NFC_GRAFANA_FLOW in eKYC), plus anorok/athanor/hermes-* event pipelines, plus the JS bridge console logging noted above.
  • Config management: the EnvironmentEndPoints enum is the compile-time spine; ix9.n().f737a selects at runtime; MmsConfig* and rigver-dynacfg layers add remote overrides; MaxApi feature 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 + RegisterNatives everywhere; mmwbapi blinds function pointers with masked constants so static analysis cannot resolve targets; libmms strips 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

notion image
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.