Proposal · Multi-bank mobile banking platform · October 2026

One Codebase, Many Banks

How we build one mobile banking product that ships as a separate app for every bank, with each bank's own features, flows, backend and release schedule, and how we run it safely.

Status Proposal Audience product, engineering, operations, bank partners Covers mobile apps, backend, delivery, operations

00Summary

The problem. Many banks want the same mobile banking product. Each needs its own app in the stores, its own set of features, its own way of doing some things, and its own data kept apart. A separate app per bank would multiply cost and bugs with every new bank.

Our approach. One product, configured per bank:

  • One app codebase and one backend codebase. A bank is a tenant pack for the app and a separate deployment of the backend.
  • Features decided by the bank and the server, through five layers, down to the individual customer. A bank's "off" is final (how to switch a feature).
  • Different flows without different code: settings, server-driven security steps and journey variants, never if (bank).
  • Five update lanes, from a live switch in minutes to a store release in weeks. Every code change passes the bank's own UAT first.
  • Operations built in: an admin portal with two-person approval, an audit log and monitoring for every bank.

Decided: one backend codebase, deployed separately for each bank. To decide: eight questions in section 15, starting with where each bank's backend runs.

What banks needHow we deliver it
Many banks, one productOne app codebase and one backend codebase; each bank is a tenant pack plus a backend deployment
Their own app in the storesA build per bank: its own app ID, name, icon, signing key and store account
Different features per bank and per customerFive feature layers; the bank and the server decide; a bank's "off" is final
Different flows per bankSettings, server-driven security steps and journey variants
Their data kept apartA separate backend, database and key pair for every bank
Fast, safe updatesOTA per bank with UAT sign-off, staged rollout and instant rollback
Control and accountabilityAdmin portal with two-person approval, audit log, per-bank monitoring
Low cost per bankA new bank in days; one fix reaches every bank

01The problem

Each bank is a separate customer with its own brand, products, rules and approval process. Differences appear at three levels:

LevelExample
BankBank A offers loans and cards; Bank B offers only payments
RoleCustomers get the banking app; field agents get the agent app
CustomerA customer whose KYC is pending cannot apply for a loan

Banks also want the same feature to work differently:

FeatureBank ABank BBank C
Approve a transferTPIN, then OTPOTP only up to ₹5,000; TPIN and OTP aboveBiometric, then TPIN
Register a deviceMobile, SMS OTP, MPINMobile, debit card and PIN, OTP, MPINMobile, account number and date of birth, OTP, MPIN
Add a payeeVerify the name, then saveSave, then a 24-hour cooling periodSave with an OTP
Loan applicationProduct, amount, employment, documents, reviewProduct, amount, documents, reviewNo loans

What the platform must guarantee

#Requirement
R1One codebase and one set of screens for every bank
R2Each bank's own branding, app identity and store listing
R3Per-bank features, changeable without a store release
R4Per-role and per-customer features, decided by the server
R5A bank's "off" can never be overridden by a role, a customer or a device
R6Correct behaviour offline and before the network answers
R7Each bank's data, keys and backend kept apart
R8One command releases any bank, and each bank releases on its own approval
R9A new bank in days, and the server enforces everything the app hides
What we will not do: a separate codebase per bank, a Git branch per bank, or if (bank === …) checks in shared code. Each of these multiplies cost and bugs with every new bank.

02Our approach

  • One product, configured per bank. One app codebase and one backend codebase. A bank is a tenant pack for the app and a separate deployment of the backend.
  • Bank differences are data. They live in tenant packs and server configuration, never in code branches.
  • The server decides access and security. The app follows the server and never widens what it allows.
  • Fail closed. An unknown feature, step or setting means off or stop.
  • Every bank is isolated: its own app build, backend, data, keys, update channels, store listing and approvals.
  • Every change has a lane. We use the fastest lane a change allows, and no code reaches a bank's customers without that bank's sign-off.
One app codebase 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 one backend codebase, deployed per bank 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.

How each kind of difference is handled

What differs between banksHandled byHow a change reaches phones
Identity and brandingTenant pack, chosen at build timeOTA for text, colours and images; store build for icon, name and app ID
Which features the bank offersPack defaults and the bank's live switchesNext config refresh, within minutes
Which features a customer getsServer rules applied at sign-inNext sign-in or refresh
How a feature worksSettings, server-driven security steps, journey variantsNext config refresh, or the next flow started
Where data livesThe bank's own backend deploymentNot applicable: it never leaves the bank's backend

03Architecture

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, backend address and defaults Services API client · security · storage tenants/bank-a build Flow engine /features/config runtime, from the bank's backend
Differs 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.

ComponentRunsJob
Mobile and business APIsIn each bank's backendBanking APIs, with an entitlement check on every call (FEATURE_NOT_AVAILABLE)
Config serviceIn each bank's backendThe bank's switches, settings and journey variants, plus per-customer entitlements after sign-in
Flow engineIn each bank's backendThe bank's security policy; replies one step at a time (TPIN, OTP, biometric)
Bank admin portalIn each bank's backendBank staff change their own bank, with two-person approval
Audit logIn each bank's backendEvery change and every ops action, kept with the bank's own data
Database, key pair, core-banking linkIn each bank's backendThe bank's customers and sessions, its PIN encryption key pair, its connection to its core banking system
Ops consoleCentralOnboarding, security policies, emergency kill, support lookup; reaches each backend over a private, audited link
Feature catalogCentralEvery feature key the app knows; each backend imports it when it is deployed
OTA serverCentralApp bundles and update channels, self-hosted
MonitoringCentralCrash reports, metrics and logs without customer data, tagged by bank and version

Environments

EnvironmentUsed byBackendApp buildOTA channelTest data
DevDevelopersShared dev backend, or a local mockbankADevDebugnoneAllowed, debug builds only
UATOur QA, the bank's testersBank A's UAT backendbankAUatReleasebank-a-uatNever mock data
ProdCustomersBank A's production backendbankAProdReleasebank-a-prodNever
No hand-edited server addresses. Each bank's backend address and certificate pins, per environment, live in its tenant pack. The build picks them from the bank and the environment.

04Tenant 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
  • Backend address and certificate pins per environment
  • Feature defaults, settings, flow choices

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 · OTA server

  • 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 the OTA client 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, build, backend 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
A bank moves through the same six stages. After go-live, every change, large or small, goes back through that bank's UAT.

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 app builds 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.

05Feature flow

Every screen, tab, button and action has a feature key such as loans.apply. Keys form a tree: loans off hides everything under it. The app resolves each key through five layers, and a later layer overrides an earlier one, with one exception that protects the bank.

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, and an unknown key is always off. The last known bank config is cached, so a kill switch still holds when the phone starts offline.
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

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 (such as loans) 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.
  • New keys ship off. A CI check fails any new key that defaults to on without a written reason, so an update never shows a new feature to a bank that did not ask for it.
  • The backend knows every key. CI publishes a feature catalog on each merge; each 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. 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.

How to switch a feature on or off

There are five places to switch a feature. Which one to use depends on who decides and for whom. Day to day, the bank admin portal is the normal way; the others are defaults, automatic rules or testing tools.

Who switches itAffectsWhereTakes effect
DeveloperEvery bank: the product defaultThe default feature config, in codeNext app update
Mobile teamOne bank's starting setuptenants/bank-b/features.tsNext app update for that bank
Bank adminAll customers of that bank, liveAdmin portal, with two-person approvalMinutes, no app release
Server rulesOne role or one customerRules in the bank's backend, or a manual block with an expiryThe customer's next sign-in
QA or staffOne phoneThe feature switchboard screen in the appImmediately, on that phone

The bank's backend sends its decisions in one config reply. Bank-wide switches arrive at app start, on return to the app and at sign-in. The customer's own switches arrive after sign-in.

{
  "version": 44,
  "overrides":     { "loans": false, "cards": true },         // the whole bank
  "roleOverrides": { "payments.beneficiaries.add": false }    // this customer, after sign-in
}

How the app decides

  1. An unknown key is off.
  2. If the bank or the session says off, it is off. Nothing can undo it.
  3. If a parent is off (for example loans for loans.apply), it is off.
  4. Otherwise, the most specific switch that has a value wins.

The switchboard (testing only). The switchboard lists every key with its current value and the reason, such as "turned off by the bank" or "parent is off". In UAT builds, testers can switch features on and off. In production builds it can only switch features off, it never undoes a bank's switch, and only staff the bank allows can open it.

What the customer sees when a feature turns off

  • The tab or menu item disappears. There is no greyed-out button and no "coming soon" message.
  • A customer already on that screen is taken back. During a payment, the app waits until the payment ends.
  • Any remaining action is blocked in the app, and the server refuses it with FEATURE_NOT_AVAILABLE.

Making a new feature switchable

// 1. Add a key; new keys always ship off
LOANS_APPLY = 'loans.apply'
[FeatureKey.LOANS_APPLY]: { enabled: false }

// 2. Gate the screen: the route is not registered while the feature is off
ApplyLoan: FeatureKey.LOANS_APPLY

// 3. Gate the button
<FeatureGate feature={FeatureKey.LOANS_APPLY}>
  <Button title="Apply for loan" onPress={apply} />
</FeatureGate>

// 4. Guard the action
const apply = useFeatureAction(FeatureKey.LOANS_APPLY, submitApplication);

On the server, the loan-application endpoint maps to loans.apply and refuses the call while it is off. CI publishes the new key in the feature catalog, so it appears in every bank's admin portal.

You want to…UseTakes effect
Turn loans off for Bank B during an incidentBank B's admin portalMinutes
Launch UPI for Bank A onlyThe key ships off in an app update; Bank A's admin turns it onMinutes after the update reaches phones
Stop one customer adding payees for a dayA manual block with a 24-hour expiryThat customer's next sign-in
Test cards on a QA phoneThe switchboard on a UAT buildImmediately
Remove a feature from one bank for goodThe bank's pack, with the bank's switch off until the update landsNext app update

06Per-bank flows

When a bank wants a feature to work differently, we pick 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.
RungUse it whenHow it works
1 · SettingOnly a value differs: a limit, a timeout, a PIN lengthTyped settings in the pack, overridable by the bank's backend; for security timeouts the stricter value wins
2 · Server challengeSecurity steps differ: TPIN, OTP, biometric, step-up by amount, cooling periodsThe bank's backend names the next check; the app only knows how to show each kind of check
3 · Journey variantA form-style journey has different steps or orderNamed step lists compiled into the app; the bank picks one
4 · StrategySame screens, different rule or integrationA small interface with a shared default; the bank's pack overrides one part
5 · Slot overrideA section must look differentThe bank's pack supplies its own component for one named slot

Security steps come from the server

If the app decided "Bank B skips the TPIN", a modified app could skip it anywhere. So each bank's backend holds its policy and replies one step at a time: "now the TPIN", "now the OTP", "done".

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
A biometric step is verified by the server: the app signs a one-time code with a key that only the customer's fingerprint or face unlocks. Older APIs that take a PIN and an OTP in fixed steps are wrapped by a small adapter, so the app works the same way everywhere.
Not allowed: if (tenant.id === 'bank-b') or switch (tenant.orgId) in shared code, a Git branch per bank, or copied screens. An automatic lint check fails the build on the first two.

07How the app renders

The app never shows anything before it knows what this bank and this customer may use, and it never shows something the server would refuse.

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
A short, bounded wait
The cache makes the first step instant and keeps kill switches working offline. The two waits are short and bounded, so a weak network never leaves the customer on the splash screen.
RuleHow
Nothing shows before the app knows what is allowedCached bank config first; the config fetch is capped at a few seconds
No flash after sign-inThe app waits for the customer's flags, or receives them in the login response
A disabled feature leaves no traceIts routes are not registered, its UI is not drawn and its actions are guarded; no greyed-out buttons
Money flows are never interruptedA running payment holds a lease; a config change takes effect after it ends
Failures are containedAn error boundary at the root and on each tab; failures are reported with bank tags
Screens hold no bank rulesFlows, settings and strategies supply them, so a screen is the same for every bank
Branding comes from tokensColours, logo and fonts come from the pack through the theme and shared text components
Re-render only on real changeA config refresh that changes nothing changes nothing on screen
Test data never reaches customersMock data and test codes exist only in dev builds; CI checks every release build

08Development 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 test data in release builds • publish 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-11-05Tag 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.
  • Test data lives in 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; its route, UI and actions are gated; the server endpoint maps to the key; tests cover the flow; every CI check passes; the catalog is published; and the bank-facing notes are written.

09Deployment flow

App builds per bank

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

Native release train

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.

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.

Versions

  • Each bank's app version lives in tenants/registry.json as MAJOR.MINOR.PATCH. A native release bumps MINOR; a native hotfix bumps PATCH.
  • The store 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 app 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.
  • OTA credentials per environment; only CI may deploy to production channels.

Keeping app and backend compatible

  • 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, and the app ignores keys it does not know.

10OTA flow

OTA updates replace the app's JavaScript and images without a store release. 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 app build, 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, the OTA client rolls that phone back by itself. Later crashes are caught by error boundaries and reported, 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, and 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 at 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: a 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.
OTA goes live only with its safety net: error boundaries, crash reporting tagged by bank and bundle, signed builds, x.y.z versions, signed bundles and the tenant guard. Without them, a staged rollout cannot see what it is doing.

11Operations 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
OTA 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. 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 that bank's backend 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 same resolution logic runs in the app and on the server, so both always agree.

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: the OTA client rolls that phone backNext launchAutomatic
A new JS bundle has a bugDisable the bundle in the OTA consoleMinutesRelease manager
A security step must change nowChange that bank's security policyNext transferOps, with the bank
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
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

12One 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 OTA updatestaged 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 · OTA server 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 its UAT backend → Bank B tests → same policy in productionNext transfer after go-liveOps, 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 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 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.

13Security

  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. Each bank's data stays in its own backend: a separate database, key pair and certificate per bank. The central platform holds no customer data.
  3. HTTPS with per-bank certificate pinning, for config, security steps and every API. The PIN encryption key comes from the bank's backend, and pinning is what makes that key trustworthy.
  4. Fail closed. An unknown feature key, challenge type or unreadable config means off or stop.
  5. The bank's off is final. Session and device layers cannot re-enable it; release builds let the device layer switch features off only.
  6. Kill switches survive offline, and the cached config only moves forward in version.
  7. No leaks between customers or banks: session flags wiped at sign-out, a bundle per bank, the tenant guard, and token-scoped config.
  8. Test data never ships. Mock data and test codes exist only in dev builds, and CI checks every release build.
  9. Signed code: a store key per bank, signed OTA bundles, production deploys only from CI.
  10. Crash reports without personal data, stored where the banks' data rules allow.
  11. Every change recorded: two-person approval and an audit log for switches and policies.

14Risks

RiskWhy it mattersHow we reduce it
Banks are slow to give store access or sign offReleases stall per bankStart account setup early; 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 app 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 overridesOverrides 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

15Decisions

QuestionRecommendationWho decides
One backend for all banks, or one per bank?Decided One backend codebase, deployed separately for each bank and environmentDecided
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; adapters cover older APIs until every backend supports itBackend 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 hostedA self-hosted OTA server in 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

The version support window (the last two app versions and at most two backend versions in production) is part of this proposal and needs the banks' agreement.