Proposal · BankCBS mobile platform · 12 October 2026

Multi-Bank Platform Proposal

How we turn today's single-bank app into one codebase that serves many banks safely. For each area this page shows what is wrong today and the flow we propose, and it ends with exactly how an update reaches one bank.

Status Draft for review Audience mobile, backend, QA, product, bank partners Detail docs/MULTI_BANK_ARCHITECTURE.md, docs/BANK_FLOW_VARIANTS.md

00Summary

Today the app is wired to one bank. Four problems block any release to customers: mock code can show a fake transfer success, release builds are signed with the debug key, the server switch that turns features off is not connected, and the PIN encryption key travels over plain HTTP.

The proposal keeps one codebase and adds:

  • a tenant pack per bank, chosen when the app is built;
  • one backend codebase, deployed separately for each bank, so each bank's data, keys and core-banking link stay isolated;
  • a five-layer feature model, so the bank and the server decide what each customer sees;
  • per-bank flows through settings and server-driven security steps, never code branches;
  • five update lanes, from a live switch in minutes to a store release in weeks, with UAT and production for every bank;
  • management through an admin portal with two-person approval, an audit log and per-bank monitoring.

Cost: about six weeks for one mobile developer, plus backend work in parallel. The first week fixes the release blockers.

Decided (12 October 2026): one backend codebase, deployed separately for each bank, the same way the app is built separately for each bank.

Still needed from you: nine decisions in section 13. The three that matter most: where each bank's backend runs, each bank's own store accounts, and the server-driven security protocol.

AreaTodayProposed
BanksOne, hard-coded in about ten placesAny number; one tenant pack and one registry entry each
BackendOne server, its address edited by hand in source; ten hosts listed in the Android network configOne backend codebase, one isolated deployment per bank and environment
Feature controlOnly what is compiled in; the server switch is not connectedLive switches per bank, rules per customer, cached for offline
Different flows per bankWould need if (bank) codeSettings, server-driven security steps, journey variants
Builds and releasesManual, debug-signed, no checksCI checks on every change, signed builds per bank and environment
Bug fixesStore release onlyOTA per bank in hours, with UAT sign-off and rollback
Safety netNo error boundary, no crash data, no forced updateError boundaries, crash reporting per bank, auto-rollback, forced update
OperationsNoneAdmin portal, two-person approval, audit log, per-bank dashboards
Blocker must be fixed before customers get a build, or before a second bank High fix in the first phases Medium fix as we go

01Target architecture

  • One codebase. Bank differences live in data (tenant packs and server config), never in if (bank) branches.
  • The server decides access and security. The app follows the server and never widens what it allows.
  • Fail closed. An unknown feature key, step or config means off or stop.
  • Every bank is isolated: its own app build, backend deployment, data, keys, OTA channels, store listing and sign-off.
  • Every change has a lane. Use the fastest lane the change allows.
  • No change reaches a bank's customers without that bank's UAT sign-off.
One repository Shared core screens, flows, engines Tenant packs one folder per bank merge Release script / CI checks, then builds every bank in a loop native change JS change Store build per bank in the bank's own store account OTA update per bank channel per bank: uat, prod installs updates JS Customer phones Bank A app Bank B app Bank C app one listing per bank HTTPS Per-bank backends Bank A backend own data, own CBS Bank B backend own data, own CBS Bank C backend own data, own CBS each: config, flows, APIs, admin, audit Platform control plane (ours) ops console · catalog · monitoring · no customer data manages metrics
Exists once per bankShared by every bank
Everything blue exists once per bank: the app build, its OTA channel and its backend deployment. Each app talks only to its own bank's backend. The control plane holds no customer data; it manages the backends over an audited link and collects their metrics.

Inside the app

App shell providers · error boundary · bootstrap · tenant guard Navigation a screen exists only if its feature is on Modules home · payments · services · loans · cards · agent Flows challenge runner · journey runner Feature engine Bank settings Strategies & slots Tenant pack (@tenant) this bank's identity and defaults Services API client · security · storage tenants/bank-a build Flow engine /features/config runtime, from the server
New in this designDiffers per bank
Screens never read the tenant pack or the server directly. They ask the decision layer ("is it on?", "what value?", "which rule?"), so a screen is the same for every bank.

Backend: one codebase, one deployment per bank

The backend follows the same rule as the app: one codebase and one versioned build, deployed separately for each bank and each environment. Banks differ only in configuration: their core-banking connection, database, keys, certificates and feature settings.

ComponentRunsJobStatus
Mobile and business APIsIn each bank's backendExisting CBS APIs, plus an entitlement check on every call (FEATURE_NOT_AVAILABLE)extend
Config serviceIn each bank's backendThe bank's switches, settings and journey variants; per-customer entitlements after sign-in; served at /features/configextend the app calls it; the server side is not built yet
Flow engineIn each bank's backendThe bank's security policy; replies one step at a time (TPIN, OTP, biometric)new
Bank admin portalIn each bank's backendBank staff change their own bank, with two-person approvalnew
Audit logIn each bank's backendEvery change and every ops action, kept with the bank's own datanew
Database, key pair, core-banking linkIn each bank's backendThe bank's customers and sessions, its MPIN/TPIN key pair, its connection to its core banking systemper bank
Ops consoleCentralOnboarding, security policies, emergency kill, support lookup; reaches each bank's backend over a private, audited linknew
Feature catalogCentralEvery feature key the app knows; each backend imports it when it is deployednew
Hot Updater serverCentralOTA bundles and channels, self-hostednew
MonitoringCentralCrash reports, metrics and logs without customer data, tagged by bank, bundle and versionnew

Environments

EnvironmentUsed byBackendBuildOTA channelMock data
DevDevelopersShared dev backend, or the local mock adapterbankADevDebugnone (Metro)Allowed, debug builds only
UATOur QA, the bank's testersBank A's UAT backendbankAUatReleasebank-a-uatNever
ProdCustomersBank A's production backendbankAProdReleasebank-a-prodNever
No more hand-edited hosts. Each bank's backend address and certificate pins, per environment, live in that bank's tenant pack. The build picks them from the bank and the environment, so nobody changes a host in source code again.

02Tenant flow

Today: what is wrong

SeverityFlawWhereEffect
BlockerThe bank is hard-coded in about ten places: org id, app name, support email, app ID, display names, colours, logo and seven strings of copyconfig/env.ts:34, config/appConfig.ts:4, android/app/build.gradle:82, theme/palette.ts:193, BrandLogo:6A second bank means editing shared code
HighThe backend address is edited by hand in source; the Android network config lists ten hostsconfig/env.ts:26, network_security_config.xmlWith a backend per bank, a build can easily talk to the wrong bank's server
HighNothing checks that the app reached its own bank's backend; the config request does not even name the bankfeatures/featureRemoteConfig.ts:113A wrong address would show another bank's switches without any error
HighNo per-bank defaults; the build-time layer is emptyfeatures/featureConfig.ts:311A fresh install offline shows global defaults, maybe features this bank lacks
MediumNo list of banksnoneNothing ties an app ID, an org, a store listing and an OTA channel together

Proposed flow

A tenant is one bank. It exists in four places, tied together by one id such as bank-b.

Code

tenants/bank-b/

  • Identity: org id, name, logo, brand colours, wording
  • Feature defaults for the first launch and offline
  • Settings, flow choices, rare strategies or slot overrides

Native shell

Android flavor bankB · iOS scheme BankB

  • App ID, launcher icon, home-screen name
  • Upload signing key, kept in CI secrets
  • OTA channel baked in: bank-b-prod

Backend

Bank B's own deployment · UAT and prod

  • Its own database, key pair and core-banking connection
  • Live switches, settings, security policy, customer rules
  • Admin portal, approvals, audit log

Delivery

tenants/registry.json · stores · Hot Updater

  • Store listing in Bank B's own developer account
  • OTA channels bank-b-uat and bank-b-prod
  • One registry entry: flavor, app IDs, version
BUILD TIME ON THE PHONE release.js TENANT=bank-b APP_ENV=prod Metro @tenant → bank-b only Bank B's pack Gradle flavor bankBProdRelease app ID, icon, channel Bank B's phone bundle: bank-b binary: bank-b-prod Tenant guard match → app starts mismatch → refuse
The bank is fixed at build time, so Bank B's app never contains Bank A's logo, config or server address. If a wrong OTA bundle ever lands on a phone, the guard refuses it before the first screen and Hot Updater rolls back. The app also checks the org id in the first config reply, so a pack pointing at the wrong bank's backend stops at once.

The pack is a typed file. The build fails if a field is missing.

// tenants/bank-b/index.ts
export default {
  id: 'bank-b',
  orgId: '…',                       // from onboarding
  displayName: 'Bank B Mobile',
  api: {                            // Bank B's own backend, per environment
    uat: 'https://uat-api.bank-b.example',
    prod: 'https://api.bank-b.example',
  },
  pins: { uat: ['sha256/…'], prod: ['sha256/…'] },   // TLS certificate pins
  features: { loans: false, cards: true },
  settings: { payments: { rtgsMinimum: 200000 } },
  flows: { transferAuthorisation: 'server-driven' },
  brand,
  logo: require('./assets/logo.png'),
} satisfies TenantConfig;

Tenant lifecycle

1 · Agree scope contract, features bank + us 2 · Set up pack, flavor, org us + DevOps 3 · UAT bank tests UAT bank 4 · Go live store + prod OTA bank + us 5 · Operate monitor, toggle bank + us 6 · Change features, flows bank + us every change goes back through UAT
Offboarding runs the lifecycle in reverse (see the last list in this section).

Onboarding a bank

  1. Copy tenants/_template to tenants/bank-c; fill in identity, brand colours, logo, features, settings, backend addresses and certificate pins.
  2. Run the contrast check with Bank C's colours and adjust until every text passes.
  3. Add Bank C to tenants/registry.json; its Android flavors and CI jobs appear automatically.
  4. Add launcher icons and the iOS configuration for Bank C.
  5. DevOps stores Bank C's signing key in CI secrets.
  6. DevOps creates Bank C's UAT and production backends from the current backend release with the infrastructure scripts: database, key pair, TLS certificate, core-banking connection.
  7. Ops configures both backends: feature switches, customer rules, security policy, admin users.
  8. The bank creates its store listing and gives our team access.
  9. Build to bank-c-uat; the bank tests and signs off; release to production.

Offboarding: switch the bank's features off live, unpublish its store listing with the bank, retire its two OTA channels, hand over or archive its data as the contract requires, shut down its backends, and remove the pack from the registry.

03Feature flow

Today: what is wrong

SeverityFlawWhereEffect
BlockerThe kill switch is not connected. Startup passes useMock: true, so the server is never asked, and an empty config is cachedfeatureRemoteConfig.ts:150, hydrateFeatures.ts:28, env.ts:40No feature can be switched off remotely
HighThe customer's flags arrive after the app is already on screenuseAppBootstrap.ts:76, RootNavigator.tsx:89A tab the customer must not have can flash
HighConfig is refreshed only at launch and sign-inhydrateFeatures.ts:28A kill reaches active users late
HighThe device layer can switch features on in release buildsadminFeatures.ts:38A staff phone can show features the bank does not have
HighThe cached config has no version checkfeatureRemoteConfig.ts:165An old or empty reply silently undoes a kill
MediumNew keys reach every bank; module children are switched off one by onefeatureConfig.ts:120, :205An OTA can leak a feature; turning on loans does not turn on the module
MediumDifferent flows per bank would need code branches: five hand-written step machines, OTP length copied in four placesPaymentConfirmationScreen.tsx:40, TpinCreateFlow.tsx:30Every bank variation becomes if (bank)

Proposed flow: who gets which feature

Later layers override earlier ones Product default set by developers compiled in Bank pack set by mobile team compiled per bank Bank live set by bank admin fetched, cached Session set by server rules role + this customer Local device set by QA or staff release: off only A false here is final. No layer to its right can switch it back on.
Differs per bankIts "off" is final (kill switch)
Two more rules: a feature is on only if every parent is on (loans off hides all of loans.*), and an unknown key is always off.
FeatureBank A · customer X (KYC pending)Bank A · customer YBank B · anyone
loanson bank packon bank packoff bank pack
loans.applyoff session ruleonoff parent is off
cards (Bank A outage)off bank liveoff bank liveoff default
Staff phone turns cards onoff still killedoff still killedoff release ignores it

Proposed flow: a feature's life

PLAN CODE · EVERY BANK SERVER · ONE BANK AT A TIME CODE 1 · Decide rung + key name product, bank 2 · Build key ships off developers 3 · Ship dark OTA, hidden release mgr 4 · Test on in bank UAT ops, bank QA 5 · Go live on in bank prod bank admin 6 · Narrow customer rules bank decides 7 · Retire remove leaf flag developers
Done on the server, per bank, with no app release
Steps 4 to 6 repeat for each bank that wants the feature, on that bank's schedule. Module roots (loans, cards) and money actions keep their switch for ever as kill switches; leaf flags are removed once fully on.
  • The root key decides a module. Children default to on and are switched off only to remove one capability. Today's loans.* and cards.* children get a one-time cleanup.
  • New keys ship off. A CI check fails any new key that defaults to on without a written reason.
  • The backend knows every key. CI exports a feature catalog on each merge; the backend imports it, so the admin portal lists the same keys the app understands.
  • The server checks again. Every endpoint maps to its feature key and refuses with FEATURE_NOT_AVAILABLE.
  • Backend first, then switch on. Each bank upgrades its backend on its own schedule. A feature that needs a newer backend stays off for a bank until that bank's backend is upgraded; each backend reports what it supports, and its config service never switches on a feature it cannot serve.

Proposed flow: when a bank's flow differs

Some banks want the same feature to work differently. Use the lowest rung that fits; each step up adds power and upkeep.

Higher rung: more power, more upkeep. Use the lowest rung that fits. 1 · Setting a value differs RTGS minimum changes live 2 · Server challenge security steps differ TPIN → OTP changes live 3 · Journey variant journey steps differ loan form steps switch live, add by OTA 4 · Strategy a rule differs account number format needs OTA 5 · Slot override a look differs Home header needs OTA
Can change without an app updateNeeds an OTA update
Most bank requests stop at rung 1 or 2. Rungs 4 and 5 are built only when a real bank needs them.
Each arrow is one server reply that names the next check. Bank A policy: TPIN, then OTP Confirm TPIN OTP Done Bank B · ₹3,000 OTP only up to ₹5,000 Confirm OTP Done Bank C biometric, then TPIN Confirm Biometric TPIN Done Same app code for all three banks. Only the server policy differs. Bank B can move ₹5,000 to ₹10,000 with no app release.
A check the server asked for
Security steps come from the server, so a modified app cannot skip them. Today's IMPS API already takes the TPIN in initiate and the OTP in verify-otp; an adapter maps it onto the same runner, so the app can move before the backend changes.
Not allowed: if (tenant.id === 'bank-b') or switch (tenant.orgId) in shared code, per-bank Git branches, or copied screens. An ESLint rule fails the build on the first two.

04How the UI renders

Today: what is wrong

SeverityFlawWhereEffect
BlockerMock code renders in release builds. Own-account transfer is on by default and accepts OTP 123456, then shows a made-up success. Dashboard, cards, loans and deposits show fixture dataenv.ts:40, serviceHelpers.ts:55, PaymentConfirmationScreen.tsx:453, paymentsApi.ts:190, featureConfig.ts:107A customer is told money moved when it did not
HighNo error boundary anywherenoneOne render error blanks the whole app; OTA auto-rollback misses crashes after the first screen
HighThe splash waits for the config request, up to the 15-second client timeoutRootNavigator.tsx:52, apiClient.ts:14Slow start on a weak network
HighWhen a feature turns off, its screen is removed, even in the middle of a paymentFeatureRoute.tsx:39, featureRoutes.tsThe transfer result is lost; a retry can create a duplicate transfer
MediumEvery config refresh re-renders every gated component, even when nothing changedfeatureService.ts:223Wasted renders; worse with refresh on resume
MediumSix screens of 640–724 lines mix UI, steps, API calls and error handling; no component testsPaymentConfirmationScreen, RegisterScreen, TpinResetFlow and three moreHard to change per bank; breaks silently
MediumThe global font is set through Text.defaultProps, which React 19 ignores for function componentsindex.js:36Text without its own font likely falls back to the system font
MediumAll on-screen warnings hidden in development; Redux serializable check offindex.js:19, store.ts:11Rendering bugs go unnoticed
MediumThree data-access patterns plus fixtures re-seeded at every sign-inARCHITECTURE.md §4Two screens can show different values for the same thing

Proposed flow: from launch to a gated screen

SPLASH SIGN-IN SCREEN SPLASH APP App start default + pack Cache last bank config Fetch config max 4 s, orgId Sign-in gated by flags Session flags role + customer App tabs allowed routes Each screen guarded actions
New behaviour
The app never shows anything before it knows what is allowed. The cache makes the first step instant and keeps kill switches working offline; the two waits are short and bounded.
FixHowPhase
No mock code in releaseBuild environments decide mocks; resolveWithMock works only in dev; features with no backend are off in UAT and prod; the OTP-123456 path is deleted0
Error boundariesOne at the root and one per tab: "Something went wrong, back to Home", reported with bank tags0
Fast, bounded startCache first; config fetch capped at 4 seconds0
No flash after sign-inHold the app until session flags land, or receive them in the login response1
Never pull a screen mid-paymentA running money flow holds a lease; its route is removed only after the flow ends1
Re-render only on real changeCompare the new config with the old before bumping the version1
Consistent fontA shared AppText component instead of Text.defaultProps2
Smaller screensThe flow framework takes steps, PIN, OTP and error handling out of screens; component tests per flow5
See warnings againRemove LogBox.ignoreAllLogs; turn the serializable check back on0
One source of truthMove slices to RTK Query as each module gets its real APIongoing

05Development flow

Today: what is wrong

SeverityFlawWhereEffect
HighNo CI pipeline; lint, type check, tests and the feature audit run only by hand.github/ has no workflowsBroken code and unwired flags reach main
HighNo build environments: one env.ts holds the host and the mock switchesconfig/env.tsWhatever is committed ships, mocks included
MediumUnit tests only; no component or navigation testsARCHITECTURE.md §11Screens and navigation break silently
MediumWarnings hidden in developmentindex.js:19Bugs found late, by customers

Proposed flow

Feature branch feat/… Pull request + code review CI checks on every PR • lint + no-bank-branching rule • type check • unit + component tests • feature audit, strict • new keys ship off • tenant matrix + contrast • bundle builds for every bank • no mock code in release builds • export feature catalog main releasable Weekly JS train OTA to each bank's channels Monthly native train store build in each bank's account cut release/x.y hotfixes
Reaches phones without a store release
Nothing merges unless every check passes. Fixes for an older native version are cherry-picked onto its release/x.y branch and ship on the same OTA train.
Branch or tagPurposeLifetime
mainAlways releasable; the weekly JS train is cut from herePermanent
feat/…One change, merged by pull requestDays
release/1.6Created at each store release; hotfixes for that native versionUntil 1.6 leaves support
v1.6.0Tag on every store releasePermanent
ota-2026-10-15Tag on every JS trainPermanent

Working locally, one bank at a time:

npm run start:bank-a                    # Metro for Bank A, dev environment
npm run android:bank-a                  # installs bankADevDebug on a device
npm run start:bank-b -- --reset-cache   # switch to Bank B

Rules for developers

  • Never branch on the bank. Use a setting, the server policy, a journey variant, a strategy or a slot.
  • Every new screen, tab, button and money action has a feature key, and the key ships off.
  • Values a bank might change are settings, not constants.
  • Security steps go through the flow framework, never a hand-written step machine.
  • Mock data lives behind the dev environment only.
  • Bank-specific code lives in that bank's pack, with a note in the pack's README saying why.

A feature is done when: its key ships off; route, UI and handler are gated; the server endpoint maps to the key; tests cover the flow; it passes every CI check; the catalog is exported; and the bank-facing notes are written.

06Deployment flow

Today: what is wrong

SeverityFlawWhereEffect
BlockerRelease builds are signed with the debug keyandroid/app/build.gradle:103Cannot be published to the Play Store
BlockerThe PIN encryption key is fetched over plain HTTP before every encryptionsecurity/mpinCrypto.ts:53, config/env.ts:26Someone on the same network can swap the key and read customers' MPIN and TPIN
HighPlain HTTP to a private LAN addressconfig/env.ts:26Works only on the office network or VPN; all traffic can be read and changed
HighOne app identity, no flavors or schemesbuild.gradle:82Cannot build a second bank's app
HighVersion 1.0, version code 1, no schemebuild.gradle:85-86Store updates and OTA targeting need x.y.z
MediumNo release automation or store uploadnoneManual, error-prone, not repeatable per bank

Proposed flow

Build variantEnvironmentSigned withInstalled fromOTA channel
bankADevDebugDevDebug keyDeveloper machinenone
bankAUatReleaseUATBank A's upload keyInternal track, TestFlightbank-a-uat
bankAProdReleaseProdBank A's upload keyPlay Store, App Storebank-a-prod
1 · Cut release release/1.6 versions bumped 2 · UAT builds bankXUatRelease CI, each bank 3 · Bank UAT internal track, TestFlight 4 · Submit prod build to the bank's own account 5 · Roll out 10% → 100% watch crash-free 6 · Min version raise it later; old apps update bug found: fix on release branch, rebuild
Runs once per bank, on that bank's sign-off
Each bank moves through steps 2 to 5 on its own schedule. CI uploads to each bank's store account with credentials the bank grants (Play Developer API, App Store Connect API key).

Versions

  • Each bank's version lives in tenants/registry.json as MAJOR.MINOR.PATCH. A native release bumps MINOR; a native hotfix bumps PATCH.
  • Version code is computed: MAJOR × 10000 + MINOR × 100 + PATCH, so 1.6.2 becomes 10602.
  • OTA bundles target exact native versions (-t 1.6.2), never wide ranges.
  • We support the last two minor versions. Below that, the server's minimum version sends the customer to the store.

Signing and secrets

  • One upload key per bank, stored only in CI secrets and backed up offline.
  • Google Play App Signing per bank app; iOS certificates in each bank's Apple account.
  • Hot Updater credentials per environment; only CI may deploy to production channels.

Working with backend releases

  • A bank's backend is upgraded before any app feature that needs it is switched on for that bank.
  • API changes are additive. A field is removed only after every bank's backend and every supported app version have moved past it.
  • The config response carries a schema version; the app ignores keys it does not know.

Backend deployment, one bank at a time

Backend PR one codebase CI + tests migration checks One image backend:2.4.0 Bank A · UAT bank tests here Bank B · UAT bank tests here Bank C · UAT bank tests here sign-off sign-off sign-off Bank A · production rolling deploy Bank B · production rolling deploy Bank C · production rolling deploy
A separate deployment for each bank
Every bank runs the same image; only configuration differs. Each bank moves to production on its own sign-off, and at most two backend versions run in production at once.
  • One image everywhere. Banks differ only in configuration and secrets: core-banking endpoint, database, key pair, certificates and org id.
  • Infrastructure as code. One script creates a new bank's UAT and production backends. The target is one day per bank.
  • Expand, then contract. Database and API changes are added first and removed later, so the previous backend version and older apps keep working during a rollout.
  • Each backend reports itself. GET /meta returns its version and capabilities, and the release registry records which version each bank runs.
  • Each backend serves one bank. It accepts only its own org id and refuses anything else.
  • Hosting is still open (section 13): our cloud with one isolated account per bank, or the bank's data centre, where its operators deploy our signed image. The image and the steps are the same.

07OTA flow

Today: what is wrong

SeverityFlawWhereEffect
BlockerNo OTA at allpackage.jsonEvery JS fix waits for a store release and review in each bank's account
HighNo safety net: no error boundary, no crash reportingnoneA staged rollout cannot see failures
HighNo forced updatenonePhones on a broken native version stay broken
MediumVersion 1.0 cannot be targeted preciselybuild.gradle:86Bundles could reach binaries they were not built for

Proposed flow

Every bank has two channels: bank-x-uat, installed by its testers, and bank-x-prod, installed by its customers. The channel is baked into the binary, so a bundle for Bank B can never reach Bank A.

Pull request one change CI checks lint · types · tests Build per bank TENANT=bank-x UAT channels bank-x-uat Bank sign-off per bank Production 10% → 50% → 100% problem Disable the bundle phones revert to the last bundle fix forward with a new pull request
Runs once per bankRollback
If a bundle crashes before the first screen, Hot Updater rolls that phone back by itself. For later crashes, the error boundary reports them and the release manager disables the bundle.

The weekly JS train

DayStepWho
MondayCut from main; CI builds a bundle per bank and per supported native version; deploys to every -uat channel at 100%CI, release manager
Tuesday–WednesdayEach bank tests on its UAT appBank QA, our QA
WednesdaySign-off per bank, recorded in the release ticketBank
ThursdayProduction 10%, only for banks that signed offRelease manager
Friday50%, if crash-free sessions stay at or above 99.5% and no error rate jumpsRelease manager
Next Monday100%Release manager
  • Hotfix: the same path, compressed into one day: UAT smoke test within two hours, then 10% and 100% the same day.
  • Rollback levels: automatic (crash before the first screen); disable one channel's bundle; disable a release on every channel with one script; then fix forward.
  • A bank that has not signed off stays on its previous bundle. It may lag at most two trains before the release manager escalates.

Keeping versions compatible

Store (native) OTA (JS) Forced update v1.4.0 v1.5.0 -t 1.4.0 fix 1 for 1.4.0 fix 2 for 1.4.0 -t 1.5.0 fix 1 for 1.5.0 Min version → 1.5.0: phones on 1.4.x must update from the store time
An OTA bundle reaches only the native version it was built for, so JavaScript never runs on a binary that lacks its native code.
Before the first OTA: error boundaries, crash reporting tagged by bank and bundle, real signing, x.y.z versions, signed bundles and the tenant guard. Without them, a staged rollout is blind.

08Management flow

Today: what is wrong

SeverityFlawWhereEffect
HighNo backend behind the feature config yetcomment above MOCK_CONFIG in featureRemoteConfig.tsNobody can change a bank's features
HighNo admin portal, approvals or audit lognoneChanges untracked; no two-person control, which banks expect
HighNo monitoring per bankno crash or analytics libraryCannot tell which bank or bundle is failing
MediumRoles are not definednoneUnclear who may switch, test or sign off

Proposed flow: changing a bank's switch

Maker Bank B staff Checker Bank B staff Bank B backend config service Audit log in Bank B's backend Customer app Bank B phone propose: loans off ask to approve approve who, what, when, v44 GET config on resume v44 → Loans tab hidden
Bank B's sideReply
A change takes effect within minutes and can be undone the same way. Our ops team can apply an emergency kill to any bank without a checker; that action is audited and the bank is told.

Who owns what

TaskPlatform team (us)BankDevOps
Feature scopeadvisesdecides—
Tenant pack, brandingbuildsapproves—
Store listing and developer accounthelpsowns—
Signing keys, CI secrets—grants account accessowns
The bank's backend environmentbuilds and deploys to itowns it if it runs in the bank's data centreruns it if it runs in our cloud
Backend upgradesbuilds, testssigns offdeploys
Live feature switchesemergency kill onlyowns—
Customer rules (KYC, account type, new device)configuresdecides—
Security policyconfigures, reviewsdecides, signs off—
UAT testingsupportsowns—
OTA releasesownssigns offruns the pipeline
Store releasesbuildsapproves in its accountuploads via CI
Monitoringowns app healthsees its own dashboardowns infrastructure

Who can change what

ToolUsed byCan changeCannot change
Bank admin portalBank staff, two-person approval; runs inside the bank's own backendIts own bank's switches, settings, journey variants, customer blocksOther banks (separate deployments), security policy, app code
Platform ops consoleOur operations team, over a private, audited link to each bank's backendOnboarding, security policies, emergency kill for any bank, support lookupApp code
Git and CIOur developersShared code, tenant packs, flow adaptersLive switches
Hot Updater consoleRelease managersRollout percentage, disable or roll back a bundleNative code
Play Console, App Store ConnectBank, with our helpStore listing, staged rollout, halt—

Watching each bank

Every crash report, API log and event carries five tags: tenant, appVersion, bundleId, configVersion and channel. Each bank's backend sends its metrics and logs, without customer data, to central monitoring, tagged with its bank and backend version. Thresholds below are starting points.

SignalAlert whenAction
Crash-free sessions, per bank and bundleBelow 99.5% after a rollout stepPause the rollout; disable the bundle if it keeps falling
FEATURE_NOT_AVAILABLE rateRises after a releaseThe app shows something the server refuses; fix the gating
OTP and challenge failures, by typeJump at one bankCheck that bank's SMS gateway or policy
Config fetch failuresAbove 5%Check the config service and network
Phones below the minimum versionAfter a forced updateBank sends a customer message

Support lookup: in the ops console, a support agent enters a customer's mobile number and sees the bank, app version, bundle, config version and every feature with the layer that decided it. The app already has this logic in featureService.explain(); the server mirrors it.

When something goes wrong

ProblemFastest fixTakes effectWho
A feature misbehaves at one bankSwitch it off in the admin portalMinutesBank admin or ops
A new JS bundle crashes on startNothing: Hot Updater rolls that phone backNext launchAutomatic
A new JS bundle has a bugDisable the bundle in the Hot Updater consoleMinutesRelease manager
A security step must change nowChange that bank's security policyNext transferOps, with the bank
A native bug in an old versionShip a fixed store build, then raise the minimum versionHours to daysPlatform team, bank
A bad store build is rolling outHalt the staged rollout in the store consoleHoursBank's store account
One bank's backend is down, or a backend release is badRoll that bank's backend back to the previous image; other banks are not affectedMinutesDevOps, with the bank

09One bank's update

Every change takes one of five lanes. The lane depends on what changed, not on urgency.

CHANGE MADE IN CHECKED BY TRAVELS VIA REACHES PHONES Switch or settingbank level, live Bank admin portalor platform ops Maker-checkertwo people approve Config fetchstart · resume · sign-in Minutesno app release Security policyTPIN, OTP, step-up Flow engine policyone per bank Ops review+ bank sign-off Next server replyinside the flow Next transferno app release Backend changeAPI, rules, fixes Pull requestbackend codebase CI + bank UATbank's UAT backend That bank's backendrolling deploy Next API callno app release JS, branding, packscreens, fixes, logos Pull requestapp codebase CI + bank UATper-bank uat channel Hot Updater OTAstaged 10% → 100% Hours to daysno store review Native changeSDK, icon, app ID Pull request+ registry version CI + store reviewGoogle, Apple Store update+ forced update Weeksstore release
Server-side: no app release at allNeeds a new app build
The three blue lanes change only the bank's backend, so no app release is needed. Every lane that ships code, on the server or on the phone, passes through that bank's UAT first.

A bug fix for Bank B, day by day

MON TUE WED THU FRI NEXT MON Platform team CI · Hot Updater Bank B testers Bank B customers Fix merged to main Metrics OK? → go to 50% Still OK? → go to 100% Bundle built → bank-b-uat Deploy prod bank-b-prod 10% Rollout 50% one click Rollout 100% done UAT phones update on launch Bank B QA signs off 10% of phones on next open 50% of phones on next open Every phone on next open
Bank B's OTA channelsCrash and error data that drives the next step
Bank A and Bank C run the same steps on their own channels and their own sign-off. If Bank B finds a problem at any point, the release manager disables the bundle and Bank B's phones return to the previous one.

Every kind of change, for Bank B

Bank B wants…LaneStepsReaches customersWho acts
Loans off today, during an incidentswitchAdmin portal → second approver → config v+1 → phones refresh on resumeMinutesBank B admin
A lower RTGS minimumsettingSame as a switchMinutesBank B admin
OTP only up to ₹5,000policyOps edits Bank B's policy on the UAT org → Bank B tests → same policy on productionNext transfer after go-liveOps, Bank B
A transfer-screen bug fixedOTAThe week shown above3–7 days; a hotfix the same dayPlatform team, Bank B QA
New logo or coloursOTAChange tenants/bank-b/ → only Bank B's bundles rebuild → UAT → prodDaysPlatform team, Bank B
A backend fix or a new APIbackendBackend pull request → new image → Bank B's UAT backend → Bank B signs off → Bank B's production backendNext API call after the deployBackend team, Bank B
A new feature (for example UPI)backend, OTA, then switchBank B's backend upgraded first → feature ships off in a JS train → on for Bank B UAT → Bank B signs off → on for Bank B prodAfter the backend upgrade, the next train and UATBackend and platform teams, Bank B
A new native SDK, app icon or app namestoreNative train → Bank B UAT build → submit in Bank B's account → staged rollout → raise minimum version1–3 weeksPlatform team, Bank B store owner

What keeps Bank B's change away from other banks

  • A change only under tenants/bank-b/: CI detects it and rebuilds only Bank B's bundles.
  • A change to shared code: every bank gets it in UAT, but each bank's production moves only after its own sign-off.
  • A backend change or a switch: it happens in Bank B's own backend deployment. Other banks run separate deployments, and Bank B's admins cannot reach them.
  • On the phone: the channel and the tenant guard stop any other bank's bundle from running.

10Security

  1. The server is the boundary. Every business API checks the bank, the role and the customer again. App-side gating is for the customer's convenience.
  2. No mock code in release builds. Fixtures and fake OTPs exist only in the dev environment, and CI checks the release bundle.
  3. Fail closed. An unknown feature key, challenge type or unreadable config means off or stop.
  4. The bank's off is final. Session and device layers cannot re-enable it; release builds let the device layer switch features off only.
  5. Kill switches survive offline, and the cached config only moves forward in version.
  6. No leaks between customers or banks: session flags wiped at sign-out, per-bank bundles, tenant guard, and a separate backend, database and key pair for every bank. The central platform holds no customer data.
  7. HTTPS with per-bank certificate pinning before production, for config, challenges and every API. The PIN encryption key is fetched from the bank's backend before every encryption, so pinning is what makes that key trustworthy.
  8. Signed code: a store key per bank, signed OTA bundles, production deploys only from CI.
  9. Crash reports without personal data, stored where the banks' data rules allow.
  10. Every change recorded: two-person approval and an audit log for switches and policies.

11Roadmap

Estimates are rough, for one mobile developer. The backend work runs in parallel from week one.

PhaseWorkEffortUnblocks
0 · Release blockersBuild environments and the mock guard; delete the OTP-123456 path; real signing; error boundaries and crash reporting; connect the feature config (HTTPS, 4-second limit, version check)5 daysAny build for customers
1 · Feature engineorgId on the config request; device layer off-only in release; wait for session flags; refresh on resume; skip no-op re-renders; payment lease3 daysReliable live switches
2 · Tenant packsFirst pack from today's values, including the backend address per environment and certificate pins; @tenant; settings; registry; AppText4 daysA second bank in code
3 · Build and CIFlavors (bank × environment), iOS schemes, release script, PR and release pipelines, version scheme5 daysRepeatable, signed releases
4 · OTASelf-hosted Hot Updater, channels, tenant guard, signed bundles, minimum-version forced update3 daysJS fixes without the store
5 · Flow frameworkChallenge runner; transfer, TPIN, MPIN and registration moved onto it8 daysDifferent flows per bank
B1 · Backend features (parallel)Config per bank with entitlements, catalog import, admin portal with approvals and audit, API enforcement, challenge protocol, /meta version reportBackend teamLive per-bank control
B2 · Per-bank deployment (parallel)Infrastructure scripts for one bank's UAT and production backends, CI deploys per bank with sign-off, ops console link, central monitoring, HTTPS certificatesDevOpsA backend for every bank
6 · Second bankThe onboarding checklist in section 021 dayBank two live

About 29 working days in total, roughly six weeks. Phase 0 comes first. Phases 1 and 2 can run side by side; phase 3 comes before phase 4; phase 5 can start any time after phase 2.

12Risks

RiskWhy it mattersHow we reduce it
Backend work slipsLive control and server-driven steps wait on itPack defaults and adapters keep the app shippable; start with an ops-only config API
Banks are slow to give store access or sign offReleases stall per bankStart account setup in phase 0; put UAT timelines in the contract
The build matrix growsBanks × platforms × environments per releaseParallel CI, rebuild only affected banks, bundle diffing
Banks drift onto different native versionsJS must work on several binariesTwo-version support window, forced update, train discipline
Running many backendsPatching, monitoring and backups for every bankOne image, infrastructure scripts, automated deploys, central dashboards, written runbooks
Bank backends drift onto different versionsThe app must work with several backend versionsFeatures gated by what each backend reports; at most two backend versions in production; additive APIs only
The OTA server is compromisedOne door into every bank's appSigned bundles, CI-only production deploys, audit of every deploy
Too many flagsCombinations become impossible to testRetire leaf flags; snapshot test of each bank's effective config; quarterly catalog review
Too many per-bank overridesSlot overrides drift from the shared screensLowest rung first; at most three overrides per bank; each has an owner
Crash reports carry personal dataBanking privacy rulesScrub before sending; choose storage that meets the banks' data rules

13Decisions needed

DecisionRecommendationWho decides
One backend for all banks, or one per bank?Decided One backend codebase, deployed separately for each bank and environmentDecided 12 Oct 2026
Where does each bank's backend run?In our cloud, one isolated account per bank, in India (RBI's payment-data storage rules), unless a bank requires its own data centre. The image and deploy steps are the same; in a bank's data centre, its operators deploy our signed imageDevOps, security, banks
Whose store accounts publish each app?Each bank's own Google Play and Apple accounts. Apple's guideline 4.2.6 expects the content owner to submit template-based appsProduct, banks
Adopt the server-driven security protocol?Yes; the app uses adapters on today's API until it is readyBackend lead, security
Who operates the admin portal?Bank staff with two-person approval; our ops for onboarding and emergenciesProduct, banks
Release rhythmWeekly JS train, monthly native train, backend releases per bank on sign-off, hotfix any dayMobile and backend leads
Where OTA bundles are hostedSelf-hosted Hot Updater on our own cloud, with signed bundlesSecurity, DevOps
Crash reporting toolOne that can scrub personal data and store it where the banks' rules allowSecurity
What a restricted customer seesHide what the bank does not offer; explain what the customer can unlock ("Complete KYC to apply")Product
How many app and backend versions we supportThe last two minor app versions, with forced update below that; at most two backend versions in production at onceMobile and backend leads, banks