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.
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.
| Area | Today | Proposed |
|---|---|---|
| Banks | One, hard-coded in about ten places | Any number; one tenant pack and one registry entry each |
| Backend | One server, its address edited by hand in source; ten hosts listed in the Android network config | One backend codebase, one isolated deployment per bank and environment |
| Feature control | Only what is compiled in; the server switch is not connected | Live switches per bank, rules per customer, cached for offline |
| Different flows per bank | Would need if (bank) code | Settings, server-driven security steps, journey variants |
| Builds and releases | Manual, debug-signed, no checks | CI checks on every change, signed builds per bank and environment |
| Bug fixes | Store release only | OTA per bank in hours, with UAT sign-off and rollback |
| Safety net | No error boundary, no crash data, no forced update | Error boundaries, crash reporting per bank, auto-rollback, forced update |
| Operations | None | Admin portal, two-person approval, audit log, per-bank dashboards |
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.
Inside the app
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.
| Component | Runs | Job | Status |
|---|---|---|---|
| Mobile and business APIs | In each bank's backend | Existing CBS APIs, plus an entitlement check on every call (FEATURE_NOT_AVAILABLE) | extend |
| Config service | In each bank's backend | The bank's switches, settings and journey variants; per-customer entitlements after sign-in; served at /features/config | extend the app calls it; the server side is not built yet |
| Flow engine | In each bank's backend | The bank's security policy; replies one step at a time (TPIN, OTP, biometric) | new |
| Bank admin portal | In each bank's backend | Bank staff change their own bank, with two-person approval | new |
| Audit log | In each bank's backend | Every change and every ops action, kept with the bank's own data | new |
| Database, key pair, core-banking link | In each bank's backend | The bank's customers and sessions, its MPIN/TPIN key pair, its connection to its core banking system | per bank |
| Ops console | Central | Onboarding, security policies, emergency kill, support lookup; reaches each bank's backend over a private, audited link | new |
| Feature catalog | Central | Every feature key the app knows; each backend imports it when it is deployed | new |
| Hot Updater server | Central | OTA bundles and channels, self-hosted | new |
| Monitoring | Central | Crash reports, metrics and logs without customer data, tagged by bank, bundle and version | new |
Environments
| Environment | Used by | Backend | Build | OTA channel | Mock data |
|---|---|---|---|---|---|
| Dev | Developers | Shared dev backend, or the local mock adapter | bankADevDebug | none (Metro) | Allowed, debug builds only |
| UAT | Our QA, the bank's testers | Bank A's UAT backend | bankAUatRelease | bank-a-uat | Never |
| Prod | Customers | Bank A's production backend | bankAProdRelease | bank-a-prod | Never |
02Tenant flow
Today: what is wrong
| Severity | Flaw | Where | Effect |
|---|---|---|---|
| Blocker | The bank is hard-coded in about ten places: org id, app name, support email, app ID, display names, colours, logo and seven strings of copy | config/env.ts:34, config/appConfig.ts:4, android/app/build.gradle:82, theme/palette.ts:193, BrandLogo:6 | A second bank means editing shared code |
| High | The backend address is edited by hand in source; the Android network config lists ten hosts | config/env.ts:26, network_security_config.xml | With a backend per bank, a build can easily talk to the wrong bank's server |
| High | Nothing checks that the app reached its own bank's backend; the config request does not even name the bank | features/featureRemoteConfig.ts:113 | A wrong address would show another bank's switches without any error |
| High | No per-bank defaults; the build-time layer is empty | features/featureConfig.ts:311 | A fresh install offline shows global defaults, maybe features this bank lacks |
| Medium | No list of banks | none | Nothing 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-uatandbank-b-prod - One registry entry: flavor, app IDs, version
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
Onboarding a bank
- Copy
tenants/_templatetotenants/bank-c; fill in identity, brand colours, logo, features, settings, backend addresses and certificate pins. - Run the contrast check with Bank C's colours and adjust until every text passes.
- Add Bank C to
tenants/registry.json; its Android flavors and CI jobs appear automatically. - Add launcher icons and the iOS configuration for Bank C.
- DevOps stores Bank C's signing key in CI secrets.
- 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.
- Ops configures both backends: feature switches, customer rules, security policy, admin users.
- The bank creates its store listing and gives our team access.
- 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
| Severity | Flaw | Where | Effect |
|---|---|---|---|
| Blocker | The kill switch is not connected. Startup passes useMock: true, so the server is never asked, and an empty config is cached | featureRemoteConfig.ts:150, hydrateFeatures.ts:28, env.ts:40 | No feature can be switched off remotely |
| High | The customer's flags arrive after the app is already on screen | useAppBootstrap.ts:76, RootNavigator.tsx:89 | A tab the customer must not have can flash |
| High | Config is refreshed only at launch and sign-in | hydrateFeatures.ts:28 | A kill reaches active users late |
| High | The device layer can switch features on in release builds | adminFeatures.ts:38 | A staff phone can show features the bank does not have |
| High | The cached config has no version check | featureRemoteConfig.ts:165 | An old or empty reply silently undoes a kill |
| Medium | New keys reach every bank; module children are switched off one by one | featureConfig.ts:120, :205 | An OTA can leak a feature; turning on loans does not turn on the module |
| Medium | Different flows per bank would need code branches: five hand-written step machines, OTP length copied in four places | PaymentConfirmationScreen.tsx:40, TpinCreateFlow.tsx:30 | Every bank variation becomes if (bank) |
Proposed flow: who gets which feature
loans off hides all of loans.*), and an unknown key is always off.| Feature | Bank A · customer X (KYC pending) | Bank A · customer Y | Bank B · anyone |
|---|---|---|---|
loans | on bank pack | on bank pack | off bank pack |
loans.apply | off session rule | on | off parent is off |
cards (Bank A outage) | off bank live | off bank live | off default |
Staff phone turns cards on | off still killed | off still killed | off release ignores it |
Proposed flow: a feature's life
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.*andcards.*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.
initiate and the OTP in verify-otp; an adapter maps it onto the same runner, so the app can move 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.04How the UI renders
Today: what is wrong
| Severity | Flaw | Where | Effect |
|---|---|---|---|
| Blocker | Mock 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 data | env.ts:40, serviceHelpers.ts:55, PaymentConfirmationScreen.tsx:453, paymentsApi.ts:190, featureConfig.ts:107 | A customer is told money moved when it did not |
| High | No error boundary anywhere | none | One render error blanks the whole app; OTA auto-rollback misses crashes after the first screen |
| High | The splash waits for the config request, up to the 15-second client timeout | RootNavigator.tsx:52, apiClient.ts:14 | Slow start on a weak network |
| High | When a feature turns off, its screen is removed, even in the middle of a payment | FeatureRoute.tsx:39, featureRoutes.ts | The transfer result is lost; a retry can create a duplicate transfer |
| Medium | Every config refresh re-renders every gated component, even when nothing changed | featureService.ts:223 | Wasted renders; worse with refresh on resume |
| Medium | Six screens of 640–724 lines mix UI, steps, API calls and error handling; no component tests | PaymentConfirmationScreen, RegisterScreen, TpinResetFlow and three more | Hard to change per bank; breaks silently |
| Medium | The global font is set through Text.defaultProps, which React 19 ignores for function components | index.js:36 | Text without its own font likely falls back to the system font |
| Medium | All on-screen warnings hidden in development; Redux serializable check off | index.js:19, store.ts:11 | Rendering bugs go unnoticed |
| Medium | Three data-access patterns plus fixtures re-seeded at every sign-in | ARCHITECTURE.md §4 | Two screens can show different values for the same thing |
Proposed flow: from launch to a gated screen
| Fix | How | Phase |
|---|---|---|
| No mock code in release | Build environments decide mocks; resolveWithMock works only in dev; features with no backend are off in UAT and prod; the OTP-123456 path is deleted | 0 |
| Error boundaries | One at the root and one per tab: "Something went wrong, back to Home", reported with bank tags | 0 |
| Fast, bounded start | Cache first; config fetch capped at 4 seconds | 0 |
| No flash after sign-in | Hold the app until session flags land, or receive them in the login response | 1 |
| Never pull a screen mid-payment | A running money flow holds a lease; its route is removed only after the flow ends | 1 |
| Re-render only on real change | Compare the new config with the old before bumping the version | 1 |
| Consistent font | A shared AppText component instead of Text.defaultProps | 2 |
| Smaller screens | The flow framework takes steps, PIN, OTP and error handling out of screens; component tests per flow | 5 |
| See warnings again | Remove LogBox.ignoreAllLogs; turn the serializable check back on | 0 |
| One source of truth | Move slices to RTK Query as each module gets its real API | ongoing |
05Development flow
Today: what is wrong
| Severity | Flaw | Where | Effect |
|---|---|---|---|
| High | No CI pipeline; lint, type check, tests and the feature audit run only by hand | .github/ has no workflows | Broken code and unwired flags reach main |
| High | No build environments: one env.ts holds the host and the mock switches | config/env.ts | Whatever is committed ships, mocks included |
| Medium | Unit tests only; no component or navigation tests | ARCHITECTURE.md §11 | Screens and navigation break silently |
| Medium | Warnings hidden in development | index.js:19 | Bugs found late, by customers |
Proposed flow
release/x.y branch and ship on the same OTA train.| Branch or tag | Purpose | Lifetime |
|---|---|---|
main | Always releasable; the weekly JS train is cut from here | Permanent |
feat/… | One change, merged by pull request | Days |
release/1.6 | Created at each store release; hotfixes for that native version | Until 1.6 leaves support |
v1.6.0 | Tag on every store release | Permanent |
ota-2026-10-15 | Tag on every JS train | Permanent |
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
| Severity | Flaw | Where | Effect |
|---|---|---|---|
| Blocker | Release builds are signed with the debug key | android/app/build.gradle:103 | Cannot be published to the Play Store |
| Blocker | The PIN encryption key is fetched over plain HTTP before every encryption | security/mpinCrypto.ts:53, config/env.ts:26 | Someone on the same network can swap the key and read customers' MPIN and TPIN |
| High | Plain HTTP to a private LAN address | config/env.ts:26 | Works only on the office network or VPN; all traffic can be read and changed |
| High | One app identity, no flavors or schemes | build.gradle:82 | Cannot build a second bank's app |
| High | Version 1.0, version code 1, no scheme | build.gradle:85-86 | Store updates and OTA targeting need x.y.z |
| Medium | No release automation or store upload | none | Manual, error-prone, not repeatable per bank |
Proposed flow
| Build variant | Environment | Signed with | Installed from | OTA channel |
|---|---|---|---|---|
bankADevDebug | Dev | Debug key | Developer machine | none |
bankAUatRelease | UAT | Bank A's upload key | Internal track, TestFlight | bank-a-uat |
bankAProdRelease | Prod | Bank A's upload key | Play Store, App Store | bank-a-prod |
Versions
- Each bank's version lives in
tenants/registry.jsonasMAJOR.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
- 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 /metareturns 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
| Severity | Flaw | Where | Effect |
|---|---|---|---|
| Blocker | No OTA at all | package.json | Every JS fix waits for a store release and review in each bank's account |
| High | No safety net: no error boundary, no crash reporting | none | A staged rollout cannot see failures |
| High | No forced update | none | Phones on a broken native version stay broken |
| Medium | Version 1.0 cannot be targeted precisely | build.gradle:86 | Bundles 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.
The weekly JS train
| Day | Step | Who |
|---|---|---|
| Monday | Cut from main; CI builds a bundle per bank and per supported native version; deploys to every -uat channel at 100% | CI, release manager |
| Tuesday–Wednesday | Each bank tests on its UAT app | Bank QA, our QA |
| Wednesday | Sign-off per bank, recorded in the release ticket | Bank |
| Thursday | Production 10%, only for banks that signed off | Release manager |
| Friday | 50%, if crash-free sessions stay at or above 99.5% and no error rate jumps | Release manager |
| Next Monday | 100% | 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
x.y.z versions, signed bundles and the tenant guard. Without them, a staged rollout is blind.08Management flow
Today: what is wrong
| Severity | Flaw | Where | Effect |
|---|---|---|---|
| High | No backend behind the feature config yet | comment above MOCK_CONFIG in featureRemoteConfig.ts | Nobody can change a bank's features |
| High | No admin portal, approvals or audit log | none | Changes untracked; no two-person control, which banks expect |
| High | No monitoring per bank | no crash or analytics library | Cannot tell which bank or bundle is failing |
| Medium | Roles are not defined | none | Unclear who may switch, test or sign off |
Proposed flow: changing a bank's switch
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 | — | grants account access | owns |
| The bank's backend environment | builds and deploys to it | owns it if it runs in the bank's data centre | runs it if it runs in our cloud |
| Backend upgrades | builds, tests | signs off | deploys |
| Live feature switches | emergency kill only | owns | — |
| Customer rules (KYC, account type, new device) | configures | decides | — |
| Security policy | configures, reviews | decides, signs off | — |
| UAT testing | supports | owns | — |
| OTA releases | owns | signs off | runs the pipeline |
| Store releases | builds | approves in its account | uploads via CI |
| 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, two-person approval; runs inside the bank's own backend | Its own bank's switches, settings, journey variants, customer blocks | Other banks (separate deployments), security policy, app code |
| Platform ops console | Our operations team, over a private, audited link to each bank's backend | Onboarding, security policies, emergency kill for any bank, support lookup | 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 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.
| Signal | Alert when | Action |
|---|---|---|
| Crash-free sessions, per bank and bundle | Below 99.5% after a rollout step | Pause the rollout; disable the bundle if it keeps falling |
FEATURE_NOT_AVAILABLE rate | Rises after a release | The app shows something the server refuses; fix the gating |
| OTP and challenge failures, by type | Jump at one bank | Check that bank's SMS gateway or policy |
| Config fetch failures | Above 5% | Check the config service and network |
| Phones below the minimum version | After a forced update | Bank 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
| 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 security 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 |
| One bank's backend is down, or a backend release is bad | Roll that bank's backend back to the previous image; other banks are not affected | Minutes | DevOps, with the bank |
09One bank's update
Every change takes one of five lanes. The lane depends on what changed, not on urgency.
A bug fix for Bank B, day by day
Every kind of change, for Bank B
| Bank B wants… | Lane | Steps | Reaches customers | Who acts |
|---|---|---|---|---|
| Loans off today, during an incident | switch | Admin portal → second approver → config v+1 → phones refresh on resume | Minutes | Bank B admin |
| A lower RTGS minimum | setting | Same as a switch | Minutes | Bank B admin |
| OTP only up to ₹5,000 | policy | Ops edits Bank B's policy on the UAT org → Bank B tests → same policy on production | Next transfer after go-live | Ops, Bank B |
| A transfer-screen bug fixed | OTA | The week shown above | 3–7 days; a hotfix the same day | Platform team, Bank B QA |
| New logo or colours | OTA | Change tenants/bank-b/ → only Bank B's bundles rebuild → UAT → prod | Days | Platform team, Bank B |
| A backend fix or a new API | backend | Backend pull request → new image → Bank B's UAT backend → Bank B signs off → Bank B's production backend | Next API call after the deploy | Backend team, Bank B |
| A new feature (for example UPI) | backend, OTA, then switch | Bank 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 prod | After the backend upgrade, the next train and UAT | Backend and platform teams, Bank B |
| A new native SDK, app icon or app name | store | Native train → Bank B UAT build → submit in Bank B's account → staged rollout → raise minimum version | 1–3 weeks | Platform 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
- 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.
- No mock code in release builds. Fixtures and fake OTPs exist only in the dev environment, and CI checks the release bundle.
- Fail closed. An unknown feature key, challenge type or unreadable config means off or stop.
- The bank's off is final. Session and device layers cannot re-enable it; release builds let the device layer switch features off only.
- Kill switches survive offline, and the cached config only moves forward in version.
- 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.
- 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.
- Signed code: a store key per bank, signed OTA bundles, production deploys only from CI.
- Crash reports without personal data, stored where the banks' data rules allow.
- 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.
| Phase | Work | Effort | Unblocks |
|---|---|---|---|
| 0 · Release blockers | Build 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 days | Any build for customers |
| 1 · Feature engine | orgId on the config request; device layer off-only in release; wait for session flags; refresh on resume; skip no-op re-renders; payment lease | 3 days | Reliable live switches |
| 2 · Tenant packs | First pack from today's values, including the backend address per environment and certificate pins; @tenant; settings; registry; AppText | 4 days | A second bank in code |
| 3 · Build and CI | Flavors (bank × environment), iOS schemes, release script, PR and release pipelines, version scheme | 5 days | Repeatable, signed releases |
| 4 · OTA | Self-hosted Hot Updater, channels, tenant guard, signed bundles, minimum-version forced update | 3 days | JS fixes without the store |
| 5 · Flow framework | Challenge runner; transfer, TPIN, MPIN and registration moved onto it | 8 days | Different 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 report | Backend team | Live 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 certificates | DevOps | A backend for every bank |
| 6 · Second bank | The onboarding checklist in section 02 | 1 day | Bank 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
| Risk | Why it matters | How we reduce it |
|---|---|---|
| Backend work slips | Live control and server-driven steps wait on it | Pack defaults and adapters keep the app shippable; start with an ops-only config API |
| Banks are slow to give store access or sign off | Releases stall per bank | Start account setup in phase 0; put UAT timelines in the contract |
| The build matrix grows | Banks × platforms × environments per release | Parallel CI, rebuild only affected banks, bundle diffing |
| Banks drift onto different native versions | JS must work on several binaries | Two-version support window, forced update, train discipline |
| Running many backends | Patching, monitoring and backups for every bank | One image, infrastructure scripts, automated deploys, central dashboards, written runbooks |
| Bank backends drift onto different versions | The app must work with several backend versions | Features gated by what each backend reports; at most two backend versions in production; additive APIs only |
| The OTA server is compromised | One door into every bank's app | Signed bundles, CI-only production deploys, audit of every deploy |
| Too many flags | Combinations become impossible to test | Retire leaf flags; snapshot test of each bank's effective config; quarterly catalog review |
| Too many per-bank overrides | Slot overrides drift from the shared screens | Lowest rung first; at most three overrides per bank; each has an owner |
| Crash reports carry personal data | Banking privacy rules | Scrub before sending; choose storage that meets the banks' data rules |
13Decisions needed
| Decision | Recommendation | Who decides |
|---|---|---|
| One backend for all banks, or one per bank? | Decided One backend codebase, deployed separately for each bank and environment | Decided 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 image | DevOps, 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 apps | Product, banks |
| Adopt the server-driven security protocol? | Yes; the app uses adapters on today's API until it is ready | Backend lead, security |
| Who operates the admin portal? | Bank staff with two-person approval; our ops for onboarding and emergencies | Product, banks |
| Release rhythm | Weekly JS train, monthly native train, backend releases per bank on sign-off, hotfix any day | Mobile and backend leads |
| Where OTA bundles are hosted | Self-hosted Hot Updater on our own cloud, with signed bundles | Security, DevOps |
| Crash reporting tool | One that can scrub personal data and store it where the banks' rules allow | Security |
| What a restricted customer sees | Hide what the bank does not offer; explain what the customer can unlock ("Complete KYC to apply") | Product |
| How many app and backend versions we support | The last two minor app versions, with forced update below that; at most two backend versions in production at once | Mobile and backend leads, banks |