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.
Which features does it show?
Five layers: product, bank, live bank switches, the customer's session, the device.
Who gets which featureHow does a feature work here?
Settings, server-driven security steps, journey variants. Never if (bank).
How does a change reach phones?
Four lanes, from minutes (switches) to days (store builds).
How every change ships01The 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.
- 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-uatandbank-a-prod - One registry entry: flavor, app IDs, version
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.
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.
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.
| Feature | Bank A · customer X | Bank A · customer Y | Bank B · anyone |
|---|---|---|---|
loans | on bank pack | on bank pack | off bank pack |
loans.apply | off session (KYC pending) | on | off parent is off |
cards | off bank live | off bank live | off default |
Staff phone turns cards on | off still killed | off still killed | off release builds ignore it |
- The root key decides a module. Turning
loanson 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_AVAILABLEeither 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.
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.
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.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.
Who owns what
| Task | Platform team (us) | Bank | DevOps |
|---|---|---|---|
| Feature scope | advises | decides | — |
| Tenant pack, branding | builds | approves | — |
| Store listing and developer account | helps | owns | — |
| Signing keys, CI secrets | — | provides account access | owns |
| Live feature switches | emergency kill only | owns | — |
| Customer rules (KYC, account type, new device) | configures | decides | — |
| Security challenge policy | configures, reviews | decides, signs off | — |
| UAT testing | supports | owns | — |
| OTA releases | owns | signs off | runs the pipeline |
| Store releases | builds | submits | — |
| Monitoring | owns app health | sees its own dashboard | owns infrastructure |
Who can change what
| Tool | Used by | Can change | Cannot change |
|---|---|---|---|
| Bank admin portal | Bank staff, with two-person approval | Its own bank's switches, settings, journey variants, customer blocks | Other banks, security policy, app code |
| Platform ops console | Our operations team | Onboarding, security policies, emergency kill for any bank | App code |
| Git and CI | Our developers | Shared code, tenant packs, flow adapters | Live switches |
| Hot Updater console | Release managers | Rollout percentage, disable or roll back a bundle | Native code |
| Play Console, App Store Connect | Bank, with our help | Store 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_AVAILABLErate: 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
- Copy
tenants/_templatetotenants/bank-c; add the identity, brand colours, logo, features and settings. - Run the contrast check with Bank C's colours and adjust until it passes.
- Add Bank C to
tenants/registry.json; the Android flavor and CI job appear automatically. - Add launcher icons and iOS configuration for Bank C.
- DevOps adds Bank C's signing key to CI secrets.
- Ops creates org
bank-c: feature switches, customer rules, security policy, admin users. - The bank creates its store listing and adds our team.
- 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.
Which change goes where
| Example | Lane | Scope |
|---|---|---|
| Turn loans off for Bank B during an incident | switch | One bank |
| Change Bank A's RTGS minimum or lock timeout | setting | One bank |
| Block one customer from adding payees for 24 hours | switch session rule | One customer |
| Raise Bank B's OTP-only limit from ₹5,000 to ₹10,000 | policy | One bank |
| Fix a bug on the transfer screen | OTA | Every bank, channel by channel |
| New logo or colours for Bank C | OTA | One bank |
| New screen behind a new feature key (ships off) | OTA, then switch per bank | Banks that opt in |
| New native SDK, permission, app icon or app ID | store | Every bank that needs it |
The OTA pipeline
Keeping versions compatible
- 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
| Problem | Fastest fix | Takes effect | Who |
|---|---|---|---|
| A feature misbehaves at one bank | Switch it off in the admin portal | Minutes | Bank admin or ops |
| A new JS bundle crashes on start | Nothing: Hot Updater rolls that phone back | Next launch | Automatic |
| A new JS bundle has a bug | Disable the bundle in the Hot Updater console | Minutes | Release manager |
| A security step must change now | Change that bank's challenge policy | Next transfer | Ops, with the bank |
| A native bug in an old version | Ship a fixed store build, then raise the minimum version | Hours to days | Platform team, bank |
| A bad store build is rolling out | Halt the staged rollout in the store console | Hours | Bank's store account |
08Security rules
- 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.
- Fail closed. An unknown feature key, an unknown challenge type or an unreadable config means "off" or "stop", never "allow".
- 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.
- Kill switches survive offline. The last bank config is cached and applied on a cold start without network.
- 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.
- HTTPS before production. Config and challenge replies must not be changeable in transit.
- Signed code. Separate store signing keys per bank, and signed OTA bundles.
- 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.
| Phase | Work | Size |
|---|---|---|
| 0 · Prepare | Branch, commit the work in progress, real release signing | ½ day |
| 1 · Harden the feature engine | Send orgId with config, 4-second fetch limit, device layer off-only, wait for session flags, refresh on resume | 1–2 days |
| 2 · Tenant packs | Move today's identity into the first pack, Metro @tenant, settings | 2–3 days |
| 3 · Build and delivery | Registry, Android flavors, iOS schemes, release script, CI matrix | 2–3 days |
| F · Flow framework | Challenge runner, then transfer, TPIN, MPIN and registration onto it | 6–8 days |
| 4 · Backend | Config with entitlements, challenge protocol, admin portal, API enforcement | Backend team |
| 5 · OTA per bank | Hot Updater channels, tenant guard, bundle signing | 1 day |
| 6 · Second bank | Follow the checklist in section 06 | ½ day |