Files
CBS-Mobile-App/docs/platform-blueprint.html
Abin ToppoandClaude Opus 5.5 f960a119e8 Add Positive Pay, statement guide v1.0, onboarding, and switch every feature on
- 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>
2026-10-14 18:18:40 +05:30

930 lines
66 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<title>Multi-Bank Platform Blueprint</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; 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;
--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; --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; --code-bg: #1C2630;
color-scheme: dark;
}
* { box-sizing: border-box; }
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; }
.wrap { max-width: 1160px; margin: 0 auto; display: grid; grid-template-columns: 190px minmax(0, 1fr); gap: 56px; }
@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) + 32px); 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: 5px 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 8px; border-bottom: 1px solid var(--line); margin-bottom: 8px; }
.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: 700px; margin: 0 0 22px; color: var(--ink); }
.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 26px; }
@media (max-width: 1020px) { .toc-inline { display: flex; } }
.qa { 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 34px; }
@media (max-width: 860px) { .qa { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 480px) { .qa { grid-template-columns: minmax(0, 1fr); } }
.qa div { background: var(--surface); padding: 16px 16px 18px; min-width: 0; }
.qa .q { font-family: var(--font-display); font-weight: 600; font-size: 17px; line-height: 1.25; margin: 0 0 6px; }
.qa .a { font-size: 14px; color: var(--muted); margin: 0 0 8px; line-height: 1.45; }
.qa a { font-size: 13px; text-decoration: none; font-weight: 500; }
section { padding-block: 40px 8px; border-top: 1px solid var(--line); }
section:first-of-type { border-top: 0; }
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: 32px 0 8px; text-wrap: balance; }
.prose { max-width: 720px; }
.prose p, .prose li { margin: 0 0 12px; }
.prose ul, .prose ol { padding-left: 22px; margin: 0 0 14px; }
figure { margin: 26px 0 30px; }
.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; }
/* 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 .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: 20px 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: 16px 0 24px; 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; }
.pill { display: inline-block; font-family: var(--font-mono); font-size: 11.5px; 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); }
.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: 14px 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; }
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; } }
html { scroll-behavior: smooth; }
</style>
<div class="wrap">
<nav class="toc" aria-label="Contents">
<p>Contents</p>
<a href="#glance"><span>01</span>The platform at a glance</a>
<a href="#bank"><span>02</span>What a bank is made of</a>
<a href="#app"><span>03</span>Inside the app</a>
<a href="#features"><span>04</span>Who gets which feature</a>
<a href="#flows"><span>05</span>When a bank's flow differs</a>
<a href="#manage"><span>06</span>Managing the banks</a>
<a href="#updates"><span>07</span>How every change ships</a>
<a href="#security"><span>08</span>Security rules</a>
<a href="#roadmap"><span>09</span>Build order</a>
</nav>
<main>
<header class="page">
<p class="eyebrow">Architecture · BankCBS mobile platform</p>
<h1>Multi-Bank Platform Blueprint</h1>
<p class="lede">One React Native codebase ships a separate app for every bank. This page shows how the pieces fit together, how we run each bank, and how every kind of change reaches customers' phones.</p>
<div class="meta">
<span><b>Status</b> Proposal</span>
<span><b>Covers</b> mobile app, backend contract, delivery, operations</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="#glance">At a glance</a><a href="#bank">A bank</a><a href="#app">The app</a><a href="#features">Features</a><a href="#flows">Flows</a><a href="#manage">Managing banks</a><a href="#updates">Updates</a><a href="#security">Security</a><a href="#roadmap">Build order</a>
</nav>
<div class="qa">
<div><p class="q">Which bank is this app?</p><p class="a">A tenant pack, chosen when the app is built.</p><a href="#bank">What a bank is made of</a></div>
<div><p class="q">Which features does it show?</p><p class="a">Five layers: product, bank, live bank switches, the customer's session, the device.</p><a href="#features">Who gets which feature</a></div>
<div><p class="q">How does a feature work here?</p><p class="a">Settings, server-driven security steps, journey variants. Never <code>if (bank)</code>.</p><a href="#flows">When a bank's flow differs</a></div>
<div><p class="q">How does a change reach phones?</p><p class="a">Four lanes, from minutes (switches) to days (store builds).</p><a href="#updates">How every change ships</a></div>
</div>
</header>
<!-- 01 -->
<section id="glance">
<h2><span class="num">01</span>The platform at a glance</h2>
<div class="prose">
<p>Code lives in one repository. A release script builds it once per bank and ships it in two ways: store builds for native changes, OTA bundles for JavaScript. On the phone, the app asks the backend what this bank and this customer may use, and the backend checks every action again.</p>
</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; installed bank apps fetch config, answer security challenges and call business APIs on the platform backend, which the platform ops console and the bank admin portal configure.">
<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" x1="780" y1="211" x2="849" y2="211" marker-end="url(#d1-m)"/>
<text class="ts" x="815" y="203" text-anchor="middle">config</text>
<line class="edge" x1="780" y1="273" x2="849" y2="273" marker-end="url(#d1-m)"/>
<text class="ts" x="815" y="265" text-anchor="middle">challenge</text>
<line class="edge" x1="780" y1="335" x2="849" y2="335" marker-end="url(#d1-m)"/>
<text class="ts" x="815" y="327" text-anchor="middle">API calls</text>
<rect class="grp" x="840" y="150" width="150" height="250" rx="8"/>
<text class="tt" x="852" y="172">Platform backend</text>
<rect class="box" x="852" y="186" width="126" height="50" rx="6"/>
<text class="tt" x="864" y="207">Config service</text>
<text class="ts" x="864" y="225">flags, settings</text>
<rect class="box" x="852" y="248" width="126" height="50" rx="6"/>
<text class="tt" x="864" y="269">Flow engine</text>
<text class="ts" x="864" y="287">security steps</text>
<rect class="box" x="852" y="310" width="126" height="50" rx="6"/>
<text class="tt" x="864" y="331">Business APIs</text>
<text class="ts" x="864" y="349">enforces access</text>
<rect class="box" x="620" y="20" width="180" height="56" rx="6"/>
<text class="tt" x="636" y="43">Platform ops console</text>
<text class="ts" x="636" y="62">our team · onboarding</text>
<rect class="box-a" x="820" y="20" width="170" height="56" rx="6"/>
<text class="tt ta" x="836" y="43">Bank admin portal</text>
<text class="ts" x="836" y="62">bank staff · approvals</text>
<path class="edge" d="M710 76 V112 H870 V147" marker-end="url(#d1-m)"/>
<text class="ts" x="790" y="104" text-anchor="middle">sets up org, policy</text>
<line class="edge-a" x1="905" y1="76" x2="905" y2="147" marker-end="url(#d1-ma)"/>
<text class="ts" x="912" y="118">toggles</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. Everything else is shared, so a fix in the shared core reaches every bank at once.</figcaption>
</figure>
<div class="prose">
<ul>
<li><b>Bank differences are data, not code branches.</b> They live in tenant packs and in server config.</li>
<li><b>The server decides security.</b> Which checks a transfer needs, and whether a customer may use a feature at all.</li>
<li><b>Every change has a lane.</b> A switch flips in minutes, a JS fix ships in hours, a native change takes days. Pick the fastest lane that is allowed (<a href="#updates">section 07</a>).</li>
</ul>
</div>
</section>
<!-- 02 -->
<section id="bank">
<h2><span class="num">02</span>What a bank is made of</h2>
<div class="prose">
<p>"Bank A" is not one thing. It exists in four places, and onboarding a bank means filling in all four. The bank's <code>id</code> (for example <code>bank-a</code>) ties them together.</p>
</div>
<div class="cards">
<div class="card">
<h4>Code</h4>
<p class="where">tenants/bank-a/</p>
<ul>
<li>Identity: <code>orgId</code>, name, logo, brand colours, wording</li>
<li>Feature defaults for the first launch and offline</li>
<li>Settings, flow choices, and rare strategies or slot overrides</li>
</ul>
</div>
<div class="card">
<h4>Native shell</h4>
<p class="where">Android flavor bankA · iOS scheme BankA</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-a-prod</code></li>
</ul>
</div>
<div class="card">
<h4>Server</h4>
<p class="where">org bank-a in the platform backend</p>
<ul>
<li>Live feature switches, settings, journey variants</li>
<li>Security challenge policy and customer rules</li>
<li>Admin users, 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 A's own developer account</li>
<li>OTA channels <code>bank-a-uat</code> and <code>bank-a-prod</code></li>
<li>One registry entry: flavor, app IDs, version</li>
</ul>
</div>
</div>
<div class="note"><b>Two lists must agree.</b> <code>tenants/registry.json</code> (mobile) and the org table (backend) list the same banks with the same <code>id</code>. CI checks that every registry entry has a pack, and the onboarding checklist creates both.</div>
</section>
<!-- 03 -->
<section id="app">
<h2><span class="num">03</span>Inside the app</h2>
<div class="prose">
<p>The app keeps its current layering. Two things are added: a tenant layer that supplies the bank's identity at build time, and a flows layer that runs security steps and journeys. Bank-specific input enters in only two places: the tenant pack when building, and server config when running.</p>
</div>
<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 · 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 &amp; 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>
</section>
<!-- 04 -->
<section id="features">
<h2><span class="num">04</span>Who gets which feature</h2>
<div class="prose">
<p>Every screen, tab, button and action has a feature key such as <code>loans.apply</code>. The app resolves each key through five layers. A later layer overrides an earlier one, with one exception that protects the bank: a <b>false</b> from the bank or from the session is final.</p>
</div>
<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 on top: 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>
<h3>Worked example</h3>
<div class="prose"><p>Bank A has loans and cards, but has switched cards off live during an outage. Bank B has no loans. Customer X at Bank A has KYC pending.</p></div>
<div class="tbl">
<table>
<thead><tr><th>Feature</th><th>Bank A · customer X</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 (KYC pending)</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></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 builds ignore it</td></tr>
</tbody>
</table>
</div>
<div class="prose">
<ul>
<li><b>The root key decides a module.</b> Turning <code>loans</code> on for a bank turns on the whole module; children are switched off only to remove one capability.</li>
<li><b>New keys ship off.</b> An OTA update never shows a new feature to a bank that did not ask for it.</li>
<li><b>The server checks again.</b> Hiding a button is for the customer's convenience. The API refuses the call with <code>FEATURE_NOT_AVAILABLE</code> either way.</li>
</ul>
</div>
</section>
<!-- 05 -->
<section id="flows">
<h2><span class="num">05</span>When a bank's flow differs</h2>
<div class="prose">
<p>Some banks want the same feature to work differently: other security checks, other steps, other limits. There are five ways to allow that. Use the lowest rung that fits; each step up adds power and upkeep.</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 added only when a real bank needs them.</figcaption>
</figure>
<h3>Security steps come from the server</h3>
<div class="prose">
<p>If the app decided "Bank B skips the TPIN", a modified app could skip it anywhere. So the server holds each bank's policy and replies one step at a time: "now the TPIN", "now the OTP", "done". The app only knows how to show each kind of check.</p>
</div>
<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>Today's API already works this way for IMPS: <code>initiate</code> takes the TPIN, and <code>verify-otp</code> takes the OTP. An adapter maps it onto the same runner, so the app can move to this structure 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>
<!-- 06 -->
<section id="manage">
<h2><span class="num">06</span>Managing the banks</h2>
<div class="prose">
<p>Each bank moves through the same lifecycle. After go-live, every change, large or small, goes back through that bank's UAT before production.</p>
</div>
<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>
</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-m)"/>
<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: switch the bank's features off live, unpublish the store listing, retire the channels, archive the org.</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>provides account access</td><td><b>owns</b></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 challenge 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>submits</b></td><td>—</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, with two-person approval</td><td>Its own bank's switches, settings, journey variants, customer blocks</td><td>Other banks, security policy, app code</td></tr>
<tr><td>Platform ops console</td><td>Our operations team</td><td>Onboarding, security policies, emergency kill for any bank</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 analytics event carries five tags: <code>tenant</code>, <code>appVersion</code>, <code>bundleId</code>, <code>configVersion</code> and <code>channel</code>. With those, one dashboard filters to any bank and support can see exactly what a customer was running.</p>
<ul>
<li>Crash-free sessions per bank and per OTA bundle, to decide when to widen a rollout</li>
<li>OTA adoption per channel, and how many phones are still on old native versions</li>
<li><code>FEATURE_NOT_AVAILABLE</code> rate: if it rises, the app is showing something the server refuses</li>
<li>Challenge failure rates by type, for example OTP not delivered at one bank</li>
</ul>
</div>
<h3>Adding a bank</h3>
<ol class="steps">
<li>Copy <code>tenants/_template</code> to <code>tenants/bank-c</code>; add the identity, brand colours, logo, features and settings.</li>
<li>Run the contrast check with Bank C's colours and adjust until it passes.</li>
<li>Add Bank C to <code>tenants/registry.json</code>; the Android flavor and CI job appear automatically.</li>
<li>Add launcher icons and iOS configuration for Bank C.</li>
<li>DevOps adds Bank C's signing key to CI secrets.</li>
<li>Ops creates org <code>bank-c</code>: feature switches, customer rules, security policy, admin users.</li>
<li>The bank creates its store listing and adds our team.</li>
<li>Build to <code>bank-c-uat</code>, the bank tests and signs off, then release to production.</li>
</ol>
</section>
<!-- 07 -->
<section id="updates">
<h2><span class="num">07</span>How every change ships</h2>
<div class="prose">
<p>There are four lanes. Choose by what changed, not by urgency: a native change can never take the OTA lane, and a switch never needs a release.</p>
</div>
<figure>
<div class="panel">
<svg class="dg" viewBox="0 0 1000 366" role="img" aria-label="Four 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. JS, branding and pack changes: a pull request, CI and bank UAT, Hot Updater OTA, in hours. Native changes: a pull request, CI and store review, a store update with forced update, in days.">
<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" x="20" y="206" width="180" height="60" rx="6"/><text class="tt" x="34" y="231">JS, branding, pack</text><text class="ts" x="34" y="249">screens, fixes, logos</text>
<line class="edge" x1="200" y1="236" x2="228" y2="236" marker-end="url(#d8-m)"/>
<rect class="box" x="230" y="206" width="180" height="60" rx="6"/><text class="tt" x="244" y="231">Pull request</text><text class="ts" x="244" y="249">one codebase</text>
<line class="edge" x1="410" y1="236" x2="438" y2="236" marker-end="url(#d8-m)"/>
<rect class="box" x="440" y="206" width="180" height="60" rx="6"/><text class="tt" x="454" y="231">CI + bank UAT</text><text class="ts" x="454" y="249">per-bank uat channel</text>
<line class="edge" x1="620" y1="236" x2="648" y2="236" marker-end="url(#d8-m)"/>
<rect class="box" x="650" y="206" width="180" height="60" rx="6"/><text class="tt" x="664" y="231">Hot Updater OTA</text><text class="ts" x="664" y="249">staged 10% → 100%</text>
<line class="edge" x1="830" y1="236" x2="858" y2="236" marker-end="url(#d8-m)"/>
<rect class="box" x="860" y="206" width="130" height="60" rx="6"/><text class="tt" x="874" y="231">Hours</text><text class="ts" x="874" y="249">no store review</text>
<rect class="box" x="20" y="286" width="180" height="60" rx="6"/><text class="tt" x="34" y="311">Native change</text><text class="ts" x="34" y="329">SDK, icon, app ID</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">+ registry version</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 + store review</text><text class="ts" x="454" y="329">Google, Apple</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">Store update</text><text class="ts" x="664" y="329">+ forced update</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">Days</text><text class="ts" x="874" y="329">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 two blue lanes change only server data, so they can be undone as quickly as they were made. The other two ship code and go through UAT.</figcaption>
</figure>
<h3>Which change goes where</h3>
<div class="tbl">
<table>
<thead><tr><th>Example</th><th>Lane</th><th>Scope</th></tr></thead>
<tbody>
<tr><td>Turn loans off for Bank B during an incident</td><td><span class="pill a">switch</span></td><td>One bank</td></tr>
<tr><td>Change Bank A's RTGS minimum or lock timeout</td><td><span class="pill a">setting</span></td><td>One bank</td></tr>
<tr><td>Block one customer from adding payees for 24 hours</td><td><span class="pill a">switch</span> session rule</td><td>One customer</td></tr>
<tr><td>Raise Bank B's OTP-only limit from ₹5,000 to ₹10,000</td><td><span class="pill a">policy</span></td><td>One bank</td></tr>
<tr><td>Fix a bug on the transfer screen</td><td><span class="pill">OTA</span></td><td>Every bank, channel by channel</td></tr>
<tr><td>New logo or colours for Bank C</td><td><span class="pill">OTA</span></td><td>One bank</td></tr>
<tr><td>New screen behind a new feature key (ships off)</td><td><span class="pill">OTA</span>, then <span class="pill a">switch</span> per bank</td><td>Banks that opt in</td></tr>
<tr><td>New native SDK, permission, app icon or app ID</td><td><span class="pill">store</span></td><td>Every bank that needs it</td></tr>
</tbody>
</table>
</div>
<h3>The OTA pipeline</h3>
<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>CI also runs the feature audit, the tenant matrix (every pack is valid) and the no-bank-branching lint rule. If a bundle crashes before the first screen, Hot Updater rolls that phone back by itself.</figcaption>
</figure>
<div class="note"><b>Suggested rhythm.</b> A weekly JS train: cut on Monday, bank UAT Tuesday to Wednesday, staged production from Thursday. A monthly native train. Hotfixes any day, on the same path with a faster sign-off.</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 only reaches the native version it was built for, so JavaScript never runs on a binary that lacks its native code.</figcaption>
</figure>
<div class="prose">
<ul>
<li>The app ignores config keys it does not know, so the server can publish ahead of the apps.</li>
<li>The backend keeps every API that the oldest supported app version uses. Remove a field only after the minimum version has moved past it.</li>
<li>The minimum supported version lives in server config. Below it, the app sends the customer to the store.</li>
</ul>
</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 challenge 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>
</tbody>
</table>
</div>
</section>
<!-- 08 -->
<section id="security">
<h2><span class="num">08</span>Security rules</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. The app's gating is for the customer's convenience.</li>
<li><b>Fail closed.</b> An unknown feature key, an unknown challenge type or an unreadable config means "off" or "stop", never "allow".</li>
<li><b>The bank's off is final.</b> Session and device layers cannot re-enable it, and release builds let the device layer switch features off only.</li>
<li><b>Kill switches survive offline.</b> The last bank config is cached and applied on a cold start without network.</li>
<li><b>No leaks between customers or banks.</b> The session layer is wiped on sign-out, each bank's bundle contains only its own pack, and a tenant guard refuses to run a bundle in the wrong bank's app.</li>
<li><b>HTTPS before production.</b> Config and challenge replies must not be changeable in transit.</li>
<li><b>Signed code.</b> Separate store signing keys per bank, and signed OTA bundles.</li>
<li><b>Every change is recorded.</b> Two-person approval and an audit log for switches and policies.</li>
</ol>
</div>
</section>
<!-- 09 -->
<section id="roadmap">
<h2><span class="num">09</span>Build order</h2>
<div class="prose"><p>The early phases change nothing customers can see. They make the current app ready for a second bank. Sizes are rough, for one developer.</p></div>
<div class="tbl">
<table>
<thead><tr><th>Phase</th><th>Work</th><th>Size</th></tr></thead>
<tbody>
<tr><td>0 · Prepare</td><td>Branch, commit the work in progress, real release signing</td><td>½ day</td></tr>
<tr><td>1 · Harden the feature engine</td><td>Send <code>orgId</code> with config, 4-second fetch limit, device layer off-only, wait for session flags, refresh on resume</td><td>1–2 days</td></tr>
<tr><td>2 · Tenant packs</td><td>Move today's identity into the first pack, Metro <code>@tenant</code>, settings</td><td>2–3 days</td></tr>
<tr><td>3 · Build and delivery</td><td>Registry, Android flavors, iOS schemes, release script, CI matrix</td><td>2–3 days</td></tr>
<tr><td>F · Flow framework</td><td>Challenge runner, then transfer, TPIN, MPIN and registration onto it</td><td>6–8 days</td></tr>
<tr><td>4 · Backend</td><td>Config with entitlements, challenge protocol, admin portal, API enforcement</td><td>Backend team</td></tr>
<tr><td>5 · OTA per bank</td><td>Hot Updater channels, tenant guard, bundle signing</td><td>1 day</td></tr>
<tr><td>6 · Second bank</td><td>Follow the checklist in section 06</td><td>½ day</td></tr>
</tbody>
</table>
</div>
</section>
<footer class="page">
<p>Full specifications, with code and file-by-file changes, are in the repository: <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/platform-blueprint.html</code>.</p>
</footer>
</main>
</div>