The brief
Crédit Populaire d'Algérie wanted to let new customers open an account from their phone, with no branch visit and no human reviewing a photo of an ID card by eye. That's a simple sentence that hides a genuinely hard problem: you have to prove, in a few seconds, that the person holding the phone is a real human, that they match the ID they're presenting, and that the ID itself hasn't been tampered with — all while running on whatever Android device a customer happens to own, not a lab-grade test phone.
The team building this was small: a React Native mobile client, a NestJS backend split into clean modules, and three separate computer-vision problems bolted onto the identity flow. My job was to make those three pieces feel like one smooth, boring five-step form instead of three research projects stapled together.
Three problems wearing one UI
Document extraction came first. We used OCR combined with FastMRZ to read the machine-readable zone on the national ID — the two lines of characters printed along the bottom that encode name, date of birth and document number in a fixed format. MRZ parsing is forgiving of OCR noise because it's a checksum-validated format, which made it the natural first pass before falling back to full-frame OCR for fields that aren't in the MRZ.
Face matching came next, using DeepFace to compare the portrait on the extracted document against a live selfie. The interesting engineering problem here wasn't the matching model itself — it's a solved problem at this point — it was normalizing wildly inconsistent input: a phone photo of a laminated card under fluorescent branch lighting versus a front-facing camera selfie in someone's kitchen. We spent more time on image preprocessing (crop, lighting normalization, glare rejection) than on the matching threshold itself.
Liveness detection closed the loop with OpenCV, making sure the 'live selfie' wasn't a photo of a photo. We used simple, resilient signals — prompted micro-movements and blink detection — rather than anything exotic, because the failure mode we cared most about was false rejections of legitimate customers on cheap devices, not defeating a sophisticated attacker with studio equipment.
Making it feel instant
None of this matters if a customer gives up halfway through. We kept the perceived latency down by running document-side checks (MRZ validation, blur/glare detection) client-side before anything hits the network, so a bad capture gets rejected in place instead of round-tripping to the server first. The heavier models — face match and liveness scoring — run server-side behind a NestJS module boundary that we could scale independently once we saw where the queue actually backed up.
The result held up to a sub-second feel end-to-end on mid-range Android hardware for the common path, with the slow path reserved for genuinely bad captures that needed a retry — which is exactly where you want the friction to live.
What regulated banking actually adds
The computer vision was the fun part. The unglamorous majority of the work was everything banking compliance demands around it: encrypting document images at rest and in transit, keeping a tamper-evident audit trail of every verification decision, building in a manual-review escalation path for anything the automated pipeline wasn't confident about, and making sure nothing about the flow silently exposed personally identifiable information in logs. None of that shows up in a demo video, and all of it is the difference between a prototype and something a bank can actually put its name on.