An account-opening flow that onboards 600 people a day
Bajet, a consumer digital banking PWA · Simorq Tejarat · 2021 to 2024
- Next.js
- TypeScript
- Redux Toolkit
- Material UI
Outcome
- 600+ verified users onboarded per day in production
- Flow survives refresh, session expiry, and asynchronous verification coming back late
The problem
Opening a bank account digitally is a long form wearing a costume. The user has to supply identity information, pass verification, and agree to things, and every one of those steps is a place where they can fail, get confused, or simply close the tab. The flow is also not really a flow: steps can fail asynchronously and come back later, some can be retried and some cannot, and the user can leave and return hours afterwards expecting to be where they left off.
The naive version of this is a wizard with a step counter in component state. That version breaks the first time someone refreshes the page.
At Bajet the journey was a tree rather than a line: personal and identity data, bank-side checks, a credit check, a payment step for the account-opening fee, then card selection and delivery details. The checks returned on their own schedule. The step that broke the model was payment. The gateway had to open in a new tab, and its callback came back to the base URL of the flow rather than to the step that had started it. Because step state lived in the client, the returning tab knew nothing: the user paid successfully and landed back at the beginning.
What I did
The flow was driven from client state, one step handing off to the next, which works until a step leaves the page entirely. Payment did exactly that, and what to do after it came back was the longest-running argument on the team.
What I argued for, and what we shipped, was a BroadcastChannel between the two tabs. The callback tab’s only job became announcing the payment result; the original tab listened, advanced its own state, and closed the other one. The alternative on the table was rebuilding the flow so the returning tab could reconstruct its position from the server — the more correct answer, and a rewrite of the whole journey rather than a change to one step. The BroadcastChannel version was small, it shipped, and it held.
Form handling was the other half. Validation had to run identically on the client and against the API's own rules, which meant one schema and no second source of truth about what a valid national ID or phone number looked like.
What I would do differently
I would spend real time on the shape of the flow before writing any of it. Past a certain complexity, holding step state in the client and stitching over the gaps costs more than routing would have. Routed steps would have made the payment callback a non-problem instead of something I had to be clever about, and I was clever about it because the architecture had already been chosen.