DEV Community

Muhammad Zeeshan Farooq
Muhammad Zeeshan Farooq

Posted on

Building Secure KYC Systems in Flutter — Lessons from Production Banking Apps

**What is KYC and Why Does it Matter?
**KYC is a mandatory regulatory process that financial institutions use to verify the identity of their customers. In digital banking, this means:

Verifying government-issued identity documents (NIC, Passport)
Biometric face matching
Liveness detection to prevent spoofing
Real-time database checks against government records

A poorly implemented KYC system can lead to regulatory fines, fraud, and data breaches. Getting it right is critical.
The Core Architecture
After building KYC systems for multiple banking apps, I settled on a Clean Architecture approach that separates concerns clearly:
Presentation Layer → Bloc/Riverpod
Domain Layer → Use Cases + Entities
Data Layer → Repositories + Remote/Local Sources
This separation means:

KYC business logic stays independent of UI
Easy to swap SDKs without touching business logic
Testable in isolation — critical for regulated environments

Key Component 1 — NIC Scanning

abstract class NicScanRepository {
Future> scanNic();
}

class NicScanRepositoryImpl implements NicScanRepository {
final NicScanDataSource dataSource;

NicScanRepositoryImpl({required this.dataSource});

@override
Future> scanNic() async {
try {
final result = await dataSource.initiateScan();
return Right(result.toDomain());
} on ScanException catch (e) {
return Left(ScanFailure(message: e.message));
}
}
}

Key Component 2 — Biometric Authentication with IdWise SDK
class BiometricKycDataSource {
Future initiateKyc({
required String journeyDefinitionId,
required String referenceNumber,
}) {
final completer = Completer();

IdWise.initialize(
  clientKey: AppConfig.idWiseClientKey,
  environment: IdWiseEnvironment.production,
);

IdWise.startJourney(
  journeyDefinitionId: journeyDefinitionId,
  referenceNumber: referenceNumber,
  locale: 'en',
  delegate: _KycDelegate(completer),
);

return completer.future;
Enter fullscreen mode Exit fullscreen mode

}
}

class _KycDelegate implements IdWiseSDKDelegate {
final Completer completer;
_KycDelegate(this.completer);

@override
void onJourneyCompleted(String journeyId, bool isCompleted) {
completer.complete(KycResult(
journeyId: journeyId,
status: isCompleted ? KycStatus.completed : KycStatus.failed,
));
}

@override
void onJourneyResumed(String journeyId) {}

@override
void onError(IdWiseError error) {
completer.completeError(KycException(message: error.message));
}
}

Key Component 3 — State Management for KYC Flow
enum KycStep { idle, scanning, biometric, verifying, completed, failed }

@riverpod
class KycNotifier extends _$KycNotifier {
@override
KycState build() => const KycState(step: KycStep.idle);

Future startNicScan() async {
state = state.copyWith(step: KycStep.scanning);

final result = await ref.read(nicScanRepositoryProvider).scanNic();

result.fold(
  (failure) => state = state.copyWith(
    step: KycStep.failed,
    errorMessage: failure.message,
  ),
  (nicData) => state = state.copyWith(
    step: KycStep.biometric,
    nicData: nicData,
  ),
);
Enter fullscreen mode Exit fullscreen mode

}

Future startBiometric() async {
state = state.copyWith(step: KycStep.verifying);

final result = await ref.read(biometricKycRepositoryProvider)
    .initiateKyc(referenceNumber: state.nicData!.nicNumber);

result.fold(
  (failure) => state = state.copyWith(
    step: KycStep.failed,
    errorMessage: failure.message,
  ),
  (kycResult) => state = state.copyWith(
    step: KycStep.completed,
    kycResult: kycResult,
  ),
);
Enter fullscreen mode Exit fullscreen mode

}
}

Lessons Learned
After building KYC for multiple production banking apps, here is what I wish I had known earlier:

Abstract your SDK dependencies early — KYC SDK providers change, merge, and update APIs frequently
Test on real low-end devices — camera performance varies dramatically across the Android ecosystem
Handle network failures gracefully — KYC mid-flow network drops are common; build resumable journeys
Log anonymously — never log NIC numbers or biometric data, even in development
Design for accessibility — banking apps serve all demographics; KYC flows must work for elderly users too

Conclusion
Building secure KYC systems in Flutter for production banking applications requires a combination of clean architecture, robust state management, and security-first thinking. The patterns described here have been battle-tested across multiple live digital banking applications serving real users in regulated financial environments.
If you are building a fintech app and have questions about KYC implementation, feel free to connect — I am always happy to discuss secure mobile architecture.

Top comments (0)