Architecture · BankCBS mobile platform

Multi-Bank Platform Blueprint

One React Native codebase ships a separate app for every bank. This page shows how the pieces fit together, how we run each bank, and how every kind of change reaches customers' phones.

Status Proposal Covers mobile app, backend contract, delivery, operations Detail docs/MULTI_BANK_ARCHITECTURE.md, docs/BANK_FLOW_VARIANTS.md

Which bank is this app?

A tenant pack, chosen when the app is built.

What a bank is made of

Which features does it show?

Five layers: product, bank, live bank switches, the customer's session, the device.

Who gets which feature

How does a feature work here?

Settings, server-driven security steps, journey variants. Never if (bank).

When a bank's flow differs

How does a change reach phones?

Four lanes, from minutes (switches) to days (store builds).

How every change ships

01The platform at a glance

Code lives in one repository. A release script builds it once per bank and ships it in two ways: store builds for native changes, OTA bundles for JavaScript. On the phone, the app asks the backend what this bank and this customer may use, and the backend checks every action again.

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 config challenge API calls Platform backend Config service flags, settings Flow engine security steps Business APIs enforces access Platform ops console our team · onboarding Bank admin portal bank staff · approvals sets up org, policy toggles
Exists once per bankShared by every bank
Everything blue exists once per bank. Everything else is shared, so a fix in the shared core reaches every bank at once.
  • Bank differences are data, not code branches. They live in tenant packs and in server config.
  • The server decides security. Which checks a transfer needs, and whether a customer may use a feature at all.
  • Every change has a lane. A switch flips in minutes, a JS fix ships in hours, a native change takes days. Pick the fastest lane that is allowed (section 07).

02What a bank is made of

"Bank A" is not one thing. It exists in four places, and onboarding a bank means filling in all four. The bank's id (for example bank-a) ties them together.

Code

tenants/bank-a/

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

Native shell

Android flavor bankA · iOS scheme BankA

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

Server

org bank-a in the platform backend

  • Live feature switches, settings, journey variants
  • Security challenge policy and customer rules
  • Admin users, approvals, audit log

Delivery

tenants/registry.json · stores · Hot Updater

  • Store listing in Bank A's own developer account
  • OTA channels bank-a-uat and bank-a-prod
  • One registry entry: flavor, app IDs, version
Two lists must agree. tenants/registry.json (mobile) and the org table (backend) list the same banks with the same id. CI checks that every registry entry has a pack, and the onboarding checklist creates both.

03Inside the app

The app keeps its current layering. Two things are added: a tenant layer that supplies the bank's identity at build time, and a flows layer that runs security steps and journeys. Bank-specific input enters in only two places: the tenant pack when building, and server config when running.

App shell providers · 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.

04Who gets which feature

Every screen, tab, button and action has a feature key such as loans.apply. The app resolves each key through five layers. A later layer overrides an earlier one, with one exception that protects the bank: a false from the bank or from the session is final.

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 on top: a feature is on only if every parent is on (loans off hides all of loans.*), and an unknown key is always off.

Worked example

Bank A has loans and cards, but has switched cards off live during an outage. Bank B has no loans. Customer X at Bank A has KYC pending.

FeatureBank A · customer XBank A · customer YBank B · anyone
loanson bank packon bank packoff bank pack
loans.applyoff session (KYC pending)onoff parent is off
cardsoff bank liveoff bank liveoff default
Staff phone turns cards onoff still killedoff still killedoff release builds ignore it
  • The root key decides a module. Turning loans on for a bank turns on the whole module; children are switched off only to remove one capability.
  • New keys ship off. An OTA update never shows a new feature to a bank that did not ask for it.
  • The server checks again. Hiding a button is for the customer's convenience. The API refuses the call with FEATURE_NOT_AVAILABLE either way.

05When a bank's flow differs

Some banks want the same feature to work differently: other security checks, other steps, other limits. There are five ways to allow that. 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 added only when a real bank needs them.

Security steps come from the server

If the app decided "Bank B skips the TPIN", a modified app could skip it anywhere. So the server holds each bank's policy and replies one step at a time: "now the TPIN", "now the OTP", "done". The app only knows how to show each kind of check.

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
Today's API already works this way for IMPS: initiate takes the TPIN, and verify-otp takes the OTP. An adapter maps it onto the same runner, so the app can move to this structure 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.

06Managing the banks

Each bank moves through the same lifecycle. After go-live, every change, large or small, goes back through that bank's UAT before production.

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: switch the bank's features off live, unpublish the store listing, retire the channels, archive the org.

Who owns what

TaskPlatform team (us)BankDevOps
Feature scopeadvisesdecides—
Tenant pack, brandingbuildsapproves—
Store listing and developer accounthelpsowns—
Signing keys, CI secrets—provides account accessowns
Live feature switchesemergency kill onlyowns—
Customer rules (KYC, account type, new device)configuresdecides—
Security challenge policyconfigures, reviewsdecides, signs off—
UAT testingsupportsowns—
OTA releasesownssigns offruns the pipeline
Store releasesbuildssubmits—
Monitoringowns app healthsees its own dashboardowns infrastructure

Who can change what

ToolUsed byCan changeCannot change
Bank admin portalBank staff, with two-person approvalIts own bank's switches, settings, journey variants, customer blocksOther banks, security policy, app code
Platform ops consoleOur operations teamOnboarding, security policies, emergency kill for any bankApp 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 analytics event carries five tags: tenant, appVersion, bundleId, configVersion and channel. With those, one dashboard filters to any bank and support can see exactly what a customer was running.

  • Crash-free sessions per bank and per OTA bundle, to decide when to widen a rollout
  • OTA adoption per channel, and how many phones are still on old native versions
  • FEATURE_NOT_AVAILABLE rate: if it rises, the app is showing something the server refuses
  • Challenge failure rates by type, for example OTP not delivered at one bank

Adding a bank

  1. Copy tenants/_template to tenants/bank-c; add the identity, brand colours, logo, features and settings.
  2. Run the contrast check with Bank C's colours and adjust until it passes.
  3. Add Bank C to tenants/registry.json; the Android flavor and CI job appear automatically.
  4. Add launcher icons and iOS configuration for Bank C.
  5. DevOps adds Bank C's signing key to CI secrets.
  6. Ops creates org bank-c: feature switches, customer rules, security policy, admin users.
  7. The bank creates its store listing and adds our team.
  8. Build to bank-c-uat, the bank tests and signs off, then release to production.

07How every change ships

There are four lanes. Choose by what changed, not by urgency: a native change can never take the OTA lane, and a switch never needs a release.

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 JS, branding, packscreens, fixes, logos Pull requestone codebase CI + bank UATper-bank uat channel Hot Updater OTAstaged 10% → 100% Hoursno store review Native changeSDK, icon, app ID Pull request+ registry version CI + store reviewGoogle, Apple Store update+ forced update Daysstore release
Server-side: no app release at allNeeds a new app build
The two blue lanes change only server data, so they can be undone as quickly as they were made. The other two ship code and go through UAT.

Which change goes where

ExampleLaneScope
Turn loans off for Bank B during an incidentswitchOne bank
Change Bank A's RTGS minimum or lock timeoutsettingOne bank
Block one customer from adding payees for 24 hoursswitch session ruleOne customer
Raise Bank B's OTP-only limit from ₹5,000 to ₹10,000policyOne bank
Fix a bug on the transfer screenOTAEvery bank, channel by channel
New logo or colours for Bank COTAOne bank
New screen behind a new feature key (ships off)OTA, then switch per bankBanks that opt in
New native SDK, permission, app icon or app IDstoreEvery bank that needs it

The OTA pipeline

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
CI also runs the feature audit, the tenant matrix (every pack is valid) and the no-bank-branching lint rule. If a bundle crashes before the first screen, Hot Updater rolls that phone back by itself.
Suggested rhythm. A weekly JS train: cut on Monday, bank UAT Tuesday to Wednesday, staged production from Thursday. A monthly native train. Hotfixes any day, on the same path with a faster sign-off.

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 only reaches the native version it was built for, so JavaScript never runs on a binary that lacks its native code.
  • The app ignores config keys it does not know, so the server can publish ahead of the apps.
  • The backend keeps every API that the oldest supported app version uses. Remove a field only after the minimum version has moved past it.
  • The minimum supported version lives in server config. Below it, the app sends the customer to the store.

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 challenge 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

08Security rules

  1. The server is the boundary. Every business API checks the bank, the role and the customer again. The app's gating is for the customer's convenience.
  2. Fail closed. An unknown feature key, an unknown challenge type or an unreadable config means "off" or "stop", never "allow".
  3. The bank's off is final. Session and device layers cannot re-enable it, and release builds let the device layer switch features off only.
  4. Kill switches survive offline. The last bank config is cached and applied on a cold start without network.
  5. No leaks between customers or banks. The session layer is wiped on sign-out, each bank's bundle contains only its own pack, and a tenant guard refuses to run a bundle in the wrong bank's app.
  6. HTTPS before production. Config and challenge replies must not be changeable in transit.
  7. Signed code. Separate store signing keys per bank, and signed OTA bundles.
  8. Every change is recorded. Two-person approval and an audit log for switches and policies.

09Build order

The early phases change nothing customers can see. They make the current app ready for a second bank. Sizes are rough, for one developer.

PhaseWorkSize
0 · PrepareBranch, commit the work in progress, real release signing½ day
1 · Harden the feature engineSend orgId with config, 4-second fetch limit, device layer off-only, wait for session flags, refresh on resume1–2 days
2 · Tenant packsMove today's identity into the first pack, Metro @tenant, settings2–3 days
3 · Build and deliveryRegistry, Android flavors, iOS schemes, release script, CI matrix2–3 days
F · Flow frameworkChallenge runner, then transfer, TPIN, MPIN and registration onto it6–8 days
4 · BackendConfig with entitlements, challenge protocol, admin portal, API enforcementBackend team
5 · OTA per bankHot Updater channels, tenant guard, bundle signing1 day
6 · Second bankFollow the checklist in section 06½ day