- Positive Pay on the 12 Oct /pps contract (account number, not account ID): OTP enrollment, cheque submit, saved cheques, detail and stop payment. Full flow passed on the phone against mobile-banking-backend.lincpay. - Statement & Mini Statement guide v1.0 (14 Oct): mini statement JSON on Home and the Statement screen; dated PDF export with the password rule. - Onboarding slides and splash screen, Inter font, shared Icon and TypewriterText components, nearby branches via OpenStreetMap. - Every feature switched on; the 14 Oct switch set is kept in featureConfig.stage-2026-10-14.ts. Loans run on demo data until the backend has /loans routes. - npm run android wraps run-android for shells spawned from VS Code. - OpenProject work packages updated to 14-10-2026 status, with screenshots of Positive Pay, statement, loans and cards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1688 lines
137 KiB
HTML
1688 lines
137 KiB
HTML
<title>Multi-Bank Platform Proposal</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans+Condensed:wght@500;600&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;1,400&display=swap">
|
||
<style>
|
||
/* Layout: a reading column beside a sticky contents rail; each area reads "Today" then "Proposed flow"; diagrams sit in framed, horizontally scrollable panels. */
|
||
:root {
|
||
--bg: #F3F5F7;
|
||
--surface: #FFFFFF;
|
||
--ink: #15202B;
|
||
--muted: #556170;
|
||
--line: #D7DEE5;
|
||
--line-strong: #98A4B0;
|
||
--accent: #0B57D0;
|
||
--accent-soft: #E4EDFC;
|
||
--accent-ink: #0A3F96;
|
||
--kill: #B42318;
|
||
--kill-soft: #FCEBE9;
|
||
--warn: #8F5300;
|
||
--warn-soft: #FCF0DB;
|
||
--code-bg: #E9EEF3;
|
||
--font-display: "IBM Plex Sans Condensed", "Arial Narrow", "Segoe UI", sans-serif;
|
||
--font-body: "IBM Plex Sans", "Segoe UI", system-ui, sans-serif;
|
||
--font-mono: "IBM Plex Mono", ui-monospace, Consolas, monospace;
|
||
}
|
||
@media (prefers-color-scheme: dark) {
|
||
:root:not([data-theme="light"]) {
|
||
--bg: #0E141A; --surface: #151D25; --ink: #E5EAF0; --muted: #9AA7B5;
|
||
--line: #25313C; --line-strong: #4C5A68;
|
||
--accent: #7FA8F5; --accent-soft: #14284A; --accent-ink: #BCD1FA;
|
||
--kill: #F8786D; --kill-soft: #3A1815;
|
||
--warn: #F2B65C; --warn-soft: #3A2A10;
|
||
--code-bg: #1C2630;
|
||
color-scheme: dark;
|
||
}
|
||
}
|
||
:root[data-theme="dark"] {
|
||
--bg: #0E141A; --surface: #151D25; --ink: #E5EAF0; --muted: #9AA7B5;
|
||
--line: #25313C; --line-strong: #4C5A68;
|
||
--accent: #7FA8F5; --accent-soft: #14284A; --accent-ink: #BCD1FA;
|
||
--kill: #F8786D; --kill-soft: #3A1815;
|
||
--warn: #F2B65C; --warn-soft: #3A2A10;
|
||
--code-bg: #1C2630;
|
||
color-scheme: dark;
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
html { scroll-behavior: smooth; }
|
||
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
|
||
body {
|
||
background: var(--bg);
|
||
color: var(--ink);
|
||
font: 16px/1.62 var(--font-body);
|
||
padding-inline: 20px;
|
||
padding-block: 0 72px;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
a { color: var(--accent); }
|
||
a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }
|
||
code { font-family: var(--font-mono); font-size: 0.86em; background: var(--code-bg); padding: 1px 5px; border-radius: 4px; }
|
||
pre { font-family: var(--font-mono); font-size: 13px; line-height: 1.6; background: var(--code-bg); padding: 14px 16px; border-radius: 8px; overflow-x: auto; margin: 14px 0 22px; max-width: 760px; }
|
||
pre code { background: none; padding: 0; font-size: inherit; }
|
||
|
||
.wrap { max-width: 1160px; margin: 0 auto; display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 52px; }
|
||
@media (max-width: 1020px) { .wrap { grid-template-columns: minmax(0, 1fr); gap: 0; } .toc { display: none; } }
|
||
|
||
.toc { position: sticky; top: calc(env(safe-area-inset-top, 0px) + 28px); align-self: start; padding-top: 48px; font-size: 14px; }
|
||
.toc p { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); margin: 0 0 10px; }
|
||
.toc a { display: grid; grid-template-columns: 26px 1fr; color: var(--muted); text-decoration: none; padding: 4px 0; line-height: 1.35; }
|
||
.toc a span { font-family: var(--font-mono); font-size: 12px; color: var(--line-strong); }
|
||
.toc a:hover { color: var(--ink); }
|
||
|
||
main { min-width: 0; }
|
||
header.page { padding-block: 48px 10px; }
|
||
.eyebrow { font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent); margin: 0 0 14px; }
|
||
h1 { font-family: var(--font-display); font-weight: 600; font-size: clamp(36px, 5.4vw, 56px); line-height: 1.02; letter-spacing: -0.01em; margin: 0 0 18px; text-wrap: balance; }
|
||
.lede { font-size: 19px; line-height: 1.55; max-width: 720px; margin: 0 0 22px; }
|
||
.meta { display: flex; flex-wrap: wrap; gap: 8px 22px; font-size: 13.5px; color: var(--muted); margin: 0 0 26px; }
|
||
.meta b { color: var(--ink); font-weight: 600; }
|
||
.toc-inline { display: none; flex-wrap: wrap; gap: 6px 14px; font-size: 14px; margin: 0 0 22px; }
|
||
@media (max-width: 1020px) { .toc-inline { display: flex; } }
|
||
|
||
.map { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; margin: 6px 0 26px; }
|
||
@media (max-width: 860px) { .map { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||
@media (max-width: 460px) { .map { grid-template-columns: minmax(0, 1fr); } }
|
||
.map a { background: var(--surface); padding: 14px 16px 16px; text-decoration: none; color: var(--ink); min-width: 0; display: block; }
|
||
.map a:hover { background: var(--accent-soft); }
|
||
.map b { display: block; font-family: var(--font-display); font-weight: 600; font-size: 17px; line-height: 1.25; margin-bottom: 4px; }
|
||
.map span { font-size: 13.5px; color: var(--muted); line-height: 1.4; display: block; }
|
||
|
||
section { padding-block: 40px 8px; border-top: 1px solid var(--line); }
|
||
h2 { font-family: var(--font-display); font-weight: 600; font-size: clamp(26px, 3.4vw, 32px); line-height: 1.12; margin: 0 0 12px; text-wrap: balance; display: flex; gap: 12px; align-items: baseline; }
|
||
h2 .num { font-family: var(--font-mono); font-weight: 400; font-size: 15px; color: var(--accent); }
|
||
h3 { font-size: 18px; font-weight: 600; margin: 30px 0 8px; text-wrap: balance; }
|
||
h3.today { color: var(--kill); }
|
||
h3.flow { color: var(--accent-ink); }
|
||
.prose { max-width: 720px; }
|
||
.prose p, .prose li { margin: 0 0 10px; }
|
||
.prose ul, .prose ol { padding-left: 22px; margin: 0 0 14px; }
|
||
|
||
figure { margin: 22px 0 28px; }
|
||
.panel { background: var(--surface); border: 1px solid var(--line); border-radius: 10px; padding: 18px; overflow-x: auto; }
|
||
figcaption { font-size: 14px; color: var(--muted); margin-top: 10px; max-width: 720px; line-height: 1.5; }
|
||
.legend { display: flex; flex-wrap: wrap; gap: 6px 20px; font-size: 13px; color: var(--muted); margin-top: 12px; }
|
||
.sw { display: inline-block; width: 13px; height: 13px; border-radius: 3px; border: 1.2px solid var(--line-strong); background: var(--surface); vertical-align: -2px; margin-right: 6px; }
|
||
.sw.a { background: var(--accent-soft); border-color: var(--accent); }
|
||
.sw.k { background: var(--kill-soft); border-color: var(--kill); }
|
||
.sw.n { border-style: dashed; }
|
||
.sw.d { border: 0; border-top: 2px dashed var(--muted); border-radius: 0; height: 0; width: 18px; vertical-align: 3px; }
|
||
|
||
/* Diagrams: page CSS styles the inline SVG through classes, so both themes follow the tokens. */
|
||
.dg { display: block; width: 100%; min-width: 760px; height: auto; font-family: var(--font-body); }
|
||
.dg text { fill: var(--ink); font-size: 12.5px; }
|
||
.dg .tt { font-weight: 600; font-size: 13px; }
|
||
.dg .ts { fill: var(--muted); font-size: 11.5px; }
|
||
.dg .tm { fill: var(--muted); font-family: var(--font-mono); font-size: 11px; }
|
||
.dg .th { fill: var(--muted); font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.08em; }
|
||
.dg .ta { fill: var(--accent-ink); }
|
||
.dg .tk { fill: var(--kill); }
|
||
.dg .box { fill: var(--surface); stroke: var(--line-strong); stroke-width: 1; }
|
||
.dg .box-a { fill: var(--accent-soft); stroke: var(--accent); stroke-width: 1.2; }
|
||
.dg .box-k { fill: var(--kill-soft); stroke: var(--kill); stroke-width: 1.2; }
|
||
.dg .grp { fill: none; stroke: var(--line-strong); stroke-width: 1; stroke-dasharray: 4 4; }
|
||
.dg .new { stroke-dasharray: 5 3; }
|
||
.dg .edge { fill: none; stroke: var(--muted); stroke-width: 1.4; }
|
||
.dg .edge-a { fill: none; stroke: var(--accent); stroke-width: 1.4; }
|
||
.dg .edge-k { fill: none; stroke: var(--kill); stroke-width: 1.4; }
|
||
.dg .ret { fill: none; stroke: var(--muted); stroke-width: 1.4; stroke-dasharray: 5 4; }
|
||
.dg .life { stroke: var(--line-strong); stroke-width: 1; stroke-dasharray: 3 4; }
|
||
.dg .sep { stroke: var(--line); stroke-width: 1; }
|
||
.dg .band { fill: var(--bg); }
|
||
.dg .ah { fill: var(--muted); }
|
||
.dg .ah-a { fill: var(--accent); }
|
||
.dg .ah-k { fill: var(--kill); }
|
||
|
||
.cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; margin: 18px 0 10px; }
|
||
@media (max-width: 640px) { .cards { grid-template-columns: minmax(0, 1fr); } }
|
||
.card { background: var(--surface); border: 1px solid var(--line); border-radius: 10px; padding: 16px 18px 12px; min-width: 0; }
|
||
.card h4 { margin: 0 0 2px; font-family: var(--font-display); font-weight: 600; font-size: 19px; }
|
||
.card .where { font-family: var(--font-mono); font-size: 12px; color: var(--muted); margin: 0 0 10px; }
|
||
.card ul { margin: 0; padding-left: 18px; font-size: 14.5px; }
|
||
.card li { margin: 0 0 6px; }
|
||
|
||
.tbl { overflow-x: auto; margin: 14px 0 22px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface); }
|
||
table { border-collapse: collapse; width: 100%; font-size: 14.5px; line-height: 1.45; }
|
||
th, td { text-align: left; vertical-align: top; padding: 10px 14px; border-bottom: 1px solid var(--line); }
|
||
tr:last-child td { border-bottom: 0; }
|
||
th { font-family: var(--font-mono); font-weight: 500; font-size: 11.5px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); white-space: nowrap; }
|
||
td:first-child { font-weight: 500; }
|
||
td.nw, td .nw { white-space: nowrap; }
|
||
td code { white-space: nowrap; }
|
||
.pill { display: inline-block; font-family: var(--font-mono); font-size: 11.5px; font-weight: 500; padding: 1px 8px; border-radius: 999px; border: 1px solid var(--line-strong); color: var(--muted); white-space: nowrap; }
|
||
.pill.a { border-color: var(--accent); color: var(--accent-ink); background: var(--accent-soft); }
|
||
.pill.k { border-color: var(--kill); color: var(--kill); background: var(--kill-soft); }
|
||
.pill.w { border-color: var(--warn); color: var(--warn); background: var(--warn-soft); }
|
||
.sevkey { display: flex; flex-wrap: wrap; gap: 6px 18px; font-size: 13.5px; color: var(--muted); margin: 6px 0 18px; }
|
||
|
||
.rule { border-left: 3px solid var(--kill); background: var(--kill-soft); padding: 12px 16px; margin: 18px 0; max-width: 720px; font-size: 15px; }
|
||
.rule b { color: var(--kill); }
|
||
.note { border-left: 3px solid var(--accent); background: var(--accent-soft); padding: 12px 16px; margin: 18px 0; max-width: 720px; font-size: 15px; }
|
||
|
||
ol.steps { counter-reset: s; list-style: none; padding: 0; margin: 12px 0 20px; max-width: 720px; }
|
||
ol.steps li { counter-increment: s; display: grid; grid-template-columns: 34px minmax(0, 1fr); gap: 4px; padding: 8px 0; border-bottom: 1px solid var(--line); font-size: 15px; }
|
||
ol.steps li::before { content: counter(s, decimal-leading-zero); font-family: var(--font-mono); font-size: 12.5px; color: var(--accent); padding-top: 2px; }
|
||
|
||
footer.page { margin-top: 48px; padding-top: 22px; border-top: 1px solid var(--line); font-size: 14px; color: var(--muted); max-width: 720px; }
|
||
</style>
|
||
|
||
<div class="wrap">
|
||
<nav class="toc" aria-label="Contents">
|
||
<p>Contents</p>
|
||
<a href="#summary"><span>00</span>Summary</a>
|
||
<a href="#architecture"><span>01</span>Target architecture</a>
|
||
<a href="#tenant"><span>02</span>Tenant flow</a>
|
||
<a href="#features"><span>03</span>Feature flow</a>
|
||
<a href="#rendering"><span>04</span>How the UI renders</a>
|
||
<a href="#development"><span>05</span>Development flow</a>
|
||
<a href="#deployment"><span>06</span>Deployment flow</a>
|
||
<a href="#ota"><span>07</span>OTA flow</a>
|
||
<a href="#manage"><span>08</span>Management flow</a>
|
||
<a href="#bank-update"><span>09</span>One bank's update</a>
|
||
<a href="#security"><span>10</span>Security</a>
|
||
<a href="#roadmap"><span>11</span>Roadmap</a>
|
||
<a href="#risks"><span>12</span>Risks</a>
|
||
<a href="#decisions"><span>13</span>Decisions needed</a>
|
||
</nav>
|
||
|
||
<main>
|
||
<header class="page">
|
||
<p class="eyebrow">Proposal · BankCBS mobile platform · 12 October 2026</p>
|
||
<h1>Multi-Bank Platform Proposal</h1>
|
||
<p class="lede">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.</p>
|
||
<div class="meta">
|
||
<span><b>Status</b> Draft for review</span>
|
||
<span><b>Audience</b> mobile, backend, QA, product, bank partners</span>
|
||
<span><b>Detail</b> <code>docs/MULTI_BANK_ARCHITECTURE.md</code>, <code>docs/BANK_FLOW_VARIANTS.md</code></span>
|
||
</div>
|
||
<nav class="toc-inline" aria-label="Contents">
|
||
<a href="#summary">Summary</a><a href="#architecture">Architecture</a><a href="#tenant">Tenant</a><a href="#features">Features</a><a href="#rendering">UI</a><a href="#development">Development</a><a href="#deployment">Deployment</a><a href="#ota">OTA</a><a href="#manage">Management</a><a href="#bank-update">One bank</a><a href="#security">Security</a><a href="#roadmap">Roadmap</a><a href="#risks">Risks</a><a href="#decisions">Decisions</a>
|
||
</nav>
|
||
<div class="map">
|
||
<a href="#tenant"><b>Tenant flow</b><span>How a bank is created, built into an app and checked on the phone</span></a>
|
||
<a href="#features"><b>Feature flow</b><span>How a feature goes from idea to one bank's customers</span></a>
|
||
<a href="#rendering"><b>How the UI renders</b><span>From launch to a screen that shows only what is allowed</span></a>
|
||
<a href="#development"><b>Development flow</b><span>Branches, automatic checks and rules for developers</span></a>
|
||
<a href="#deployment"><b>Deployment flow</b><span>Environments, store releases and version numbers</span></a>
|
||
<a href="#ota"><b>OTA flow</b><span>The weekly JS train, per-bank channels and rollback</span></a>
|
||
<a href="#manage"><b>Management flow</b><span>Who changes what, approvals, monitoring, incidents</span></a>
|
||
<a href="#bank-update"><b>One bank's update</b><span>Exactly how each kind of change reaches Bank B</span></a>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- 00 SUMMARY -->
|
||
<section id="summary">
|
||
<h2><span class="num">00</span>Summary</h2>
|
||
<div class="prose">
|
||
<p><b>Today</b> 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.</p>
|
||
<p><b>The proposal</b> keeps one codebase and adds:</p>
|
||
<ul>
|
||
<li>a <b>tenant pack</b> per bank, chosen when the app is built;</li>
|
||
<li><b>one backend codebase, deployed separately for each bank</b>, so each bank's data, keys and core-banking link stay isolated;</li>
|
||
<li>a <b>five-layer feature model</b>, so the bank and the server decide what each customer sees;</li>
|
||
<li><b>per-bank flows</b> through settings and server-driven security steps, never code branches;</li>
|
||
<li><b>five update lanes</b>, from a live switch in minutes to a store release in weeks, with UAT and production for every bank;</li>
|
||
<li><b>management</b> through an admin portal with two-person approval, an audit log and per-bank monitoring.</li>
|
||
</ul>
|
||
<p><b>Cost:</b> about six weeks for one mobile developer, plus backend work in parallel. The first week fixes the release blockers.</p>
|
||
<p><b>Decided (12 October 2026):</b> one backend codebase, deployed separately for each bank, the same way the app is built separately for each bank.</p>
|
||
<p><b>Still needed from you:</b> nine decisions in <a href="#decisions">section 13</a>. The three that matter most: where each bank's backend runs, each bank's own store accounts, and the server-driven security protocol.</p>
|
||
</div>
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Area</th><th>Today</th><th>Proposed</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Banks</td><td>One, hard-coded in about ten places</td><td>Any number; one tenant pack and one registry entry each</td></tr>
|
||
<tr><td>Backend</td><td>One server, its address edited by hand in source; ten hosts listed in the Android network config</td><td>One backend codebase, one isolated deployment per bank and environment</td></tr>
|
||
<tr><td>Feature control</td><td>Only what is compiled in; the server switch is not connected</td><td>Live switches per bank, rules per customer, cached for offline</td></tr>
|
||
<tr><td>Different flows per bank</td><td>Would need <code>if (bank)</code> code</td><td>Settings, server-driven security steps, journey variants</td></tr>
|
||
<tr><td>Builds and releases</td><td>Manual, debug-signed, no checks</td><td>CI checks on every change, signed builds per bank and environment</td></tr>
|
||
<tr><td>Bug fixes</td><td>Store release only</td><td>OTA per bank in hours, with UAT sign-off and rollback</td></tr>
|
||
<tr><td>Safety net</td><td>No error boundary, no crash data, no forced update</td><td>Error boundaries, crash reporting per bank, auto-rollback, forced update</td></tr>
|
||
<tr><td>Operations</td><td>None</td><td>Admin portal, two-person approval, audit log, per-bank dashboards</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="sevkey">
|
||
<span><span class="pill k">Blocker</span> must be fixed before customers get a build, or before a second bank</span>
|
||
<span><span class="pill w">High</span> fix in the first phases</span>
|
||
<span><span class="pill">Medium</span> fix as we go</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 01 ARCHITECTURE -->
|
||
<section id="architecture">
|
||
<h2><span class="num">01</span>Target architecture</h2>
|
||
<div class="prose">
|
||
<ul>
|
||
<li><b>One codebase.</b> Bank differences live in data (tenant packs and server config), never in <code>if (bank)</code> branches.</li>
|
||
<li><b>The server decides access and security.</b> The app follows the server and never widens what it allows.</li>
|
||
<li><b>Fail closed.</b> An unknown feature key, step or config means off or stop.</li>
|
||
<li><b>Every bank is isolated:</b> its own app build, backend deployment, data, keys, OTA channels, store listing and sign-off.</li>
|
||
<li><b>Every change has a lane.</b> Use the fastest lane the change allows.</li>
|
||
<li><b>No change reaches a bank's customers</b> without that bank's UAT sign-off.</li>
|
||
</ul>
|
||
</div>
|
||
<figure>
|
||
<div class="panel">
|
||
<svg class="dg" viewBox="0 0 1000 420" role="img" aria-label="Platform overview: one repository with shared core and tenant packs feeds a release script that produces a store build and an OTA update per bank; each installed bank app talks only to its own bank's backend deployment; a central platform control plane manages the backends and receives metrics without customer data.">
|
||
<defs>
|
||
<marker id="d1-m" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah" d="M0 0L10 5L0 10z"/></marker>
|
||
<marker id="d1-ma" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah-a" d="M0 0L10 5L0 10z"/></marker>
|
||
</defs>
|
||
<rect class="grp" x="20" y="130" width="230" height="176" rx="8"/>
|
||
<text class="tt" x="36" y="152">One repository</text>
|
||
<rect class="box" x="36" y="166" width="198" height="52" rx="6"/>
|
||
<text class="tt" x="50" y="188">Shared core</text>
|
||
<text class="ts" x="50" y="206">screens, flows, engines</text>
|
||
<rect class="box-a" x="36" y="232" width="198" height="56" rx="6"/>
|
||
<text class="tt ta" x="50" y="255">Tenant packs</text>
|
||
<text class="ts" x="50" y="274">one folder per bank</text>
|
||
<line class="edge" x1="135" y1="306" x2="135" y2="327" marker-end="url(#d1-m)"/>
|
||
<text class="ts" x="143" y="321">merge</text>
|
||
<rect class="box" x="36" y="330" width="198" height="70" rx="6"/>
|
||
<text class="tt" x="50" y="352">Release script / CI</text>
|
||
<text class="ts" x="50" y="370">checks, then builds</text>
|
||
<text class="ts" x="50" y="387">every bank in a loop</text>
|
||
<path class="edge" d="M234 342 H296 V190 H327" marker-end="url(#d1-m)"/>
|
||
<text class="ts" x="302" y="268">native change</text>
|
||
<line class="edge" x1="234" y1="372" x2="327" y2="372" marker-end="url(#d1-m)"/>
|
||
<text class="ts" x="281" y="390" text-anchor="middle">JS change</text>
|
||
<rect class="box-a" x="330" y="160" width="210" height="60" rx="6"/>
|
||
<text class="tt ta" x="344" y="185">Store build per bank</text>
|
||
<text class="ts" x="344" y="204">in the bank's own store account</text>
|
||
<rect class="box-a" x="330" y="342" width="210" height="60" rx="6"/>
|
||
<text class="tt ta" x="344" y="367">OTA update per bank</text>
|
||
<text class="ts" x="344" y="386">channel per bank: uat, prod</text>
|
||
<line class="edge" x1="540" y1="190" x2="617" y2="190" marker-end="url(#d1-m)"/>
|
||
<text class="ts" x="579" y="182" text-anchor="middle">installs</text>
|
||
<line class="edge" x1="540" y1="372" x2="617" y2="372" marker-end="url(#d1-m)"/>
|
||
<text class="ts" x="579" y="364" text-anchor="middle">updates JS</text>
|
||
<rect class="grp" x="620" y="150" width="160" height="250" rx="8"/>
|
||
<text class="tt" x="636" y="172">Customer phones</text>
|
||
<rect class="box-a" x="636" y="186" width="128" height="44" rx="6"/>
|
||
<text class="tt ta" x="700" y="213" text-anchor="middle">Bank A app</text>
|
||
<rect class="box-a" x="636" y="244" width="128" height="44" rx="6"/>
|
||
<text class="tt ta" x="700" y="271" text-anchor="middle">Bank B app</text>
|
||
<rect class="box-a" x="636" y="302" width="128" height="44" rx="6"/>
|
||
<text class="tt ta" x="700" y="329" text-anchor="middle">Bank C app</text>
|
||
<text class="ts" x="636" y="378">one listing per bank</text>
|
||
<line class="edge-a" x1="764" y1="208" x2="829" y2="208" marker-end="url(#d1-ma)"/>
|
||
<text class="ts" x="796" y="200" text-anchor="middle">HTTPS</text>
|
||
<line class="edge-a" x1="764" y1="266" x2="829" y2="266" marker-end="url(#d1-ma)"/>
|
||
<line class="edge-a" x1="764" y1="324" x2="829" y2="324" marker-end="url(#d1-ma)"/>
|
||
<rect class="grp" x="820" y="150" width="170" height="250" rx="8"/>
|
||
<text class="tt" x="832" y="172">Per-bank backends</text>
|
||
<rect class="box-a" x="832" y="186" width="146" height="44" rx="6"/>
|
||
<text class="tt ta" x="844" y="205">Bank A backend</text>
|
||
<text class="ts" x="844" y="221">own data, own CBS</text>
|
||
<rect class="box-a" x="832" y="244" width="146" height="44" rx="6"/>
|
||
<text class="tt ta" x="844" y="263">Bank B backend</text>
|
||
<text class="ts" x="844" y="279">own data, own CBS</text>
|
||
<rect class="box-a" x="832" y="302" width="146" height="44" rx="6"/>
|
||
<text class="tt ta" x="844" y="321">Bank C backend</text>
|
||
<text class="ts" x="844" y="337">own data, own CBS</text>
|
||
<text class="ts" x="832" y="368">each: config, flows,</text>
|
||
<text class="ts" x="832" y="386">APIs, admin, audit</text>
|
||
<rect class="box" x="620" y="20" width="370" height="56" rx="6"/>
|
||
<text class="tt" x="636" y="43">Platform control plane (ours)</text>
|
||
<text class="ts" x="636" y="62">ops console · catalog · monitoring · no customer data</text>
|
||
<line class="edge" x1="905" y1="76" x2="905" y2="147" marker-end="url(#d1-m)"/>
|
||
<text class="ts" x="912" y="118">manages</text>
|
||
<line class="ret" x1="860" y1="149" x2="860" y2="79" marker-end="url(#d1-m)"/>
|
||
<text class="ts" x="853" y="118" text-anchor="end">metrics</text>
|
||
</svg>
|
||
<div class="legend"><span><i class="sw a"></i>Exists once per bank</span><span><i class="sw"></i>Shared by every bank</span></div>
|
||
</div>
|
||
<figcaption>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.</figcaption>
|
||
</figure>
|
||
|
||
<h3>Inside the app</h3>
|
||
<figure>
|
||
<div class="panel">
|
||
<svg class="dg" viewBox="0 0 1000 380" role="img" aria-label="App layers from top to bottom: app shell, navigation, modules, flows, a decision layer of feature engine, bank settings and strategies, the tenant pack, and services. The tenant pack enters at build time; feature config and the flow engine feed the decision and flows layers at runtime.">
|
||
<defs>
|
||
<marker id="d3-m" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah" d="M0 0L10 5L0 10z"/></marker>
|
||
<marker id="d3-ma" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah-a" d="M0 0L10 5L0 10z"/></marker>
|
||
</defs>
|
||
<rect class="box" x="220" y="20" width="620" height="40" rx="6"/>
|
||
<text class="tt" x="236" y="45">App shell</text>
|
||
<text class="ts" x="410" y="45">providers · error boundary · bootstrap · tenant guard</text>
|
||
<rect class="box" x="220" y="70" width="620" height="40" rx="6"/>
|
||
<text class="tt" x="236" y="95">Navigation</text>
|
||
<text class="ts" x="410" y="95">a screen exists only if its feature is on</text>
|
||
<rect class="box" x="220" y="120" width="620" height="40" rx="6"/>
|
||
<text class="tt" x="236" y="145">Modules</text>
|
||
<text class="ts" x="410" y="145">home · payments · services · loans · cards · agent</text>
|
||
<rect class="box new" x="220" y="170" width="620" height="40" rx="6"/>
|
||
<text class="tt" x="236" y="195">Flows</text>
|
||
<text class="ts" x="410" y="195">challenge runner · journey runner</text>
|
||
<rect class="box" x="220" y="220" width="196" height="40" rx="6"/>
|
||
<text class="tt" x="318" y="245" text-anchor="middle">Feature engine</text>
|
||
<rect class="box new" x="432" y="220" width="196" height="40" rx="6"/>
|
||
<text class="tt" x="530" y="245" text-anchor="middle">Bank settings</text>
|
||
<rect class="box new" x="644" y="220" width="196" height="40" rx="6"/>
|
||
<text class="tt" x="742" y="245" text-anchor="middle">Strategies & slots</text>
|
||
<rect class="box-a new" x="220" y="270" width="620" height="40" rx="6"/>
|
||
<text class="tt ta" x="236" y="295">Tenant pack (@tenant)</text>
|
||
<text class="ts" x="410" y="295">this bank's identity and defaults</text>
|
||
<rect class="box" x="220" y="320" width="620" height="40" rx="6"/>
|
||
<text class="tt" x="236" y="345">Services</text>
|
||
<text class="ts" x="410" y="345">API client · security · storage</text>
|
||
<rect class="box-a" x="20" y="270" width="150" height="40" rx="6"/>
|
||
<text class="tm ta" x="95" y="294" text-anchor="middle">tenants/bank-a</text>
|
||
<line class="edge-a" x1="170" y1="290" x2="217" y2="290" marker-end="url(#d3-ma)"/>
|
||
<text class="ts" x="194" y="282" text-anchor="middle">build</text>
|
||
<rect class="box" x="870" y="170" width="120" height="40" rx="6"/>
|
||
<text class="tt" x="930" y="195" text-anchor="middle">Flow engine</text>
|
||
<line class="edge" x1="870" y1="190" x2="843" y2="190" marker-end="url(#d3-m)"/>
|
||
<rect class="box" x="870" y="220" width="120" height="40" rx="6"/>
|
||
<text class="tm" x="930" y="244" text-anchor="middle">/features/config</text>
|
||
<line class="edge" x1="870" y1="240" x2="843" y2="240" marker-end="url(#d3-m)"/>
|
||
<text class="ts" x="930" y="282" text-anchor="middle">runtime, from</text>
|
||
<text class="ts" x="930" y="298" text-anchor="middle">the server</text>
|
||
</svg>
|
||
<div class="legend"><span><i class="sw n"></i>New in this design</span><span><i class="sw a"></i>Differs per bank</span></div>
|
||
</div>
|
||
<figcaption>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.</figcaption>
|
||
</figure>
|
||
|
||
<h3>Backend: one codebase, one deployment per bank</h3>
|
||
<div class="prose"><p>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.</p></div>
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Component</th><th>Runs</th><th>Job</th><th>Status</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Mobile and business APIs</td><td>In each bank's backend</td><td>Existing CBS APIs, plus an entitlement check on every call (<code>FEATURE_NOT_AVAILABLE</code>)</td><td><span class="pill">extend</span></td></tr>
|
||
<tr><td>Config service</td><td>In each bank's backend</td><td>The bank's switches, settings and journey variants; per-customer entitlements after sign-in; served at <code>/features/config</code></td><td><span class="pill">extend</span> the app calls it; the server side is not built yet</td></tr>
|
||
<tr><td>Flow engine</td><td>In each bank's backend</td><td>The bank's security policy; replies one step at a time (TPIN, OTP, biometric)</td><td><span class="pill a">new</span></td></tr>
|
||
<tr><td>Bank admin portal</td><td>In each bank's backend</td><td>Bank staff change their own bank, with two-person approval</td><td><span class="pill a">new</span></td></tr>
|
||
<tr><td>Audit log</td><td>In each bank's backend</td><td>Every change and every ops action, kept with the bank's own data</td><td><span class="pill a">new</span></td></tr>
|
||
<tr><td>Database, key pair, core-banking link</td><td>In each bank's backend</td><td>The bank's customers and sessions, its MPIN/TPIN key pair, its connection to its core banking system</td><td><span class="pill">per bank</span></td></tr>
|
||
<tr><td>Ops console</td><td>Central</td><td>Onboarding, security policies, emergency kill, support lookup; reaches each bank's backend over a private, audited link</td><td><span class="pill a">new</span></td></tr>
|
||
<tr><td>Feature catalog</td><td>Central</td><td>Every feature key the app knows; each backend imports it when it is deployed</td><td><span class="pill a">new</span></td></tr>
|
||
<tr><td>Hot Updater server</td><td>Central</td><td>OTA bundles and channels, self-hosted</td><td><span class="pill a">new</span></td></tr>
|
||
<tr><td>Monitoring</td><td>Central</td><td>Crash reports, metrics and logs without customer data, tagged by bank, bundle and version</td><td><span class="pill a">new</span></td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h3>Environments</h3>
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Environment</th><th>Used by</th><th>Backend</th><th>Build</th><th>OTA channel</th><th>Mock data</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Dev</td><td>Developers</td><td>Shared dev backend, or the local mock adapter</td><td><code>bankADevDebug</code></td><td>none (Metro)</td><td>Allowed, debug builds only</td></tr>
|
||
<tr><td>UAT</td><td>Our QA, the bank's testers</td><td>Bank A's UAT backend</td><td><code>bankAUatRelease</code></td><td><code>bank-a-uat</code></td><td>Never</td></tr>
|
||
<tr><td>Prod</td><td>Customers</td><td>Bank A's production backend</td><td><code>bankAProdRelease</code></td><td><code>bank-a-prod</code></td><td>Never</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="note"><b>No more hand-edited hosts.</b> Each bank's backend address and certificate pins, per environment, live in that bank's tenant pack. The build picks them from the bank and the environment, so nobody changes a host in source code again.</div>
|
||
</section>
|
||
|
||
<!-- 02 TENANT -->
|
||
<section id="tenant">
|
||
<h2><span class="num">02</span>Tenant flow</h2>
|
||
<h3 class="today">Today: what is wrong</h3>
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Severity</th><th>Flaw</th><th>Where</th><th>Effect</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><span class="pill k">Blocker</span></td><td>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</td><td><code>config/env.ts:34</code>, <code>config/appConfig.ts:4</code>, <code>android/app/build.gradle:82</code>, <code>theme/palette.ts:193</code>, <code>BrandLogo:6</code></td><td>A second bank means editing shared code</td></tr>
|
||
<tr><td><span class="pill w">High</span></td><td>The backend address is edited by hand in source; the Android network config lists ten hosts</td><td><code>config/env.ts:26</code>, <code>network_security_config.xml</code></td><td>With a backend per bank, a build can easily talk to the wrong bank's server</td></tr>
|
||
<tr><td><span class="pill w">High</span></td><td>Nothing checks that the app reached its own bank's backend; the config request does not even name the bank</td><td><code>features/featureRemoteConfig.ts:113</code></td><td>A wrong address would show another bank's switches without any error</td></tr>
|
||
<tr><td><span class="pill w">High</span></td><td>No per-bank defaults; the build-time layer is empty</td><td><code>features/featureConfig.ts:311</code></td><td>A fresh install offline shows global defaults, maybe features this bank lacks</td></tr>
|
||
<tr><td><span class="pill">Medium</span></td><td>No list of banks</td><td>none</td><td>Nothing ties an app ID, an org, a store listing and an OTA channel together</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h3 class="flow">Proposed flow</h3>
|
||
<div class="prose"><p>A tenant is one bank. It exists in four places, tied together by one id such as <code>bank-b</code>.</p></div>
|
||
<div class="cards">
|
||
<div class="card">
|
||
<h4>Code</h4>
|
||
<p class="where">tenants/bank-b/</p>
|
||
<ul>
|
||
<li>Identity: org id, name, logo, brand colours, wording</li>
|
||
<li>Feature defaults for the first launch and offline</li>
|
||
<li>Settings, flow choices, rare strategies or slot overrides</li>
|
||
</ul>
|
||
</div>
|
||
<div class="card">
|
||
<h4>Native shell</h4>
|
||
<p class="where">Android flavor bankB · iOS scheme BankB</p>
|
||
<ul>
|
||
<li>App ID, launcher icon, home-screen name</li>
|
||
<li>Upload signing key, kept in CI secrets</li>
|
||
<li>OTA channel baked in: <code>bank-b-prod</code></li>
|
||
</ul>
|
||
</div>
|
||
<div class="card">
|
||
<h4>Backend</h4>
|
||
<p class="where">Bank B's own deployment · UAT and prod</p>
|
||
<ul>
|
||
<li>Its own database, key pair and core-banking connection</li>
|
||
<li>Live switches, settings, security policy, customer rules</li>
|
||
<li>Admin portal, approvals, audit log</li>
|
||
</ul>
|
||
</div>
|
||
<div class="card">
|
||
<h4>Delivery</h4>
|
||
<p class="where">tenants/registry.json · stores · Hot Updater</p>
|
||
<ul>
|
||
<li>Store listing in Bank B's own developer account</li>
|
||
<li>OTA channels <code>bank-b-uat</code> and <code>bank-b-prod</code></li>
|
||
<li>One registry entry: flavor, app IDs, version</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<figure>
|
||
<div class="panel">
|
||
<svg class="dg" viewBox="0 0 1000 160" role="img" aria-label="How the tenant is chosen and checked: the release script sets TENANT=bank-b; Metro bundles only Bank B's pack; the Gradle flavor gives Bank B's app ID, icon and channel; on the phone the tenant guard compares the bundle's bank with the binary's channel and refuses to start on a mismatch.">
|
||
<defs>
|
||
<marker id="n2-m" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah" d="M0 0L10 5L0 10z"/></marker>
|
||
</defs>
|
||
<text class="th" x="20" y="30">BUILD TIME</text>
|
||
<line class="sep" x1="20" y1="40" x2="585" y2="40"/>
|
||
<text class="th" x="605" y="30">ON THE PHONE</text>
|
||
<line class="sep" x1="605" y1="40" x2="980" y2="40"/>
|
||
|
||
<rect class="box" x="20" y="54" width="170" height="86" rx="6"/>
|
||
<text class="tt" x="34" y="78">release.js</text>
|
||
<text class="tm" x="34" y="99">TENANT=bank-b</text>
|
||
<text class="tm" x="34" y="119">APP_ENV=prod</text>
|
||
<line class="edge" x1="190" y1="97" x2="213" y2="97" marker-end="url(#n2-m)"/>
|
||
|
||
<rect class="box" x="215" y="54" width="175" height="86" rx="6"/>
|
||
<text class="tt" x="229" y="78">Metro</text>
|
||
<text class="tm" x="229" y="99">@tenant → bank-b</text>
|
||
<text class="ts" x="229" y="119">only Bank B's pack</text>
|
||
<line class="edge" x1="390" y1="97" x2="408" y2="97" marker-end="url(#n2-m)"/>
|
||
|
||
<rect class="box-a" x="410" y="54" width="175" height="86" rx="6"/>
|
||
<text class="tt ta" x="424" y="78">Gradle flavor</text>
|
||
<text class="tm" x="424" y="99">bankBProdRelease</text>
|
||
<text class="ts" x="424" y="119">app ID, icon, channel</text>
|
||
<line class="edge" x1="585" y1="97" x2="603" y2="97" marker-end="url(#n2-m)"/>
|
||
|
||
<rect class="box-a" x="605" y="54" width="175" height="86" rx="6"/>
|
||
<text class="tt ta" x="619" y="78">Bank B's phone</text>
|
||
<text class="tm" x="619" y="99">bundle: bank-b</text>
|
||
<text class="tm" x="619" y="119">binary: bank-b-prod</text>
|
||
<line class="edge" x1="780" y1="97" x2="798" y2="97" marker-end="url(#n2-m)"/>
|
||
|
||
<rect class="box" x="800" y="54" width="180" height="86" rx="6"/>
|
||
<text class="tt" x="814" y="78">Tenant guard</text>
|
||
<text class="ta" x="814" y="99">match → app starts</text>
|
||
<text class="tk" x="814" y="119">mismatch → refuse</text>
|
||
</svg>
|
||
</div>
|
||
<figcaption>The bank is fixed at build time, so Bank B's app never contains Bank A's logo, config or server address. If a wrong OTA bundle ever lands on a phone, the guard refuses it before the first screen and Hot Updater rolls back. The app also checks the org id in the first config reply, so a pack pointing at the wrong bank's backend stops at once.</figcaption>
|
||
</figure>
|
||
|
||
<div class="prose"><p>The pack is a typed file. The build fails if a field is missing.</p></div>
|
||
<pre><code>// 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;</code></pre>
|
||
|
||
<h3>Tenant lifecycle</h3>
|
||
<figure>
|
||
<div class="panel">
|
||
<svg class="dg" viewBox="0 0 1000 200" role="img" aria-label="Bank lifecycle: agree scope, set up, UAT, go live, operate, change. Every change loops back to UAT.">
|
||
<defs>
|
||
<marker id="d7-m" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah" d="M0 0L10 5L0 10z"/></marker>
|
||
<marker id="d7-ma" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah-a" d="M0 0L10 5L0 10z"/></marker>
|
||
</defs>
|
||
<rect class="box" x="20" y="30" width="140" height="84" rx="6"/>
|
||
<text class="tt" x="34" y="54">1 · Agree scope</text>
|
||
<text class="ts" x="34" y="74">contract, features</text>
|
||
<text class="tm" x="34" y="94">bank + us</text>
|
||
<line class="edge" x1="160" y1="72" x2="180" y2="72" marker-end="url(#d7-m)"/>
|
||
<rect class="box" x="182" y="30" width="140" height="84" rx="6"/>
|
||
<text class="tt" x="196" y="54">2 · Set up</text>
|
||
<text class="ts" x="196" y="74">pack, flavor, org</text>
|
||
<text class="tm" x="196" y="94">us + DevOps</text>
|
||
<line class="edge" x1="322" y1="72" x2="342" y2="72" marker-end="url(#d7-m)"/>
|
||
<rect class="box-a" x="344" y="30" width="140" height="84" rx="6"/>
|
||
<text class="tt ta" x="358" y="54">3 · UAT</text>
|
||
<text class="ts" x="358" y="74">bank tests UAT</text>
|
||
<text class="tm" x="358" y="94">bank</text>
|
||
<line class="edge" x1="484" y1="72" x2="504" y2="72" marker-end="url(#d7-m)"/>
|
||
<rect class="box" x="506" y="30" width="140" height="84" rx="6"/>
|
||
<text class="tt" x="520" y="54">4 · Go live</text>
|
||
<text class="ts" x="520" y="74">store + prod OTA</text>
|
||
<text class="tm" x="520" y="94">bank + us</text>
|
||
<line class="edge" x1="646" y1="72" x2="666" y2="72" marker-end="url(#d7-m)"/>
|
||
<rect class="box" x="668" y="30" width="140" height="84" rx="6"/>
|
||
<text class="tt" x="682" y="54">5 · Operate</text>
|
||
<text class="ts" x="682" y="74">monitor, toggle</text>
|
||
<text class="tm" x="682" y="94">bank + us</text>
|
||
<line class="edge" x1="808" y1="72" x2="828" y2="72" marker-end="url(#d7-m)"/>
|
||
<rect class="box" x="830" y="30" width="140" height="84" rx="6"/>
|
||
<text class="tt" x="844" y="54">6 · Change</text>
|
||
<text class="ts" x="844" y="74">features, flows</text>
|
||
<text class="tm" x="844" y="94">bank + us</text>
|
||
<path class="edge-a" d="M900 114 V160 H414 V118" marker-end="url(#d7-ma)"/>
|
||
<text class="ts" x="657" y="180" text-anchor="middle">every change goes back through UAT</text>
|
||
</svg>
|
||
</div>
|
||
<figcaption>Offboarding runs the lifecycle in reverse (see the last list in this section).</figcaption>
|
||
</figure>
|
||
|
||
<h3>Onboarding a bank</h3>
|
||
<ol class="steps">
|
||
<li>Copy <code>tenants/_template</code> to <code>tenants/bank-c</code>; fill in identity, brand colours, logo, features, settings, backend addresses and certificate pins.</li>
|
||
<li>Run the contrast check with Bank C's colours and adjust until every text passes.</li>
|
||
<li>Add Bank C to <code>tenants/registry.json</code>; its Android flavors and CI jobs appear automatically.</li>
|
||
<li>Add launcher icons and the iOS configuration for Bank C.</li>
|
||
<li>DevOps stores Bank C's signing key in CI secrets.</li>
|
||
<li>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.</li>
|
||
<li>Ops configures both backends: feature switches, customer rules, security policy, admin users.</li>
|
||
<li>The bank creates its store listing and gives our team access.</li>
|
||
<li>Build to <code>bank-c-uat</code>; the bank tests and signs off; release to production.</li>
|
||
</ol>
|
||
<div class="prose">
|
||
<p><b>Offboarding:</b> 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.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 03 FEATURES -->
|
||
<section id="features">
|
||
<h2><span class="num">03</span>Feature flow</h2>
|
||
<h3 class="today">Today: what is wrong</h3>
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Severity</th><th>Flaw</th><th>Where</th><th>Effect</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><span class="pill k">Blocker</span></td><td>The kill switch is not connected. Startup passes <code>useMock: true</code>, so the server is never asked, and an empty config is cached</td><td><code>featureRemoteConfig.ts:150</code>, <code>hydrateFeatures.ts:28</code>, <code>env.ts:40</code></td><td>No feature can be switched off remotely</td></tr>
|
||
<tr><td><span class="pill w">High</span></td><td>The customer's flags arrive after the app is already on screen</td><td><code>useAppBootstrap.ts:76</code>, <code>RootNavigator.tsx:89</code></td><td>A tab the customer must not have can flash</td></tr>
|
||
<tr><td><span class="pill w">High</span></td><td>Config is refreshed only at launch and sign-in</td><td><code>hydrateFeatures.ts:28</code></td><td>A kill reaches active users late</td></tr>
|
||
<tr><td><span class="pill w">High</span></td><td>The device layer can switch features on in release builds</td><td><code>adminFeatures.ts:38</code></td><td>A staff phone can show features the bank does not have</td></tr>
|
||
<tr><td><span class="pill w">High</span></td><td>The cached config has no version check</td><td><code>featureRemoteConfig.ts:165</code></td><td>An old or empty reply silently undoes a kill</td></tr>
|
||
<tr><td><span class="pill">Medium</span></td><td>New keys reach every bank; module children are switched off one by one</td><td><code>featureConfig.ts:120</code>, <code>:205</code></td><td>An OTA can leak a feature; turning on <code>loans</code> does not turn on the module</td></tr>
|
||
<tr><td><span class="pill">Medium</span></td><td>Different flows per bank would need code branches: five hand-written step machines, OTP length copied in four places</td><td><code>PaymentConfirmationScreen.tsx:40</code>, <code>TpinCreateFlow.tsx:30</code></td><td>Every bank variation becomes <code>if (bank)</code></td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h3 class="flow">Proposed flow: who gets which feature</h3>
|
||
<figure>
|
||
<div class="panel">
|
||
<svg class="dg" viewBox="0 0 1000 222" role="img" aria-label="Five feature layers in priority order: product default, bank pack, bank live, session, local device. A false from bank live or session is final; no later layer can switch it back on.">
|
||
<defs>
|
||
<marker id="d4-m" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah" d="M0 0L10 5L0 10z"/></marker>
|
||
<marker id="d4-mk" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah-k" d="M0 0L10 5L0 10z"/></marker>
|
||
</defs>
|
||
<text class="ts" x="20" y="16">Later layers override earlier ones</text>
|
||
<line class="edge" x1="20" y1="26" x2="928" y2="26" marker-end="url(#d4-m)"/>
|
||
<rect class="box" x="20" y="44" width="170" height="84" rx="6"/>
|
||
<text class="tt" x="34" y="68">Product default</text>
|
||
<text class="ts" x="34" y="88">set by developers</text>
|
||
<text class="ts" x="34" y="106">compiled in</text>
|
||
<rect class="box-a" x="205" y="44" width="170" height="84" rx="6"/>
|
||
<text class="tt ta" x="219" y="68">Bank pack</text>
|
||
<text class="ts" x="219" y="88">set by mobile team</text>
|
||
<text class="ts" x="219" y="106">compiled per bank</text>
|
||
<rect class="box-k" x="390" y="44" width="170" height="84" rx="6"/>
|
||
<text class="tt tk" x="404" y="68">Bank live</text>
|
||
<text class="ts" x="404" y="88">set by bank admin</text>
|
||
<text class="ts" x="404" y="106">fetched, cached</text>
|
||
<rect class="box-k" x="575" y="44" width="170" height="84" rx="6"/>
|
||
<text class="tt tk" x="589" y="68">Session</text>
|
||
<text class="ts" x="589" y="88">set by server rules</text>
|
||
<text class="ts" x="589" y="106">role + this customer</text>
|
||
<rect class="box" x="760" y="44" width="170" height="84" rx="6"/>
|
||
<text class="tt" x="774" y="68">Local device</text>
|
||
<text class="ts" x="774" y="88">set by QA or staff</text>
|
||
<text class="ts" x="774" y="106">release: off only</text>
|
||
<path class="edge-k" d="M475 128 V150 M660 128 V150 M475 150 H660"/>
|
||
<line class="edge-k" x1="567" y1="150" x2="567" y2="164" marker-end="url(#d4-mk)"/>
|
||
<rect class="box-k" x="330" y="166" width="475" height="36" rx="6"/>
|
||
<text class="tk" x="567" y="189" text-anchor="middle">A false here is final. No layer to its right can switch it back on.</text>
|
||
</svg>
|
||
<div class="legend"><span><i class="sw a"></i>Differs per bank</span><span><i class="sw k"></i>Its "off" is final (kill switch)</span></div>
|
||
</div>
|
||
<figcaption>Two more rules: a feature is on only if every parent is on (<code>loans</code> off hides all of <code>loans.*</code>), and an unknown key is always off.</figcaption>
|
||
</figure>
|
||
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Feature</th><th>Bank A · customer X (KYC pending)</th><th>Bank A · customer Y</th><th>Bank B · anyone</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>loans</code></td><td><span class="pill a">on</span> bank pack</td><td><span class="pill a">on</span> bank pack</td><td><span class="pill">off</span> bank pack</td></tr>
|
||
<tr><td><code>loans.apply</code></td><td><span class="pill k">off</span> session rule</td><td><span class="pill a">on</span></td><td><span class="pill">off</span> parent is off</td></tr>
|
||
<tr><td><code>cards</code> (Bank A outage)</td><td><span class="pill k">off</span> bank live</td><td><span class="pill k">off</span> bank live</td><td><span class="pill">off</span> default</td></tr>
|
||
<tr><td>Staff phone turns <code>cards</code> on</td><td><span class="pill k">off</span> still killed</td><td><span class="pill k">off</span> still killed</td><td><span class="pill">off</span> release ignores it</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h3 class="flow">Proposed flow: a feature's life</h3>
|
||
<figure>
|
||
<div class="panel">
|
||
<svg class="dg" viewBox="0 0 1000 160" role="img" aria-label="A feature's life in seven steps: decide, build with the key off, ship dark by OTA to every bank, test in one bank's UAT, go live in that bank, narrow with customer rules, retire the flag.">
|
||
<defs>
|
||
<marker id="n3-m" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah" d="M0 0L10 5L0 10z"/></marker>
|
||
</defs>
|
||
<text class="th" x="20" y="30">PLAN</text>
|
||
<line class="sep" x1="20" y1="40" x2="144" y2="40"/>
|
||
<text class="th" x="159" y="30">CODE · EVERY BANK</text>
|
||
<line class="sep" x1="159" y1="40" x2="422" y2="40"/>
|
||
<text class="th" x="437" y="30">SERVER · ONE BANK AT A TIME</text>
|
||
<line class="sep" x1="437" y1="40" x2="839" y2="40"/>
|
||
<text class="th" x="854" y="30">CODE</text>
|
||
<line class="sep" x1="854" y1="40" x2="978" y2="40"/>
|
||
|
||
<rect class="box" x="20" y="52" width="124" height="84" rx="6"/>
|
||
<text class="tt" x="32" y="76">1 · Decide</text>
|
||
<text class="ts" x="32" y="97">rung + key name</text>
|
||
<text class="tm" x="32" y="117">product, bank</text>
|
||
<line class="edge" x1="144" y1="94" x2="157" y2="94" marker-end="url(#n3-m)"/>
|
||
<rect class="box" x="159" y="52" width="124" height="84" rx="6"/>
|
||
<text class="tt" x="171" y="76">2 · Build</text>
|
||
<text class="ts" x="171" y="97">key ships off</text>
|
||
<text class="tm" x="171" y="117">developers</text>
|
||
<line class="edge" x1="283" y1="94" x2="296" y2="94" marker-end="url(#n3-m)"/>
|
||
<rect class="box" x="298" y="52" width="124" height="84" rx="6"/>
|
||
<text class="tt" x="310" y="76">3 · Ship dark</text>
|
||
<text class="ts" x="310" y="97">OTA, hidden</text>
|
||
<text class="tm" x="310" y="117">release mgr</text>
|
||
<line class="edge" x1="422" y1="94" x2="435" y2="94" marker-end="url(#n3-m)"/>
|
||
<rect class="box-a" x="437" y="52" width="124" height="84" rx="6"/>
|
||
<text class="tt ta" x="449" y="76">4 · Test</text>
|
||
<text class="ts" x="449" y="97">on in bank UAT</text>
|
||
<text class="tm" x="449" y="117">ops, bank QA</text>
|
||
<line class="edge" x1="561" y1="94" x2="574" y2="94" marker-end="url(#n3-m)"/>
|
||
<rect class="box-a" x="576" y="52" width="124" height="84" rx="6"/>
|
||
<text class="tt ta" x="588" y="76">5 · Go live</text>
|
||
<text class="ts" x="588" y="97">on in bank prod</text>
|
||
<text class="tm" x="588" y="117">bank admin</text>
|
||
<line class="edge" x1="700" y1="94" x2="713" y2="94" marker-end="url(#n3-m)"/>
|
||
<rect class="box-a" x="715" y="52" width="124" height="84" rx="6"/>
|
||
<text class="tt ta" x="727" y="76">6 · Narrow</text>
|
||
<text class="ts" x="727" y="97">customer rules</text>
|
||
<text class="tm" x="727" y="117">bank decides</text>
|
||
<line class="edge" x1="839" y1="94" x2="852" y2="94" marker-end="url(#n3-m)"/>
|
||
<rect class="box" x="854" y="52" width="124" height="84" rx="6"/>
|
||
<text class="tt" x="866" y="76">7 · Retire</text>
|
||
<text class="ts" x="866" y="97">remove leaf flag</text>
|
||
<text class="tm" x="866" y="117">developers</text>
|
||
</svg>
|
||
<div class="legend"><span><i class="sw a"></i>Done on the server, per bank, with no app release</span></div>
|
||
</div>
|
||
<figcaption>Steps 4 to 6 repeat for each bank that wants the feature, on that bank's schedule. Module roots (<code>loans</code>, <code>cards</code>) and money actions keep their switch for ever as kill switches; leaf flags are removed once fully on.</figcaption>
|
||
</figure>
|
||
<div class="prose">
|
||
<ul>
|
||
<li><b>The root key decides a module.</b> Children default to on and are switched off only to remove one capability. Today's <code>loans.*</code> and <code>cards.*</code> children get a one-time cleanup.</li>
|
||
<li><b>New keys ship off.</b> A CI check fails any new key that defaults to on without a written reason.</li>
|
||
<li><b>The backend knows every key.</b> CI exports a feature catalog on each merge; the backend imports it, so the admin portal lists the same keys the app understands.</li>
|
||
<li><b>The server checks again.</b> Every endpoint maps to its feature key and refuses with <code>FEATURE_NOT_AVAILABLE</code>.</li>
|
||
<li><b>Backend first, then switch on.</b> 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.</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<h3 class="flow">Proposed flow: when a bank's flow differs</h3>
|
||
<div class="prose"><p>Some banks want the same feature to work differently. Use the lowest rung that fits; each step up adds power and upkeep.</p></div>
|
||
<figure>
|
||
<div class="panel">
|
||
<svg class="dg" viewBox="0 0 1000 318" role="img" aria-label="The variation ladder, five rungs from lowest to highest: setting, server challenge, journey variant, strategy, slot override. The first three can change without an app update; strategies and slot overrides need an OTA update.">
|
||
<defs>
|
||
<marker id="d5-m" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah" d="M0 0L10 5L0 10z"/></marker>
|
||
</defs>
|
||
<text class="ts" x="20" y="28">Higher rung: more power, more upkeep. Use the lowest rung that fits.</text>
|
||
<line class="edge" x1="20" y1="40" x2="975" y2="40" marker-end="url(#d5-m)"/>
|
||
<rect class="box-a" x="20" y="200" width="180" height="100" rx="6"/>
|
||
<text class="tt ta" x="34" y="224">1 · Setting</text>
|
||
<text class="ts" x="34" y="244">a value differs</text>
|
||
<text class="tm" x="34" y="262">RTGS minimum</text>
|
||
<text class="ta" x="34" y="282">changes live</text>
|
||
<rect class="box-a" x="215" y="175" width="180" height="125" rx="6"/>
|
||
<text class="tt ta" x="229" y="199">2 · Server challenge</text>
|
||
<text class="ts" x="229" y="219">security steps differ</text>
|
||
<text class="tm" x="229" y="237">TPIN → OTP</text>
|
||
<text class="ta" x="229" y="257">changes live</text>
|
||
<rect class="box-a" x="410" y="150" width="180" height="150" rx="6"/>
|
||
<text class="tt ta" x="424" y="174">3 · Journey variant</text>
|
||
<text class="ts" x="424" y="194">journey steps differ</text>
|
||
<text class="tm" x="424" y="212">loan form steps</text>
|
||
<text class="ta" x="424" y="232">switch live, add by OTA</text>
|
||
<rect class="box" x="605" y="125" width="180" height="175" rx="6"/>
|
||
<text class="tt" x="619" y="149">4 · Strategy</text>
|
||
<text class="ts" x="619" y="169">a rule differs</text>
|
||
<text class="tm" x="619" y="187">account number format</text>
|
||
<text x="619" y="207">needs OTA</text>
|
||
<rect class="box" x="800" y="100" width="180" height="200" rx="6"/>
|
||
<text class="tt" x="814" y="124">5 · Slot override</text>
|
||
<text class="ts" x="814" y="144">a look differs</text>
|
||
<text class="tm" x="814" y="162">Home header</text>
|
||
<text x="814" y="182">needs OTA</text>
|
||
</svg>
|
||
<div class="legend"><span><i class="sw a"></i>Can change without an app update</span><span><i class="sw"></i>Needs an OTA update</span></div>
|
||
</div>
|
||
<figcaption>Most bank requests stop at rung 1 or 2. Rungs 4 and 5 are built only when a real bank needs them.</figcaption>
|
||
</figure>
|
||
<figure>
|
||
<div class="panel">
|
||
<svg class="dg" viewBox="0 0 1000 250" role="img" aria-label="The same transfer in three banks. Bank A: confirm, TPIN, OTP, done. Bank B for 3,000 rupees: confirm, OTP, done. Bank C: confirm, biometric, TPIN, done. The app code is identical; only the server policy differs.">
|
||
<defs>
|
||
<marker id="d6-m" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah" d="M0 0L10 5L0 10z"/></marker>
|
||
</defs>
|
||
<text class="ts" x="180" y="22">Each arrow is one server reply that names the next check.</text>
|
||
<text class="tt" x="20" y="58">Bank A</text>
|
||
<text class="ts" x="20" y="76">policy: TPIN, then OTP</text>
|
||
<rect class="box" x="180" y="40" width="130" height="44" rx="6"/><text class="tt" x="245" y="67" text-anchor="middle">Confirm</text>
|
||
<line class="edge" x1="310" y1="62" x2="348" y2="62" marker-end="url(#d6-m)"/>
|
||
<rect class="box-a" x="350" y="40" width="130" height="44" rx="6"/><text class="tt ta" x="415" y="67" text-anchor="middle">TPIN</text>
|
||
<line class="edge" x1="480" y1="62" x2="518" y2="62" marker-end="url(#d6-m)"/>
|
||
<rect class="box-a" x="520" y="40" width="130" height="44" rx="6"/><text class="tt ta" x="585" y="67" text-anchor="middle">OTP</text>
|
||
<line class="edge" x1="650" y1="62" x2="688" y2="62" marker-end="url(#d6-m)"/>
|
||
<rect class="box" x="690" y="40" width="130" height="44" rx="6"/><text class="tt" x="755" y="67" text-anchor="middle">Done</text>
|
||
<text class="tt" x="20" y="128">Bank B · ₹3,000</text>
|
||
<text class="ts" x="20" y="146">OTP only up to ₹5,000</text>
|
||
<rect class="box" x="180" y="110" width="130" height="44" rx="6"/><text class="tt" x="245" y="137" text-anchor="middle">Confirm</text>
|
||
<line class="edge" x1="310" y1="132" x2="348" y2="132" marker-end="url(#d6-m)"/>
|
||
<rect class="box-a" x="350" y="110" width="130" height="44" rx="6"/><text class="tt ta" x="415" y="137" text-anchor="middle">OTP</text>
|
||
<line class="edge" x1="480" y1="132" x2="518" y2="132" marker-end="url(#d6-m)"/>
|
||
<rect class="box" x="520" y="110" width="130" height="44" rx="6"/><text class="tt" x="585" y="137" text-anchor="middle">Done</text>
|
||
<text class="tt" x="20" y="198">Bank C</text>
|
||
<text class="ts" x="20" y="216">biometric, then TPIN</text>
|
||
<rect class="box" x="180" y="180" width="130" height="44" rx="6"/><text class="tt" x="245" y="207" text-anchor="middle">Confirm</text>
|
||
<line class="edge" x1="310" y1="202" x2="348" y2="202" marker-end="url(#d6-m)"/>
|
||
<rect class="box-a" x="350" y="180" width="130" height="44" rx="6"/><text class="tt ta" x="415" y="207" text-anchor="middle">Biometric</text>
|
||
<line class="edge" x1="480" y1="202" x2="518" y2="202" marker-end="url(#d6-m)"/>
|
||
<rect class="box-a" x="520" y="180" width="130" height="44" rx="6"/><text class="tt ta" x="585" y="207" text-anchor="middle">TPIN</text>
|
||
<line class="edge" x1="650" y1="202" x2="688" y2="202" marker-end="url(#d6-m)"/>
|
||
<rect class="box" x="690" y="180" width="130" height="44" rx="6"/><text class="tt" x="755" y="207" text-anchor="middle">Done</text>
|
||
<rect class="grp" x="850" y="40" width="140" height="184" rx="8"/>
|
||
<text class="tt" x="866" y="70">Same app code</text>
|
||
<text class="ts" x="866" y="92">for all three banks.</text>
|
||
<text class="ts" x="866" y="112">Only the server</text>
|
||
<text class="ts" x="866" y="130">policy differs.</text>
|
||
<text class="ts" x="866" y="160">Bank B can move</text>
|
||
<text class="ts" x="866" y="178">₹5,000 to ₹10,000</text>
|
||
<text class="ts" x="866" y="196">with no app release.</text>
|
||
</svg>
|
||
<div class="legend"><span><i class="sw a"></i>A check the server asked for</span></div>
|
||
</div>
|
||
<figcaption>Security steps come from the server, so a modified app cannot skip them. Today's IMPS API already takes the TPIN in <code>initiate</code> and the OTP in <code>verify-otp</code>; an adapter maps it onto the same runner, so the app can move before the backend changes.</figcaption>
|
||
</figure>
|
||
<div class="rule"><b>Not allowed:</b> <code>if (tenant.id === 'bank-b')</code> or <code>switch (tenant.orgId)</code> in shared code, per-bank Git branches, or copied screens. An ESLint rule fails the build on the first two.</div>
|
||
</section>
|
||
|
||
<!-- 04 RENDERING -->
|
||
<section id="rendering">
|
||
<h2><span class="num">04</span>How the UI renders</h2>
|
||
<h3 class="today">Today: what is wrong</h3>
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Severity</th><th>Flaw</th><th>Where</th><th>Effect</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><span class="pill k">Blocker</span></td><td>Mock code renders in release builds. Own-account transfer is on by default and accepts OTP <code>123456</code>, then shows a made-up success. Dashboard, cards, loans and deposits show fixture data</td><td><code>env.ts:40</code>, <code>serviceHelpers.ts:55</code>, <code>PaymentConfirmationScreen.tsx:453</code>, <code>paymentsApi.ts:190</code>, <code>featureConfig.ts:107</code></td><td>A customer is told money moved when it did not</td></tr>
|
||
<tr><td><span class="pill w">High</span></td><td>No error boundary anywhere</td><td>none</td><td>One render error blanks the whole app; OTA auto-rollback misses crashes after the first screen</td></tr>
|
||
<tr><td><span class="pill w">High</span></td><td>The splash waits for the config request, up to the 15-second client timeout</td><td><code>RootNavigator.tsx:52</code>, <code>apiClient.ts:14</code></td><td>Slow start on a weak network</td></tr>
|
||
<tr><td><span class="pill w">High</span></td><td>When a feature turns off, its screen is removed, even in the middle of a payment</td><td><code>FeatureRoute.tsx:39</code>, <code>featureRoutes.ts</code></td><td>The transfer result is lost; a retry can create a duplicate transfer</td></tr>
|
||
<tr><td><span class="pill">Medium</span></td><td>Every config refresh re-renders every gated component, even when nothing changed</td><td><code>featureService.ts:223</code></td><td>Wasted renders; worse with refresh on resume</td></tr>
|
||
<tr><td><span class="pill">Medium</span></td><td>Six screens of 640–724 lines mix UI, steps, API calls and error handling; no component tests</td><td><code>PaymentConfirmationScreen</code>, <code>RegisterScreen</code>, <code>TpinResetFlow</code> and three more</td><td>Hard to change per bank; breaks silently</td></tr>
|
||
<tr><td><span class="pill">Medium</span></td><td>The global font is set through <code>Text.defaultProps</code>, which React 19 ignores for function components</td><td><code>index.js:36</code></td><td>Text without its own font likely falls back to the system font</td></tr>
|
||
<tr><td><span class="pill">Medium</span></td><td>All on-screen warnings hidden in development; Redux serializable check off</td><td><code>index.js:19</code>, <code>store.ts:11</code></td><td>Rendering bugs go unnoticed</td></tr>
|
||
<tr><td><span class="pill">Medium</span></td><td>Three data-access patterns plus fixtures re-seeded at every sign-in</td><td>ARCHITECTURE.md §4</td><td>Two screens can show different values for the same thing</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h3 class="flow">Proposed flow: from launch to a gated screen</h3>
|
||
<figure>
|
||
<div class="panel">
|
||
<svg class="dg" viewBox="0 0 1000 140" role="img" aria-label="Render path: app start with compiled defaults, cached bank config, a config fetch capped at four seconds, the sign-in screen gated by flags, waiting for session flags after login, app tabs with only allowed routes, and each screen with gated parts and guarded actions.">
|
||
<defs>
|
||
<marker id="n8-m" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah" d="M0 0L10 5L0 10z"/></marker>
|
||
</defs>
|
||
<text class="th" x="20" y="26">SPLASH</text>
|
||
<line class="sep" x1="20" y1="36" x2="422" y2="36"/>
|
||
<text class="th" x="437" y="26">SIGN-IN SCREEN</text>
|
||
<line class="sep" x1="437" y1="36" x2="561" y2="36"/>
|
||
<text class="th" x="576" y="26">SPLASH</text>
|
||
<line class="sep" x1="576" y1="36" x2="700" y2="36"/>
|
||
<text class="th" x="715" y="26">APP</text>
|
||
<line class="sep" x1="715" y1="36" x2="978" y2="36"/>
|
||
|
||
<rect class="box" x="20" y="46" width="124" height="72" rx="6"/>
|
||
<text class="tt" x="32" y="72">App start</text>
|
||
<text class="ts" x="32" y="94">default + pack</text>
|
||
<line class="edge" x1="144" y1="82" x2="157" y2="82" marker-end="url(#n8-m)"/>
|
||
<rect class="box" x="159" y="46" width="124" height="72" rx="6"/>
|
||
<text class="tt" x="171" y="72">Cache</text>
|
||
<text class="ts" x="171" y="94">last bank config</text>
|
||
<line class="edge" x1="283" y1="82" x2="296" y2="82" marker-end="url(#n8-m)"/>
|
||
<rect class="box new" x="298" y="46" width="124" height="72" rx="6"/>
|
||
<text class="tt" x="310" y="72">Fetch config</text>
|
||
<text class="ts" x="310" y="94">max 4 s, orgId</text>
|
||
<line class="edge" x1="422" y1="82" x2="435" y2="82" marker-end="url(#n8-m)"/>
|
||
<rect class="box" x="437" y="46" width="124" height="72" rx="6"/>
|
||
<text class="tt" x="449" y="72">Sign-in</text>
|
||
<text class="ts" x="449" y="94">gated by flags</text>
|
||
<line class="edge" x1="561" y1="82" x2="574" y2="82" marker-end="url(#n8-m)"/>
|
||
<rect class="box new" x="576" y="46" width="124" height="72" rx="6"/>
|
||
<text class="tt" x="588" y="72">Session flags</text>
|
||
<text class="ts" x="588" y="94">role + customer</text>
|
||
<line class="edge" x1="700" y1="82" x2="713" y2="82" marker-end="url(#n8-m)"/>
|
||
<rect class="box" x="715" y="46" width="124" height="72" rx="6"/>
|
||
<text class="tt" x="727" y="72">App tabs</text>
|
||
<text class="ts" x="727" y="94">allowed routes</text>
|
||
<line class="edge" x1="839" y1="82" x2="852" y2="82" marker-end="url(#n8-m)"/>
|
||
<rect class="box" x="854" y="46" width="124" height="72" rx="6"/>
|
||
<text class="tt" x="866" y="72">Each screen</text>
|
||
<text class="ts" x="866" y="94">guarded actions</text>
|
||
</svg>
|
||
<div class="legend"><span><i class="sw n"></i>New behaviour</span></div>
|
||
</div>
|
||
<figcaption>The app never shows anything before it knows what is allowed. The cache makes the first step instant and keeps kill switches working offline; the two waits are short and bounded.</figcaption>
|
||
</figure>
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Fix</th><th>How</th><th>Phase</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>No mock code in release</td><td>Build environments decide mocks; <code>resolveWithMock</code> works only in dev; features with no backend are off in UAT and prod; the OTP-<code>123456</code> path is deleted</td><td class="nw">0</td></tr>
|
||
<tr><td>Error boundaries</td><td>One at the root and one per tab: "Something went wrong, back to Home", reported with bank tags</td><td class="nw">0</td></tr>
|
||
<tr><td>Fast, bounded start</td><td>Cache first; config fetch capped at 4 seconds</td><td class="nw">0</td></tr>
|
||
<tr><td>No flash after sign-in</td><td>Hold the app until session flags land, or receive them in the login response</td><td class="nw">1</td></tr>
|
||
<tr><td>Never pull a screen mid-payment</td><td>A running money flow holds a lease; its route is removed only after the flow ends</td><td class="nw">1</td></tr>
|
||
<tr><td>Re-render only on real change</td><td>Compare the new config with the old before bumping the version</td><td class="nw">1</td></tr>
|
||
<tr><td>Consistent font</td><td>A shared <code>AppText</code> component instead of <code>Text.defaultProps</code></td><td class="nw">2</td></tr>
|
||
<tr><td>Smaller screens</td><td>The flow framework takes steps, PIN, OTP and error handling out of screens; component tests per flow</td><td class="nw">5</td></tr>
|
||
<tr><td>See warnings again</td><td>Remove <code>LogBox.ignoreAllLogs</code>; turn the serializable check back on</td><td class="nw">0</td></tr>
|
||
<tr><td>One source of truth</td><td>Move slices to RTK Query as each module gets its real API</td><td class="nw">ongoing</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 05 DEVELOPMENT -->
|
||
<section id="development">
|
||
<h2><span class="num">05</span>Development flow</h2>
|
||
<h3 class="today">Today: what is wrong</h3>
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Severity</th><th>Flaw</th><th>Where</th><th>Effect</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><span class="pill w">High</span></td><td>No CI pipeline; lint, type check, tests and the feature audit run only by hand</td><td><code>.github/</code> has no workflows</td><td>Broken code and unwired flags reach <code>main</code></td></tr>
|
||
<tr><td><span class="pill w">High</span></td><td>No build environments: one <code>env.ts</code> holds the host and the mock switches</td><td><code>config/env.ts</code></td><td>Whatever is committed ships, mocks included</td></tr>
|
||
<tr><td><span class="pill">Medium</span></td><td>Unit tests only; no component or navigation tests</td><td>ARCHITECTURE.md §11</td><td>Screens and navigation break silently</td></tr>
|
||
<tr><td><span class="pill">Medium</span></td><td>Warnings hidden in development</td><td><code>index.js:19</code></td><td>Bugs found late, by customers</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h3 class="flow">Proposed flow</h3>
|
||
<figure>
|
||
<div class="panel">
|
||
<svg class="dg" viewBox="0 0 1000 280" role="img" aria-label="Development flow: a feature branch becomes a pull request with code review; CI runs nine checks; the change merges to main, which feeds the weekly JS train and the monthly native train; a release branch is cut at each store release for hotfixes.">
|
||
<defs>
|
||
<marker id="n4-m" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah" d="M0 0L10 5L0 10z"/></marker>
|
||
</defs>
|
||
<rect class="box" x="20" y="107" width="130" height="56" rx="6"/>
|
||
<text class="tt" x="34" y="131">Feature branch</text>
|
||
<text class="tm" x="34" y="150">feat/…</text>
|
||
<line class="edge" x1="150" y1="135" x2="172" y2="135" marker-end="url(#n4-m)"/>
|
||
<rect class="box" x="175" y="107" width="120" height="56" rx="6"/>
|
||
<text class="tt" x="189" y="131">Pull request</text>
|
||
<text class="ts" x="189" y="150">+ code review</text>
|
||
<line class="edge" x1="295" y1="135" x2="317" y2="135" marker-end="url(#n4-m)"/>
|
||
<rect class="box" x="320" y="20" width="240" height="240" rx="6"/>
|
||
<text class="tt" x="334" y="44">CI checks on every PR</text>
|
||
<text class="ts" x="334" y="70">• lint + no-bank-branching rule</text>
|
||
<text class="ts" x="334" y="91">• type check</text>
|
||
<text class="ts" x="334" y="112">• unit + component tests</text>
|
||
<text class="ts" x="334" y="133">• feature audit, strict</text>
|
||
<text class="ts" x="334" y="154">• new keys ship off</text>
|
||
<text class="ts" x="334" y="175">• tenant matrix + contrast</text>
|
||
<text class="ts" x="334" y="196">• bundle builds for every bank</text>
|
||
<text class="ts" x="334" y="217">• no mock code in release builds</text>
|
||
<text class="ts" x="334" y="238">• export feature catalog</text>
|
||
<line class="edge" x1="560" y1="135" x2="582" y2="135" marker-end="url(#n4-m)"/>
|
||
<rect class="box" x="585" y="107" width="110" height="56" rx="6"/>
|
||
<text class="tt" x="599" y="131">main</text>
|
||
<text class="ts" x="599" y="150">releasable</text>
|
||
<path class="edge" d="M695 125 H714 V70 H732" marker-end="url(#n4-m)"/>
|
||
<path class="edge" d="M695 145 H714 V210 H732" marker-end="url(#n4-m)"/>
|
||
<rect class="box-a" x="735" y="40" width="245" height="60" rx="6"/>
|
||
<text class="tt ta" x="749" y="64">Weekly JS train</text>
|
||
<text class="ts" x="749" y="84">OTA to each bank's channels</text>
|
||
<rect class="box" x="735" y="180" width="245" height="60" rx="6"/>
|
||
<text class="tt" x="749" y="204">Monthly native train</text>
|
||
<text class="ts" x="749" y="224">store build in each bank's account</text>
|
||
<line class="edge" x1="640" y1="163" x2="640" y2="198" marker-end="url(#n4-m)"/>
|
||
<text class="ts" x="648" y="185">cut</text>
|
||
<rect class="box" x="585" y="200" width="110" height="48" rx="6"/>
|
||
<text class="tm" x="599" y="220">release/x.y</text>
|
||
<text class="ts" x="599" y="238">hotfixes</text>
|
||
</svg>
|
||
<div class="legend"><span><i class="sw a"></i>Reaches phones without a store release</span></div>
|
||
</div>
|
||
<figcaption>Nothing merges unless every check passes. Fixes for an older native version are cherry-picked onto its <code>release/x.y</code> branch and ship on the same OTA train.</figcaption>
|
||
</figure>
|
||
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Branch or tag</th><th>Purpose</th><th>Lifetime</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>main</code></td><td>Always releasable; the weekly JS train is cut from here</td><td>Permanent</td></tr>
|
||
<tr><td><code>feat/…</code></td><td>One change, merged by pull request</td><td>Days</td></tr>
|
||
<tr><td><code>release/1.6</code></td><td>Created at each store release; hotfixes for that native version</td><td>Until 1.6 leaves support</td></tr>
|
||
<tr><td><code>v1.6.0</code></td><td>Tag on every store release</td><td>Permanent</td></tr>
|
||
<tr><td><code>ota-2026-10-15</code></td><td>Tag on every JS train</td><td>Permanent</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="prose"><p>Working locally, one bank at a time:</p></div>
|
||
<pre><code>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</code></pre>
|
||
|
||
<div class="prose">
|
||
<p><b>Rules for developers</b></p>
|
||
<ul>
|
||
<li>Never branch on the bank. Use a setting, the server policy, a journey variant, a strategy or a slot.</li>
|
||
<li>Every new screen, tab, button and money action has a feature key, and the key ships off.</li>
|
||
<li>Values a bank might change are settings, not constants.</li>
|
||
<li>Security steps go through the flow framework, never a hand-written step machine.</li>
|
||
<li>Mock data lives behind the dev environment only.</li>
|
||
<li>Bank-specific code lives in that bank's pack, with a note in the pack's README saying why.</li>
|
||
</ul>
|
||
<p><b>A feature is done when:</b> 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.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 06 DEPLOYMENT -->
|
||
<section id="deployment">
|
||
<h2><span class="num">06</span>Deployment flow</h2>
|
||
<h3 class="today">Today: what is wrong</h3>
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Severity</th><th>Flaw</th><th>Where</th><th>Effect</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><span class="pill k">Blocker</span></td><td>Release builds are signed with the debug key</td><td><code>android/app/build.gradle:103</code></td><td>Cannot be published to the Play Store</td></tr>
|
||
<tr><td><span class="pill k">Blocker</span></td><td>The PIN encryption key is fetched over plain HTTP before every encryption</td><td><code>security/mpinCrypto.ts:53</code>, <code>config/env.ts:26</code></td><td>Someone on the same network can swap the key and read customers' MPIN and TPIN</td></tr>
|
||
<tr><td><span class="pill w">High</span></td><td>Plain HTTP to a private LAN address</td><td><code>config/env.ts:26</code></td><td>Works only on the office network or VPN; all traffic can be read and changed</td></tr>
|
||
<tr><td><span class="pill w">High</span></td><td>One app identity, no flavors or schemes</td><td><code>build.gradle:82</code></td><td>Cannot build a second bank's app</td></tr>
|
||
<tr><td><span class="pill w">High</span></td><td>Version <code>1.0</code>, version code <code>1</code>, no scheme</td><td><code>build.gradle:85-86</code></td><td>Store updates and OTA targeting need <code>x.y.z</code></td></tr>
|
||
<tr><td><span class="pill">Medium</span></td><td>No release automation or store upload</td><td>none</td><td>Manual, error-prone, not repeatable per bank</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h3 class="flow">Proposed flow</h3>
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Build variant</th><th>Environment</th><th>Signed with</th><th>Installed from</th><th>OTA channel</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>bankADevDebug</code></td><td>Dev</td><td>Debug key</td><td>Developer machine</td><td>none</td></tr>
|
||
<tr><td><code>bankAUatRelease</code></td><td>UAT</td><td>Bank A's upload key</td><td>Internal track, TestFlight</td><td><code>bank-a-uat</code></td></tr>
|
||
<tr><td><code>bankAProdRelease</code></td><td>Prod</td><td>Bank A's upload key</td><td>Play Store, App Store</td><td><code>bank-a-prod</code></td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<figure>
|
||
<div class="panel">
|
||
<svg class="dg" viewBox="0 0 1000 206" role="img" aria-label="Monthly native release train: cut the release branch and bump versions, build UAT for each bank, bank UAT on the internal track or TestFlight, submit the production build to the bank's own store account, roll out from 10 to 100 percent, later raise the minimum version. A bug found in UAT is fixed on the release branch and rebuilt.">
|
||
<defs>
|
||
<marker id="n5-m" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah" d="M0 0L10 5L0 10z"/></marker>
|
||
</defs>
|
||
<rect class="box" x="12" y="40" width="146" height="90" rx="6"/>
|
||
<text class="tt" x="26" y="64">1 · Cut release</text>
|
||
<text class="tm" x="26" y="86">release/1.6</text>
|
||
<text class="ts" x="26" y="106">versions bumped</text>
|
||
<line class="edge" x1="158" y1="85" x2="176" y2="85" marker-end="url(#n5-m)"/>
|
||
<rect class="box-a" x="178" y="40" width="146" height="90" rx="6"/>
|
||
<text class="tt ta" x="192" y="64">2 · UAT builds</text>
|
||
<text class="tm" x="192" y="86">bankXUatRelease</text>
|
||
<text class="ts" x="192" y="106">CI, each bank</text>
|
||
<line class="edge" x1="324" y1="85" x2="342" y2="85" marker-end="url(#n5-m)"/>
|
||
<rect class="box-a" x="344" y="40" width="146" height="90" rx="6"/>
|
||
<text class="tt ta" x="358" y="64">3 · Bank UAT</text>
|
||
<text class="ts" x="358" y="86">internal track,</text>
|
||
<text class="ts" x="358" y="106">TestFlight</text>
|
||
<line class="edge" x1="490" y1="85" x2="508" y2="85" marker-end="url(#n5-m)"/>
|
||
<rect class="box-a" x="510" y="40" width="146" height="90" rx="6"/>
|
||
<text class="tt ta" x="524" y="64">4 · Submit</text>
|
||
<text class="ts" x="524" y="86">prod build to the</text>
|
||
<text class="ts" x="524" y="106">bank's own account</text>
|
||
<line class="edge" x1="656" y1="85" x2="674" y2="85" marker-end="url(#n5-m)"/>
|
||
<rect class="box-a" x="676" y="40" width="146" height="90" rx="6"/>
|
||
<text class="tt ta" x="690" y="64">5 · Roll out</text>
|
||
<text class="tm" x="690" y="86">10% → 100%</text>
|
||
<text class="ts" x="690" y="106">watch crash-free</text>
|
||
<line class="edge" x1="822" y1="85" x2="840" y2="85" marker-end="url(#n5-m)"/>
|
||
<rect class="box" x="842" y="40" width="146" height="90" rx="6"/>
|
||
<text class="tt" x="856" y="64">6 · Min version</text>
|
||
<text class="ts" x="856" y="86">raise it later;</text>
|
||
<text class="ts" x="856" y="106">old apps update</text>
|
||
<path class="edge" d="M417 130 V166 H251 V133" marker-end="url(#n5-m)"/>
|
||
<text class="ts" x="334" y="186" text-anchor="middle">bug found: fix on release branch, rebuild</text>
|
||
</svg>
|
||
<div class="legend"><span><i class="sw a"></i>Runs once per bank, on that bank's sign-off</span></div>
|
||
</div>
|
||
<figcaption>Each bank moves through steps 2 to 5 on its own schedule. CI uploads to each bank's store account with credentials the bank grants (Play Developer API, App Store Connect API key).</figcaption>
|
||
</figure>
|
||
<div class="prose">
|
||
<p><b>Versions</b></p>
|
||
<ul>
|
||
<li>Each bank's version lives in <code>tenants/registry.json</code> as <code>MAJOR.MINOR.PATCH</code>. A native release bumps MINOR; a native hotfix bumps PATCH.</li>
|
||
<li>Version code is computed: <code>MAJOR × 10000 + MINOR × 100 + PATCH</code>, so 1.6.2 becomes 10602.</li>
|
||
<li>OTA bundles target exact native versions (<code>-t 1.6.2</code>), never wide ranges.</li>
|
||
<li>We support the last two minor versions. Below that, the server's minimum version sends the customer to the store.</li>
|
||
</ul>
|
||
<p><b>Signing and secrets</b></p>
|
||
<ul>
|
||
<li>One upload key per bank, stored only in CI secrets and backed up offline.</li>
|
||
<li>Google Play App Signing per bank app; iOS certificates in each bank's Apple account.</li>
|
||
<li>Hot Updater credentials per environment; only CI may deploy to production channels.</li>
|
||
</ul>
|
||
<p><b>Working with backend releases</b></p>
|
||
<ul>
|
||
<li>A bank's backend is upgraded before any app feature that needs it is switched on for that bank.</li>
|
||
<li>API changes are additive. A field is removed only after every bank's backend and every supported app version have moved past it.</li>
|
||
<li>The config response carries a schema version; the app ignores keys it does not know.</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<h3>Backend deployment, one bank at a time</h3>
|
||
<figure>
|
||
<div class="panel">
|
||
<svg class="dg" viewBox="0 0 1000 240" role="img" aria-label="Backend deployment: a backend pull request passes CI and becomes one image, backend 2.4.0, which is deployed to each bank's UAT backend; after that bank signs off, the same image goes to that bank's production backend.">
|
||
<defs>
|
||
<marker id="n9-m" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah" d="M0 0L10 5L0 10z"/></marker>
|
||
</defs>
|
||
<rect class="box" x="20" y="96" width="130" height="56" rx="6"/>
|
||
<text class="tt" x="34" y="120">Backend PR</text>
|
||
<text class="ts" x="34" y="139">one codebase</text>
|
||
<line class="edge" x1="150" y1="124" x2="173" y2="124" marker-end="url(#n9-m)"/>
|
||
<rect class="box" x="175" y="96" width="140" height="56" rx="6"/>
|
||
<text class="tt" x="189" y="120">CI + tests</text>
|
||
<text class="ts" x="189" y="139">migration checks</text>
|
||
<line class="edge" x1="315" y1="124" x2="338" y2="124" marker-end="url(#n9-m)"/>
|
||
<rect class="box" x="340" y="96" width="150" height="56" rx="6"/>
|
||
<text class="tt" x="354" y="120">One image</text>
|
||
<text class="tm" x="354" y="139">backend:2.4.0</text>
|
||
<path class="edge" d="M490 124 H525 V50 H557" marker-end="url(#n9-m)"/>
|
||
<line class="edge" x1="490" y1="124" x2="557" y2="124" marker-end="url(#n9-m)"/>
|
||
<path class="edge" d="M490 124 H525 V198 H557" marker-end="url(#n9-m)"/>
|
||
|
||
<rect class="box-a" x="560" y="28" width="150" height="44" rx="6"/>
|
||
<text class="tt ta" x="574" y="47">Bank A · UAT</text>
|
||
<text class="ts" x="574" y="63">bank tests here</text>
|
||
<rect class="box-a" x="560" y="102" width="150" height="44" rx="6"/>
|
||
<text class="tt ta" x="574" y="121">Bank B · UAT</text>
|
||
<text class="ts" x="574" y="137">bank tests here</text>
|
||
<rect class="box-a" x="560" y="176" width="150" height="44" rx="6"/>
|
||
<text class="tt ta" x="574" y="195">Bank C · UAT</text>
|
||
<text class="ts" x="574" y="211">bank tests here</text>
|
||
|
||
<line class="edge" x1="710" y1="50" x2="797" y2="50" marker-end="url(#n9-m)"/>
|
||
<text class="ts" x="754" y="42" text-anchor="middle">sign-off</text>
|
||
<line class="edge" x1="710" y1="124" x2="797" y2="124" marker-end="url(#n9-m)"/>
|
||
<text class="ts" x="754" y="116" text-anchor="middle">sign-off</text>
|
||
<line class="edge" x1="710" y1="198" x2="797" y2="198" marker-end="url(#n9-m)"/>
|
||
<text class="ts" x="754" y="190" text-anchor="middle">sign-off</text>
|
||
|
||
<rect class="box-a" x="800" y="28" width="180" height="44" rx="6"/>
|
||
<text class="tt ta" x="814" y="47">Bank A · production</text>
|
||
<text class="ts" x="814" y="63">rolling deploy</text>
|
||
<rect class="box-a" x="800" y="102" width="180" height="44" rx="6"/>
|
||
<text class="tt ta" x="814" y="121">Bank B · production</text>
|
||
<text class="ts" x="814" y="137">rolling deploy</text>
|
||
<rect class="box-a" x="800" y="176" width="180" height="44" rx="6"/>
|
||
<text class="tt ta" x="814" y="195">Bank C · production</text>
|
||
<text class="ts" x="814" y="211">rolling deploy</text>
|
||
</svg>
|
||
<div class="legend"><span><i class="sw a"></i>A separate deployment for each bank</span></div>
|
||
</div>
|
||
<figcaption>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.</figcaption>
|
||
</figure>
|
||
<div class="prose">
|
||
<ul>
|
||
<li><b>One image everywhere.</b> Banks differ only in configuration and secrets: core-banking endpoint, database, key pair, certificates and org id.</li>
|
||
<li><b>Infrastructure as code.</b> One script creates a new bank's UAT and production backends. The target is one day per bank.</li>
|
||
<li><b>Expand, then contract.</b> Database and API changes are added first and removed later, so the previous backend version and older apps keep working during a rollout.</li>
|
||
<li><b>Each backend reports itself.</b> <code>GET /meta</code> returns its version and capabilities, and the release registry records which version each bank runs.</li>
|
||
<li><b>Each backend serves one bank.</b> It accepts only its own org id and refuses anything else.</li>
|
||
<li><b>Hosting is still open</b> (<a href="#decisions">section 13</a>): 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.</li>
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 07 OTA -->
|
||
<section id="ota">
|
||
<h2><span class="num">07</span>OTA flow</h2>
|
||
<h3 class="today">Today: what is wrong</h3>
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Severity</th><th>Flaw</th><th>Where</th><th>Effect</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><span class="pill k">Blocker</span></td><td>No OTA at all</td><td><code>package.json</code></td><td>Every JS fix waits for a store release and review in each bank's account</td></tr>
|
||
<tr><td><span class="pill w">High</span></td><td>No safety net: no error boundary, no crash reporting</td><td>none</td><td>A staged rollout cannot see failures</td></tr>
|
||
<tr><td><span class="pill w">High</span></td><td>No forced update</td><td>none</td><td>Phones on a broken native version stay broken</td></tr>
|
||
<tr><td><span class="pill">Medium</span></td><td>Version <code>1.0</code> cannot be targeted precisely</td><td><code>build.gradle:86</code></td><td>Bundles could reach binaries they were not built for</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h3 class="flow">Proposed flow</h3>
|
||
<div class="prose"><p>Every bank has two channels: <code>bank-x-uat</code>, installed by its testers, and <code>bank-x-prod</code>, installed by its customers. The channel is baked into the binary, so a bundle for Bank B can never reach Bank A.</p></div>
|
||
<figure>
|
||
<div class="panel">
|
||
<svg class="dg" viewBox="0 0 1000 226" role="img" aria-label="OTA pipeline: pull request, CI checks, build per bank, UAT channels, bank sign-off, production rollout from 10 to 100 percent. On a problem, disable the bundle so phones revert, then fix forward with a new pull request.">
|
||
<defs>
|
||
<marker id="d9-m" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah" d="M0 0L10 5L0 10z"/></marker>
|
||
<marker id="d9-mk" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah-k" d="M0 0L10 5L0 10z"/></marker>
|
||
</defs>
|
||
<rect class="box" x="20" y="50" width="100" height="64" rx="6"/>
|
||
<text class="tt" x="34" y="76">Pull request</text>
|
||
<text class="ts" x="34" y="95">one change</text>
|
||
<line class="edge" x1="120" y1="82" x2="143" y2="82" marker-end="url(#d9-m)"/>
|
||
<rect class="box" x="145" y="50" width="160" height="64" rx="6"/>
|
||
<text class="tt" x="159" y="76">CI checks</text>
|
||
<text class="ts" x="159" y="95">lint · types · tests</text>
|
||
<line class="edge" x1="305" y1="82" x2="328" y2="82" marker-end="url(#d9-m)"/>
|
||
<rect class="box" x="330" y="50" width="150" height="64" rx="6"/>
|
||
<text class="tt" x="344" y="76">Build per bank</text>
|
||
<text class="tm" x="344" y="95">TENANT=bank-x</text>
|
||
<line class="edge" x1="480" y1="82" x2="503" y2="82" marker-end="url(#d9-m)"/>
|
||
<rect class="box-a" x="505" y="50" width="140" height="64" rx="6"/>
|
||
<text class="tt ta" x="519" y="76">UAT channels</text>
|
||
<text class="tm" x="519" y="95">bank-x-uat</text>
|
||
<line class="edge" x1="645" y1="82" x2="668" y2="82" marker-end="url(#d9-m)"/>
|
||
<rect class="box-a" x="670" y="50" width="110" height="64" rx="6"/>
|
||
<text class="tt ta" x="684" y="76">Bank sign-off</text>
|
||
<text class="ts" x="684" y="95">per bank</text>
|
||
<line class="edge" x1="780" y1="82" x2="803" y2="82" marker-end="url(#d9-m)"/>
|
||
<rect class="box-a" x="805" y="50" width="185" height="64" rx="6"/>
|
||
<text class="tt ta" x="819" y="76">Production</text>
|
||
<text class="tm" x="819" y="95">10% → 50% → 100%</text>
|
||
<line class="edge-k" x1="897" y1="114" x2="897" y2="155" marker-end="url(#d9-mk)"/>
|
||
<text class="ts" x="905" y="140">problem</text>
|
||
<rect class="box-k" x="760" y="158" width="230" height="48" rx="6"/>
|
||
<text class="tt tk" x="774" y="178">Disable the bundle</text>
|
||
<text class="ts" x="774" y="196">phones revert to the last bundle</text>
|
||
<path class="edge" d="M760 182 H70 V117" marker-end="url(#d9-m)"/>
|
||
<text class="ts" x="415" y="200" text-anchor="middle">fix forward with a new pull request</text>
|
||
</svg>
|
||
<div class="legend"><span><i class="sw a"></i>Runs once per bank</span><span><i class="sw k"></i>Rollback</span></div>
|
||
</div>
|
||
<figcaption>If a bundle crashes before the first screen, Hot Updater rolls that phone back by itself. For later crashes, the error boundary reports them and the release manager disables the bundle.</figcaption>
|
||
</figure>
|
||
|
||
<h3>The weekly JS train</h3>
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Day</th><th>Step</th><th>Who</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Monday</td><td>Cut from <code>main</code>; CI builds a bundle per bank and per supported native version; deploys to every <code>-uat</code> channel at 100%</td><td>CI, release manager</td></tr>
|
||
<tr><td>Tuesday–Wednesday</td><td>Each bank tests on its UAT app</td><td>Bank QA, our QA</td></tr>
|
||
<tr><td>Wednesday</td><td>Sign-off per bank, recorded in the release ticket</td><td>Bank</td></tr>
|
||
<tr><td>Thursday</td><td>Production 10%, only for banks that signed off</td><td>Release manager</td></tr>
|
||
<tr><td>Friday</td><td>50%, if crash-free sessions stay at or above 99.5% and no error rate jumps</td><td>Release manager</td></tr>
|
||
<tr><td>Next Monday</td><td>100%</td><td>Release manager</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="prose">
|
||
<ul>
|
||
<li><b>Hotfix:</b> the same path, compressed into one day: UAT smoke test within two hours, then 10% and 100% the same day.</li>
|
||
<li><b>Rollback levels:</b> automatic (crash before the first screen); disable one channel's bundle; disable a release on every channel with one script; then fix forward.</li>
|
||
<li><b>A bank that has not signed off</b> stays on its previous bundle. It may lag at most two trains before the release manager escalates.</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<h3>Keeping versions compatible</h3>
|
||
<figure>
|
||
<div class="panel">
|
||
<svg class="dg" viewBox="0 0 1000 240" role="img" aria-label="Versions over time: store release 1.4.0 receives OTA fixes targeted with -t 1.4.0; store release 1.5.0 receives its own fixes with -t 1.5.0; raising the minimum version to 1.5.0 forces phones still on 1.4 to update from the store.">
|
||
<defs>
|
||
<marker id="d10-m" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah" d="M0 0L10 5L0 10z"/></marker>
|
||
<marker id="d10-ma" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah-a" d="M0 0L10 5L0 10z"/></marker>
|
||
</defs>
|
||
<text class="tt" x="20" y="64">Store (native)</text>
|
||
<text class="tt" x="20" y="126">OTA (JS)</text>
|
||
<text class="tt" x="20" y="184">Forced update</text>
|
||
<rect class="box" x="160" y="40" width="120" height="40" rx="6"/>
|
||
<text class="tt" x="220" y="65" text-anchor="middle">v1.4.0</text>
|
||
<rect class="box" x="600" y="40" width="120" height="40" rx="6"/>
|
||
<text class="tt" x="660" y="65" text-anchor="middle">v1.5.0</text>
|
||
<path class="edge-a" d="M220 80 V122 H308" marker-end="url(#d10-ma)"/>
|
||
<text class="tm" x="264" y="114" text-anchor="middle">-t 1.4.0</text>
|
||
<rect class="box-a" x="310" y="104" width="130" height="36" rx="6"/>
|
||
<text class="ta" x="375" y="127" text-anchor="middle">fix 1 for 1.4.0</text>
|
||
<line class="edge-a" x1="440" y1="122" x2="458" y2="122" marker-end="url(#d10-ma)"/>
|
||
<rect class="box-a" x="460" y="104" width="130" height="36" rx="6"/>
|
||
<text class="ta" x="525" y="127" text-anchor="middle">fix 2 for 1.4.0</text>
|
||
<path class="edge-a" d="M660 80 V122 H748" marker-end="url(#d10-ma)"/>
|
||
<text class="tm" x="704" y="114" text-anchor="middle">-t 1.5.0</text>
|
||
<rect class="box-a" x="750" y="104" width="130" height="36" rx="6"/>
|
||
<text class="ta" x="815" y="127" text-anchor="middle">fix 1 for 1.5.0</text>
|
||
<rect class="box-k" x="600" y="162" width="390" height="40" rx="6"/>
|
||
<text class="tk" x="614" y="187">Min version → 1.5.0: phones on 1.4.x must update from the store</text>
|
||
<line class="edge" x1="160" y1="224" x2="980" y2="224" marker-end="url(#d10-m)"/>
|
||
<text class="ts" x="150" y="228" text-anchor="end">time</text>
|
||
</svg>
|
||
</div>
|
||
<figcaption>An OTA bundle reaches only the native version it was built for, so JavaScript never runs on a binary that lacks its native code.</figcaption>
|
||
</figure>
|
||
<div class="note"><b>Before the first OTA:</b> error boundaries, crash reporting tagged by bank and bundle, real signing, <code>x.y.z</code> versions, signed bundles and the tenant guard. Without them, a staged rollout is blind.</div>
|
||
</section>
|
||
|
||
<!-- 08 MANAGEMENT -->
|
||
<section id="manage">
|
||
<h2><span class="num">08</span>Management flow</h2>
|
||
<h3 class="today">Today: what is wrong</h3>
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Severity</th><th>Flaw</th><th>Where</th><th>Effect</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><span class="pill w">High</span></td><td>No backend behind the feature config yet</td><td>comment above <code>MOCK_CONFIG</code> in <code>featureRemoteConfig.ts</code></td><td>Nobody can change a bank's features</td></tr>
|
||
<tr><td><span class="pill w">High</span></td><td>No admin portal, approvals or audit log</td><td>none</td><td>Changes untracked; no two-person control, which banks expect</td></tr>
|
||
<tr><td><span class="pill w">High</span></td><td>No monitoring per bank</td><td>no crash or analytics library</td><td>Cannot tell which bank or bundle is failing</td></tr>
|
||
<tr><td><span class="pill">Medium</span></td><td>Roles are not defined</td><td>none</td><td>Unclear who may switch, test or sign off</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h3 class="flow">Proposed flow: changing a bank's switch</h3>
|
||
<figure>
|
||
<div class="panel">
|
||
<svg class="dg" viewBox="0 0 1000 320" role="img" aria-label="Config change with two-person approval: a maker at Bank B proposes loans off; the config service asks the checker to approve; the checker approves; the audit log records who, what, when and version 44; the customer app fetches config on resume and receives version 44, and the Loans tab is hidden.">
|
||
<defs>
|
||
<marker id="n6-m" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah" d="M0 0L10 5L0 10z"/></marker>
|
||
</defs>
|
||
<rect class="box-a" x="25" y="20" width="150" height="46" rx="6"/>
|
||
<text class="tt ta" x="100" y="39" text-anchor="middle">Maker</text>
|
||
<text class="ts" x="100" y="56" text-anchor="middle">Bank B staff</text>
|
||
<rect class="box-a" x="215" y="20" width="150" height="46" rx="6"/>
|
||
<text class="tt ta" x="290" y="39" text-anchor="middle">Checker</text>
|
||
<text class="ts" x="290" y="56" text-anchor="middle">Bank B staff</text>
|
||
<rect class="box" x="425" y="20" width="150" height="46" rx="6"/>
|
||
<text class="tt" x="500" y="39" text-anchor="middle">Bank B backend</text>
|
||
<text class="ts" x="500" y="56" text-anchor="middle">config service</text>
|
||
<rect class="box" x="625" y="20" width="150" height="46" rx="6"/>
|
||
<text class="tt" x="700" y="39" text-anchor="middle">Audit log</text>
|
||
<text class="ts" x="700" y="56" text-anchor="middle">in Bank B's backend</text>
|
||
<rect class="box-a" x="825" y="20" width="150" height="46" rx="6"/>
|
||
<text class="tt ta" x="900" y="39" text-anchor="middle">Customer app</text>
|
||
<text class="ts" x="900" y="56" text-anchor="middle">Bank B phone</text>
|
||
<line class="life" x1="100" y1="66" x2="100" y2="316"/>
|
||
<line class="life" x1="290" y1="66" x2="290" y2="316"/>
|
||
<line class="life" x1="500" y1="66" x2="500" y2="316"/>
|
||
<line class="life" x1="700" y1="66" x2="700" y2="316"/>
|
||
<line class="life" x1="900" y1="66" x2="900" y2="316"/>
|
||
<line class="edge" x1="100" y1="100" x2="497" y2="100" marker-end="url(#n6-m)"/>
|
||
<text class="ts" x="195" y="92" text-anchor="middle">propose: loans off</text>
|
||
<line class="edge" x1="500" y1="140" x2="293" y2="140" marker-end="url(#n6-m)"/>
|
||
<text class="ts" x="395" y="132" text-anchor="middle">ask to approve</text>
|
||
<line class="edge" x1="290" y1="180" x2="497" y2="180" marker-end="url(#n6-m)"/>
|
||
<text class="ts" x="395" y="172" text-anchor="middle">approve</text>
|
||
<line class="edge" x1="500" y1="220" x2="697" y2="220" marker-end="url(#n6-m)"/>
|
||
<text class="ts" x="600" y="212" text-anchor="middle">who, what, when, v44</text>
|
||
<line class="edge" x1="900" y1="260" x2="503" y2="260" marker-end="url(#n6-m)"/>
|
||
<text class="ts" x="800" y="252" text-anchor="middle">GET config on resume</text>
|
||
<line class="ret" x1="500" y1="300" x2="897" y2="300" marker-end="url(#n6-m)"/>
|
||
<text class="ts" x="800" y="292" text-anchor="middle">v44 → Loans tab hidden</text>
|
||
</svg>
|
||
<div class="legend"><span><i class="sw a"></i>Bank B's side</span><span><i class="sw d"></i>Reply</span></div>
|
||
</div>
|
||
<figcaption>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.</figcaption>
|
||
</figure>
|
||
|
||
<h3>Who owns what</h3>
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Task</th><th>Platform team (us)</th><th>Bank</th><th>DevOps</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Feature scope</td><td>advises</td><td><b>decides</b></td><td>—</td></tr>
|
||
<tr><td>Tenant pack, branding</td><td><b>builds</b></td><td>approves</td><td>—</td></tr>
|
||
<tr><td>Store listing and developer account</td><td>helps</td><td><b>owns</b></td><td>—</td></tr>
|
||
<tr><td>Signing keys, CI secrets</td><td>—</td><td>grants account access</td><td><b>owns</b></td></tr>
|
||
<tr><td>The bank's backend environment</td><td>builds and deploys to it</td><td><b>owns it</b> if it runs in the bank's data centre</td><td><b>runs it</b> if it runs in our cloud</td></tr>
|
||
<tr><td>Backend upgrades</td><td><b>builds</b>, tests</td><td>signs off</td><td>deploys</td></tr>
|
||
<tr><td>Live feature switches</td><td>emergency kill only</td><td><b>owns</b></td><td>—</td></tr>
|
||
<tr><td>Customer rules (KYC, account type, new device)</td><td>configures</td><td><b>decides</b></td><td>—</td></tr>
|
||
<tr><td>Security policy</td><td>configures, reviews</td><td><b>decides</b>, signs off</td><td>—</td></tr>
|
||
<tr><td>UAT testing</td><td>supports</td><td><b>owns</b></td><td>—</td></tr>
|
||
<tr><td>OTA releases</td><td><b>owns</b></td><td>signs off</td><td>runs the pipeline</td></tr>
|
||
<tr><td>Store releases</td><td>builds</td><td><b>approves</b> in its account</td><td>uploads via CI</td></tr>
|
||
<tr><td>Monitoring</td><td><b>owns</b> app health</td><td>sees its own dashboard</td><td><b>owns</b> infrastructure</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h3>Who can change what</h3>
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Tool</th><th>Used by</th><th>Can change</th><th>Cannot change</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Bank admin portal</td><td>Bank staff, two-person approval; runs inside the bank's own backend</td><td>Its own bank's switches, settings, journey variants, customer blocks</td><td>Other banks (separate deployments), security policy, app code</td></tr>
|
||
<tr><td>Platform ops console</td><td>Our operations team, over a private, audited link to each bank's backend</td><td>Onboarding, security policies, emergency kill for any bank, support lookup</td><td>App code</td></tr>
|
||
<tr><td>Git and CI</td><td>Our developers</td><td>Shared code, tenant packs, flow adapters</td><td>Live switches</td></tr>
|
||
<tr><td>Hot Updater console</td><td>Release managers</td><td>Rollout percentage, disable or roll back a bundle</td><td>Native code</td></tr>
|
||
<tr><td>Play Console, App Store Connect</td><td>Bank, with our help</td><td>Store listing, staged rollout, halt</td><td>—</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h3>Watching each bank</h3>
|
||
<div class="prose"><p>Every crash report, API log and event carries five tags: <code>tenant</code>, <code>appVersion</code>, <code>bundleId</code>, <code>configVersion</code> and <code>channel</code>. 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.</p></div>
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Signal</th><th>Alert when</th><th>Action</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Crash-free sessions, per bank and bundle</td><td>Below 99.5% after a rollout step</td><td>Pause the rollout; disable the bundle if it keeps falling</td></tr>
|
||
<tr><td><code>FEATURE_NOT_AVAILABLE</code> rate</td><td>Rises after a release</td><td>The app shows something the server refuses; fix the gating</td></tr>
|
||
<tr><td>OTP and challenge failures, by type</td><td>Jump at one bank</td><td>Check that bank's SMS gateway or policy</td></tr>
|
||
<tr><td>Config fetch failures</td><td>Above 5%</td><td>Check the config service and network</td></tr>
|
||
<tr><td>Phones below the minimum version</td><td>After a forced update</td><td>Bank sends a customer message</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="prose"><p><b>Support lookup:</b> 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 <code>featureService.explain()</code>; the server mirrors it.</p></div>
|
||
|
||
<h3>When something goes wrong</h3>
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Problem</th><th>Fastest fix</th><th>Takes effect</th><th>Who</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>A feature misbehaves at one bank</td><td>Switch it off in the admin portal</td><td>Minutes</td><td>Bank admin or ops</td></tr>
|
||
<tr><td>A new JS bundle crashes on start</td><td>Nothing: Hot Updater rolls that phone back</td><td>Next launch</td><td>Automatic</td></tr>
|
||
<tr><td>A new JS bundle has a bug</td><td>Disable the bundle in the Hot Updater console</td><td>Minutes</td><td>Release manager</td></tr>
|
||
<tr><td>A security step must change now</td><td>Change that bank's security policy</td><td>Next transfer</td><td>Ops, with the bank</td></tr>
|
||
<tr><td>A native bug in an old version</td><td>Ship a fixed store build, then raise the minimum version</td><td>Hours to days</td><td>Platform team, bank</td></tr>
|
||
<tr><td>A bad store build is rolling out</td><td>Halt the staged rollout in the store console</td><td>Hours</td><td>Bank's store account</td></tr>
|
||
<tr><td>One bank's backend is down, or a backend release is bad</td><td>Roll that bank's backend back to the previous image; other banks are not affected</td><td>Minutes</td><td>DevOps, with the bank</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 09 ONE BANK -->
|
||
<section id="bank-update">
|
||
<h2><span class="num">09</span>One bank's update</h2>
|
||
<div class="prose"><p>Every change takes one of five lanes. The lane depends on what changed, not on urgency.</p></div>
|
||
<figure>
|
||
<div class="panel">
|
||
<svg class="dg" viewBox="0 0 1000 446" role="img" aria-label="Five update lanes. Switches and settings: made in the bank admin portal, approved by two people, delivered by config fetch, in minutes. Security policy: made in the flow engine, reviewed by ops and the bank, delivered in the next server reply, from the next transfer. Backend changes: a pull request to the backend codebase, CI and the bank's UAT backend, a rolling deploy to that bank's backend, from the next API call. JS, branding and pack changes: a pull request, CI and bank UAT, Hot Updater OTA, in hours to days. Native changes: a pull request, CI and store review, a store update with forced update, in weeks.">
|
||
<defs>
|
||
<marker id="d8-m" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah" d="M0 0L10 5L0 10z"/></marker>
|
||
</defs>
|
||
<text class="th" x="20" y="30">CHANGE</text>
|
||
<text class="th" x="230" y="30">MADE IN</text>
|
||
<text class="th" x="440" y="30">CHECKED BY</text>
|
||
<text class="th" x="650" y="30">TRAVELS VIA</text>
|
||
<text class="th" x="860" y="30">REACHES PHONES</text>
|
||
<rect class="box-a" x="20" y="46" width="180" height="60" rx="6"/><text class="tt ta" x="34" y="71">Switch or setting</text><text class="ts" x="34" y="89">bank level, live</text>
|
||
<line class="edge" x1="200" y1="76" x2="228" y2="76" marker-end="url(#d8-m)"/>
|
||
<rect class="box-a" x="230" y="46" width="180" height="60" rx="6"/><text class="tt ta" x="244" y="71">Bank admin portal</text><text class="ts" x="244" y="89">or platform ops</text>
|
||
<line class="edge" x1="410" y1="76" x2="438" y2="76" marker-end="url(#d8-m)"/>
|
||
<rect class="box-a" x="440" y="46" width="180" height="60" rx="6"/><text class="tt ta" x="454" y="71">Maker-checker</text><text class="ts" x="454" y="89">two people approve</text>
|
||
<line class="edge" x1="620" y1="76" x2="648" y2="76" marker-end="url(#d8-m)"/>
|
||
<rect class="box-a" x="650" y="46" width="180" height="60" rx="6"/><text class="tt ta" x="664" y="71">Config fetch</text><text class="ts" x="664" y="89">start · resume · sign-in</text>
|
||
<line class="edge" x1="830" y1="76" x2="858" y2="76" marker-end="url(#d8-m)"/>
|
||
<rect class="box-a" x="860" y="46" width="130" height="60" rx="6"/><text class="tt ta" x="874" y="71">Minutes</text><text class="ts" x="874" y="89">no app release</text>
|
||
<rect class="box-a" x="20" y="126" width="180" height="60" rx="6"/><text class="tt ta" x="34" y="151">Security policy</text><text class="ts" x="34" y="169">TPIN, OTP, step-up</text>
|
||
<line class="edge" x1="200" y1="156" x2="228" y2="156" marker-end="url(#d8-m)"/>
|
||
<rect class="box-a" x="230" y="126" width="180" height="60" rx="6"/><text class="tt ta" x="244" y="151">Flow engine policy</text><text class="ts" x="244" y="169">one per bank</text>
|
||
<line class="edge" x1="410" y1="156" x2="438" y2="156" marker-end="url(#d8-m)"/>
|
||
<rect class="box-a" x="440" y="126" width="180" height="60" rx="6"/><text class="tt ta" x="454" y="151">Ops review</text><text class="ts" x="454" y="169">+ bank sign-off</text>
|
||
<line class="edge" x1="620" y1="156" x2="648" y2="156" marker-end="url(#d8-m)"/>
|
||
<rect class="box-a" x="650" y="126" width="180" height="60" rx="6"/><text class="tt ta" x="664" y="151">Next server reply</text><text class="ts" x="664" y="169">inside the flow</text>
|
||
<line class="edge" x1="830" y1="156" x2="858" y2="156" marker-end="url(#d8-m)"/>
|
||
<rect class="box-a" x="860" y="126" width="130" height="60" rx="6"/><text class="tt ta" x="874" y="151">Next transfer</text><text class="ts" x="874" y="169">no app release</text>
|
||
<rect class="box-a" x="20" y="206" width="180" height="60" rx="6"/><text class="tt ta" x="34" y="231">Backend change</text><text class="ts" x="34" y="249">API, rules, fixes</text>
|
||
<line class="edge" x1="200" y1="236" x2="228" y2="236" marker-end="url(#d8-m)"/>
|
||
<rect class="box-a" x="230" y="206" width="180" height="60" rx="6"/><text class="tt ta" x="244" y="231">Pull request</text><text class="ts" x="244" y="249">backend codebase</text>
|
||
<line class="edge" x1="410" y1="236" x2="438" y2="236" marker-end="url(#d8-m)"/>
|
||
<rect class="box-a" x="440" y="206" width="180" height="60" rx="6"/><text class="tt ta" x="454" y="231">CI + bank UAT</text><text class="ts" x="454" y="249">bank's UAT backend</text>
|
||
<line class="edge" x1="620" y1="236" x2="648" y2="236" marker-end="url(#d8-m)"/>
|
||
<rect class="box-a" x="650" y="206" width="180" height="60" rx="6"/><text class="tt ta" x="664" y="231">That bank's backend</text><text class="ts" x="664" y="249">rolling deploy</text>
|
||
<line class="edge" x1="830" y1="236" x2="858" y2="236" marker-end="url(#d8-m)"/>
|
||
<rect class="box-a" x="860" y="206" width="130" height="60" rx="6"/><text class="tt ta" x="874" y="231">Next API call</text><text class="ts" x="874" y="249">no app release</text>
|
||
<rect class="box" x="20" y="286" width="180" height="60" rx="6"/><text class="tt" x="34" y="311">JS, branding, pack</text><text class="ts" x="34" y="329">screens, fixes, logos</text>
|
||
<line class="edge" x1="200" y1="316" x2="228" y2="316" marker-end="url(#d8-m)"/>
|
||
<rect class="box" x="230" y="286" width="180" height="60" rx="6"/><text class="tt" x="244" y="311">Pull request</text><text class="ts" x="244" y="329">app codebase</text>
|
||
<line class="edge" x1="410" y1="316" x2="438" y2="316" marker-end="url(#d8-m)"/>
|
||
<rect class="box" x="440" y="286" width="180" height="60" rx="6"/><text class="tt" x="454" y="311">CI + bank UAT</text><text class="ts" x="454" y="329">per-bank uat channel</text>
|
||
<line class="edge" x1="620" y1="316" x2="648" y2="316" marker-end="url(#d8-m)"/>
|
||
<rect class="box" x="650" y="286" width="180" height="60" rx="6"/><text class="tt" x="664" y="311">Hot Updater OTA</text><text class="ts" x="664" y="329">staged 10% → 100%</text>
|
||
<line class="edge" x1="830" y1="316" x2="858" y2="316" marker-end="url(#d8-m)"/>
|
||
<rect class="box" x="860" y="286" width="130" height="60" rx="6"/><text class="tt" x="874" y="311">Hours to days</text><text class="ts" x="874" y="329">no store review</text>
|
||
<rect class="box" x="20" y="366" width="180" height="60" rx="6"/><text class="tt" x="34" y="391">Native change</text><text class="ts" x="34" y="409">SDK, icon, app ID</text>
|
||
<line class="edge" x1="200" y1="396" x2="228" y2="396" marker-end="url(#d8-m)"/>
|
||
<rect class="box" x="230" y="366" width="180" height="60" rx="6"/><text class="tt" x="244" y="391">Pull request</text><text class="ts" x="244" y="409">+ registry version</text>
|
||
<line class="edge" x1="410" y1="396" x2="438" y2="396" marker-end="url(#d8-m)"/>
|
||
<rect class="box" x="440" y="366" width="180" height="60" rx="6"/><text class="tt" x="454" y="391">CI + store review</text><text class="ts" x="454" y="409">Google, Apple</text>
|
||
<line class="edge" x1="620" y1="396" x2="648" y2="396" marker-end="url(#d8-m)"/>
|
||
<rect class="box" x="650" y="366" width="180" height="60" rx="6"/><text class="tt" x="664" y="391">Store update</text><text class="ts" x="664" y="409">+ forced update</text>
|
||
<line class="edge" x1="830" y1="396" x2="858" y2="396" marker-end="url(#d8-m)"/>
|
||
<rect class="box" x="860" y="366" width="130" height="60" rx="6"/><text class="tt" x="874" y="391">Weeks</text><text class="ts" x="874" y="409">store release</text>
|
||
</svg>
|
||
<div class="legend"><span><i class="sw a"></i>Server-side: no app release at all</span><span><i class="sw"></i>Needs a new app build</span></div>
|
||
</div>
|
||
<figcaption>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.</figcaption>
|
||
</figure>
|
||
|
||
<h3>A bug fix for Bank B, day by day</h3>
|
||
<figure>
|
||
<div class="panel">
|
||
<svg class="dg" viewBox="0 0 1000 320" role="img" aria-label="A Bank B bug fix through the week: Monday the fix is merged and the bundle is deployed to bank-b-uat; Tuesday UAT phones update; Wednesday Bank B QA signs off; Thursday the bundle goes to bank-b-prod at 10 percent; Friday, if metrics are fine, 50 percent; next Monday 100 percent. Crash data from customers' phones drives each rollout decision.">
|
||
<defs>
|
||
<marker id="n7-m" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah" d="M0 0L10 5L0 10z"/></marker>
|
||
</defs>
|
||
<rect class="band" x="20" y="100" width="960" height="70"/>
|
||
<rect class="band" x="20" y="240" width="960" height="70"/>
|
||
<line class="sep" x1="20" y1="30" x2="980" y2="30"/>
|
||
<line class="sep" x1="20" y1="100" x2="980" y2="100"/>
|
||
<line class="sep" x1="20" y1="170" x2="980" y2="170"/>
|
||
<line class="sep" x1="20" y1="240" x2="980" y2="240"/>
|
||
<line class="sep" x1="20" y1="310" x2="980" y2="310"/>
|
||
<text class="th" x="215" y="20" text-anchor="middle">MON</text>
|
||
<text class="th" x="355" y="20" text-anchor="middle">TUE</text>
|
||
<text class="th" x="495" y="20" text-anchor="middle">WED</text>
|
||
<text class="th" x="635" y="20" text-anchor="middle">THU</text>
|
||
<text class="th" x="775" y="20" text-anchor="middle">FRI</text>
|
||
<text class="th" x="915" y="20" text-anchor="middle">NEXT MON</text>
|
||
<text class="tt" x="28" y="69">Platform team</text>
|
||
<text class="tt" x="28" y="139">CI · Hot Updater</text>
|
||
<text class="tt" x="28" y="209">Bank B testers</text>
|
||
<text class="tt" x="28" y="279">Bank B customers</text>
|
||
|
||
<rect class="box" x="155" y="43" width="120" height="44" rx="6"/>
|
||
<text class="tt" x="215" y="62" text-anchor="middle">Fix merged</text>
|
||
<text class="ts" x="215" y="78" text-anchor="middle">to main</text>
|
||
<rect class="box" x="715" y="43" width="120" height="44" rx="6"/>
|
||
<text class="tt" x="775" y="62" text-anchor="middle">Metrics OK?</text>
|
||
<text class="ts" x="775" y="78" text-anchor="middle">→ go to 50%</text>
|
||
<rect class="box" x="855" y="43" width="120" height="44" rx="6"/>
|
||
<text class="tt" x="915" y="62" text-anchor="middle">Still OK?</text>
|
||
<text class="ts" x="915" y="78" text-anchor="middle">→ go to 100%</text>
|
||
|
||
<rect class="box-a" x="155" y="113" width="120" height="44" rx="6"/>
|
||
<text class="tt ta" x="215" y="132" text-anchor="middle">Bundle built</text>
|
||
<text class="tm" x="215" y="148" text-anchor="middle">→ bank-b-uat</text>
|
||
<rect class="box-a" x="575" y="113" width="120" height="44" rx="6"/>
|
||
<text class="tt ta" x="635" y="132" text-anchor="middle">Deploy prod</text>
|
||
<text class="tm" x="635" y="148" text-anchor="middle">bank-b-prod 10%</text>
|
||
<rect class="box-a" x="715" y="113" width="120" height="44" rx="6"/>
|
||
<text class="tt ta" x="775" y="132" text-anchor="middle">Rollout 50%</text>
|
||
<text class="ts" x="775" y="148" text-anchor="middle">one click</text>
|
||
<rect class="box-a" x="855" y="113" width="120" height="44" rx="6"/>
|
||
<text class="tt ta" x="915" y="132" text-anchor="middle">Rollout 100%</text>
|
||
<text class="ts" x="915" y="148" text-anchor="middle">done</text>
|
||
|
||
<rect class="box" x="295" y="183" width="120" height="44" rx="6"/>
|
||
<text class="tt" x="355" y="202" text-anchor="middle">UAT phones</text>
|
||
<text class="ts" x="355" y="218" text-anchor="middle">update on launch</text>
|
||
<rect class="box" x="435" y="183" width="120" height="44" rx="6"/>
|
||
<text class="tt" x="495" y="202" text-anchor="middle">Bank B QA</text>
|
||
<text class="ts" x="495" y="218" text-anchor="middle">signs off</text>
|
||
|
||
<rect class="box" x="575" y="253" width="120" height="44" rx="6"/>
|
||
<text class="tt" x="635" y="272" text-anchor="middle">10% of phones</text>
|
||
<text class="ts" x="635" y="288" text-anchor="middle">on next open</text>
|
||
<rect class="box" x="715" y="253" width="120" height="44" rx="6"/>
|
||
<text class="tt" x="775" y="272" text-anchor="middle">50% of phones</text>
|
||
<text class="ts" x="775" y="288" text-anchor="middle">on next open</text>
|
||
<rect class="box" x="855" y="253" width="120" height="44" rx="6"/>
|
||
<text class="tt" x="915" y="272" text-anchor="middle">Every phone</text>
|
||
<text class="ts" x="915" y="288" text-anchor="middle">on next open</text>
|
||
|
||
<line class="edge" x1="215" y1="87" x2="215" y2="111" marker-end="url(#n7-m)"/>
|
||
<path class="edge" d="M275 135 H355 V181" marker-end="url(#n7-m)"/>
|
||
<line class="edge" x1="415" y1="205" x2="433" y2="205" marker-end="url(#n7-m)"/>
|
||
<path class="edge" d="M555 199 H565 V135 H573" marker-end="url(#n7-m)"/>
|
||
<line class="edge" x1="635" y1="157" x2="635" y2="251" marker-end="url(#n7-m)"/>
|
||
<line class="edge" x1="775" y1="87" x2="775" y2="111" marker-end="url(#n7-m)"/>
|
||
<line class="edge" x1="775" y1="157" x2="775" y2="251" marker-end="url(#n7-m)"/>
|
||
<line class="edge" x1="915" y1="87" x2="915" y2="111" marker-end="url(#n7-m)"/>
|
||
<line class="edge" x1="915" y1="157" x2="915" y2="251" marker-end="url(#n7-m)"/>
|
||
<path class="ret" d="M695 275 H705 V65 H713" marker-end="url(#n7-m)"/>
|
||
<path class="ret" d="M835 275 H845 V65 H853" marker-end="url(#n7-m)"/>
|
||
</svg>
|
||
<div class="legend"><span><i class="sw a"></i>Bank B's OTA channels</span><span><i class="sw d"></i>Crash and error data that drives the next step</span></div>
|
||
</div>
|
||
<figcaption>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.</figcaption>
|
||
</figure>
|
||
|
||
<h3>Every kind of change, for Bank B</h3>
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Bank B wants…</th><th>Lane</th><th>Steps</th><th>Reaches customers</th><th>Who acts</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Loans off today, during an incident</td><td><span class="pill a">switch</span></td><td>Admin portal → second approver → config v+1 → phones refresh on resume</td><td>Minutes</td><td>Bank B admin</td></tr>
|
||
<tr><td>A lower RTGS minimum</td><td><span class="pill a">setting</span></td><td>Same as a switch</td><td>Minutes</td><td>Bank B admin</td></tr>
|
||
<tr><td>OTP only up to ₹5,000</td><td><span class="pill a">policy</span></td><td>Ops edits Bank B's policy on the UAT org → Bank B tests → same policy on production</td><td>Next transfer after go-live</td><td>Ops, Bank B</td></tr>
|
||
<tr><td>A transfer-screen bug fixed</td><td><span class="pill">OTA</span></td><td>The week shown above</td><td>3–7 days; a hotfix the same day</td><td>Platform team, Bank B QA</td></tr>
|
||
<tr><td>New logo or colours</td><td><span class="pill">OTA</span></td><td>Change <code>tenants/bank-b/</code> → only Bank B's bundles rebuild → UAT → prod</td><td>Days</td><td>Platform team, Bank B</td></tr>
|
||
<tr><td>A backend fix or a new API</td><td><span class="pill a">backend</span></td><td>Backend pull request → new image → Bank B's UAT backend → Bank B signs off → Bank B's production backend</td><td>Next API call after the deploy</td><td>Backend team, Bank B</td></tr>
|
||
<tr><td>A new feature (for example UPI)</td><td><span class="pill a">backend</span>, <span class="pill">OTA</span>, then <span class="pill a">switch</span></td><td>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</td><td>After the backend upgrade, the next train and UAT</td><td>Backend and platform teams, Bank B</td></tr>
|
||
<tr><td>A new native SDK, app icon or app name</td><td><span class="pill">store</span></td><td>Native train → Bank B UAT build → submit in Bank B's account → staged rollout → raise minimum version</td><td>1–3 weeks</td><td>Platform team, Bank B store owner</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="prose">
|
||
<p><b>What keeps Bank B's change away from other banks</b></p>
|
||
<ul>
|
||
<li>A change only under <code>tenants/bank-b/</code>: CI detects it and rebuilds only Bank B's bundles.</li>
|
||
<li>A change to shared code: every bank gets it in UAT, but each bank's production moves only after its own sign-off.</li>
|
||
<li>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.</li>
|
||
<li>On the phone: the channel and the tenant guard stop any other bank's bundle from running.</li>
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 10 SECURITY -->
|
||
<section id="security">
|
||
<h2><span class="num">10</span>Security</h2>
|
||
<div class="prose">
|
||
<ol>
|
||
<li><b>The server is the boundary.</b> Every business API checks the bank, the role and the customer again. App-side gating is for the customer's convenience.</li>
|
||
<li><b>No mock code in release builds.</b> Fixtures and fake OTPs exist only in the dev environment, and CI checks the release bundle.</li>
|
||
<li><b>Fail closed.</b> An unknown feature key, challenge type or unreadable config means off or stop.</li>
|
||
<li><b>The bank's off is final.</b> Session and device layers cannot re-enable it; release builds let the device layer switch features off only.</li>
|
||
<li><b>Kill switches survive offline,</b> and the cached config only moves forward in version.</li>
|
||
<li><b>No leaks between customers or banks:</b> 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.</li>
|
||
<li><b>HTTPS with per-bank certificate pinning before production,</b> 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.</li>
|
||
<li><b>Signed code:</b> a store key per bank, signed OTA bundles, production deploys only from CI.</li>
|
||
<li><b>Crash reports without personal data,</b> stored where the banks' data rules allow.</li>
|
||
<li><b>Every change recorded:</b> two-person approval and an audit log for switches and policies.</li>
|
||
</ol>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 11 ROADMAP -->
|
||
<section id="roadmap">
|
||
<h2><span class="num">11</span>Roadmap</h2>
|
||
<div class="prose"><p>Estimates are rough, for one mobile developer. The backend work runs in parallel from week one.</p></div>
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Phase</th><th>Work</th><th>Effort</th><th>Unblocks</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>0 · Release blockers</td><td>Build environments and the mock guard; delete the OTP-<code>123456</code> path; real signing; error boundaries and crash reporting; connect the feature config (HTTPS, 4-second limit, version check)</td><td class="nw">5 days</td><td>Any build for customers</td></tr>
|
||
<tr><td>1 · Feature engine</td><td><code>orgId</code> on the config request; device layer off-only in release; wait for session flags; refresh on resume; skip no-op re-renders; payment lease</td><td class="nw">3 days</td><td>Reliable live switches</td></tr>
|
||
<tr><td>2 · Tenant packs</td><td>First pack from today's values, including the backend address per environment and certificate pins; <code>@tenant</code>; settings; registry; <code>AppText</code></td><td class="nw">4 days</td><td>A second bank in code</td></tr>
|
||
<tr><td>3 · Build and CI</td><td>Flavors (bank × environment), iOS schemes, release script, PR and release pipelines, version scheme</td><td class="nw">5 days</td><td>Repeatable, signed releases</td></tr>
|
||
<tr><td>4 · OTA</td><td>Self-hosted Hot Updater, channels, tenant guard, signed bundles, minimum-version forced update</td><td class="nw">3 days</td><td>JS fixes without the store</td></tr>
|
||
<tr><td>5 · Flow framework</td><td>Challenge runner; transfer, TPIN, MPIN and registration moved onto it</td><td class="nw">8 days</td><td>Different flows per bank</td></tr>
|
||
<tr><td>B1 · Backend features (parallel)</td><td>Config per bank with entitlements, catalog import, admin portal with approvals and audit, API enforcement, challenge protocol, <code>/meta</code> version report</td><td class="nw">Backend team</td><td>Live per-bank control</td></tr>
|
||
<tr><td>B2 · Per-bank deployment (parallel)</td><td>Infrastructure scripts for one bank's UAT and production backends, CI deploys per bank with sign-off, ops console link, central monitoring, HTTPS certificates</td><td class="nw">DevOps</td><td>A backend for every bank</td></tr>
|
||
<tr><td>6 · Second bank</td><td>The onboarding checklist in section 02</td><td class="nw">1 day</td><td>Bank two live</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="prose"><p>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.</p></div>
|
||
</section>
|
||
|
||
<!-- 12 RISKS -->
|
||
<section id="risks">
|
||
<h2><span class="num">12</span>Risks</h2>
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Risk</th><th>Why it matters</th><th>How we reduce it</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Backend work slips</td><td>Live control and server-driven steps wait on it</td><td>Pack defaults and adapters keep the app shippable; start with an ops-only config API</td></tr>
|
||
<tr><td>Banks are slow to give store access or sign off</td><td>Releases stall per bank</td><td>Start account setup in phase 0; put UAT timelines in the contract</td></tr>
|
||
<tr><td>The build matrix grows</td><td>Banks × platforms × environments per release</td><td>Parallel CI, rebuild only affected banks, bundle diffing</td></tr>
|
||
<tr><td>Banks drift onto different native versions</td><td>JS must work on several binaries</td><td>Two-version support window, forced update, train discipline</td></tr>
|
||
<tr><td>Running many backends</td><td>Patching, monitoring and backups for every bank</td><td>One image, infrastructure scripts, automated deploys, central dashboards, written runbooks</td></tr>
|
||
<tr><td>Bank backends drift onto different versions</td><td>The app must work with several backend versions</td><td>Features gated by what each backend reports; at most two backend versions in production; additive APIs only</td></tr>
|
||
<tr><td>The OTA server is compromised</td><td>One door into every bank's app</td><td>Signed bundles, CI-only production deploys, audit of every deploy</td></tr>
|
||
<tr><td>Too many flags</td><td>Combinations become impossible to test</td><td>Retire leaf flags; snapshot test of each bank's effective config; quarterly catalog review</td></tr>
|
||
<tr><td>Too many per-bank overrides</td><td>Slot overrides drift from the shared screens</td><td>Lowest rung first; at most three overrides per bank; each has an owner</td></tr>
|
||
<tr><td>Crash reports carry personal data</td><td>Banking privacy rules</td><td>Scrub before sending; choose storage that meets the banks' data rules</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 13 DECISIONS -->
|
||
<section id="decisions">
|
||
<h2><span class="num">13</span>Decisions needed</h2>
|
||
<div class="tbl">
|
||
<table>
|
||
<thead><tr><th>Decision</th><th>Recommendation</th><th>Who decides</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>One backend for all banks, or one per bank?</td><td><span class="pill a">Decided</span> One backend codebase, deployed separately for each bank and environment</td><td>Decided 12 Oct 2026</td></tr>
|
||
<tr><td>Where does each bank's backend run?</td><td>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</td><td>DevOps, security, banks</td></tr>
|
||
<tr><td>Whose store accounts publish each app?</td><td>Each bank's own Google Play and Apple accounts. Apple's guideline 4.2.6 expects the content owner to submit template-based apps</td><td>Product, banks</td></tr>
|
||
<tr><td>Adopt the server-driven security protocol?</td><td>Yes; the app uses adapters on today's API until it is ready</td><td>Backend lead, security</td></tr>
|
||
<tr><td>Who operates the admin portal?</td><td>Bank staff with two-person approval; our ops for onboarding and emergencies</td><td>Product, banks</td></tr>
|
||
<tr><td>Release rhythm</td><td>Weekly JS train, monthly native train, backend releases per bank on sign-off, hotfix any day</td><td>Mobile and backend leads</td></tr>
|
||
<tr><td>Where OTA bundles are hosted</td><td>Self-hosted Hot Updater on our own cloud, with signed bundles</td><td>Security, DevOps</td></tr>
|
||
<tr><td>Crash reporting tool</td><td>One that can scrub personal data and store it where the banks' rules allow</td><td>Security</td></tr>
|
||
<tr><td>What a restricted customer sees</td><td>Hide what the bank does not offer; explain what the customer can unlock ("Complete KYC to apply")</td><td>Product</td></tr>
|
||
<tr><td>How many app and backend versions we support</td><td>The last two minor app versions, with forced update below that; at most two backend versions in production at once</td><td>Mobile and backend leads, banks</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<footer class="page">
|
||
<p>Based on a review of the BankCBS repository on 12 October 2026. File references point into that repository. Code-level specifications: <code>docs/MULTI_BANK_ARCHITECTURE.md</code> (banks, features, build, backend) and <code>docs/BANK_FLOW_VARIANTS.md</code> (per-bank flows). This page is <code>docs/multi-bank-proposal.html</code>.</p>
|
||
</footer>
|
||
</main>
|
||
</div>
|