YeboVerify Services Deep Dive
VerificationService
Location: yeboverify-api/src/services/verification.service.ts
Core verification orchestration.
createVerification
Initiates a new verification request.
typescript
interface VerificationInput {
businessId: string;
idFrontBuffer: Buffer;
idFrontMimeType: string;
idBackBuffer?: Buffer;
idBackMimeType?: string;
selfieBuffer: Buffer;
selfieMimeType: string;
externalRef?: string;
documentType?: string;
}
async createVerification(input: VerificationInput): Promise<{
verificationId: string;
status: VerificationStatus;
}> {
// 1. Generate unique ID
const verificationId = `vrf_${nanoid()}`;
// 2. Upload images to R2 (private storage)
const idFrontKey = `verifications/${verificationId}/id-front-${Date.now()}`;
const selfieKey = `verifications/${verificationId}/selfie-${Date.now()}`;
await storageService.uploadFile(input.idFrontBuffer, idFrontKey, input.idFrontMimeType);
await storageService.uploadFile(input.selfieBuffer, selfieKey, input.selfieMimeType);
if (input.idBackBuffer) {
const idBackKey = `verifications/${verificationId}/id-back-${Date.now()}`;
await storageService.uploadFile(input.idBackBuffer, idBackKey, input.idBackMimeType);
}
// 3. Create database record
const verification = await prisma.verification.create({
data: {
verificationId,
businessId: input.businessId,
externalRef: input.externalRef,
status: 'PENDING',
idFrontImage: idFrontKey,
idBackImage: idBackKey,
selfieImage: selfieKey,
},
});
// 4. Start async processing
setImmediate(() => {
this.processVerification(verification.id).catch(console.error);
});
return { verificationId, status: 'PENDING' };
}processVerification
Background processing pipeline.
typescript
private async processVerification(dbId: string): Promise<void> {
const startTime = Date.now();
// Update to PROCESSING
const verification = await prisma.verification.update({
where: { id: dbId },
data: { status: 'PROCESSING' },
include: { business: true },
});
// === STEP 1: Face Comparison ===
let faceScore = 0;
let faceDecision: 'approved' | 'rejected' | 'needs_review' = 'rejected';
if (rekognitionService.isEnabled()) {
const [idFrontBuffer, selfieBuffer] = await Promise.all([
storageService.downloadBuffer(verification.idFrontImage),
storageService.downloadBuffer(verification.selfieImage),
]);
const faceResult = await rekognitionService.compareFaces(idFrontBuffer, selfieBuffer);
faceScore = faceResult.similarity;
faceDecision = rekognitionService.getVerificationDecision(faceResult).decision;
}
// Early rejection if face score too low
if (faceScore < 60) {
await this.completeVerification(dbId, startTime, {
faceScore,
faceDecision: 'rejected',
decision: 'REJECTED',
decisionReason: 'Face similarity below threshold (60%)',
confidence: 'low',
});
return;
}
// === STEP 2: Document OCR ===
let ocrConfidence = 0;
let extractedData = {};
if (geminiOCRService.isEnabled()) {
const idFrontBuf = await storageService.downloadBuffer(verification.idFrontImage);
const ocrResult = await geminiOCRService.extractPersonalInfo(idFrontBuf);
if (ocrResult.success) {
ocrConfidence = ocrResult.confidence;
extractedData = {
surname: ocrResult.personalInfo.surname,
names: ocrResult.personalInfo.names,
dateOfBirth: ocrResult.personalInfo.dateOfBirth,
idNumber: ocrResult.personalInfo.idNumber,
documentType: ocrResult.personalInfo.documentType,
};
}
}
// === STEP 3: Decision ===
let finalDecision: Decision;
let confidence: 'high' | 'medium' | 'low';
let decisionReason: string;
if (faceScore >= 85 && ocrConfidence >= 70) {
finalDecision = 'APPROVED';
confidence = 'high';
decisionReason = 'High face match and OCR confidence';
} else if (faceScore >= 70 && ocrConfidence >= 50) {
if (faceScore >= 80 && ocrConfidence >= 60) {
finalDecision = 'APPROVED';
confidence = 'medium';
decisionReason = 'Acceptable face match and OCR confidence';
} else {
finalDecision = 'NEEDS_REVIEW';
confidence = 'medium';
decisionReason = 'Face or OCR confidence requires manual review';
}
} else {
finalDecision = 'REJECTED';
confidence = 'low';
decisionReason = 'Face or OCR confidence below threshold';
}
await this.completeVerification(dbId, startTime, {
faceScore,
faceDecision,
ocrConfidence,
extractedData,
decision: finalDecision,
decisionReason,
confidence,
});
}RekognitionService
Location: yeboverify-api/src/services/aws-rekognition.service.ts
AWS Rekognition for face comparison.
compareFaces
typescript
interface FaceComparisonResult {
similarity: number; // 0-100
confidence: number; // 0-100
matched: boolean;
faceDetails?: {
boundingBox: object;
quality: object;
};
}
async compareFaces(
sourceImage: Buffer, // ID document
targetImage: Buffer // Selfie
): Promise<FaceComparisonResult> {
const command = new CompareFacesCommand({
SourceImage: { Bytes: sourceImage },
TargetImage: { Bytes: targetImage },
SimilarityThreshold: 0, // Get all results
});
const response = await this.client.send(command);
if (response.FaceMatches && response.FaceMatches.length > 0) {
const match = response.FaceMatches[0];
return {
similarity: match.Similarity || 0,
confidence: match.Face?.Confidence || 0,
matched: (match.Similarity || 0) >= 80,
faceDetails: {
boundingBox: match.Face?.BoundingBox,
quality: match.Face?.Quality,
},
};
}
return { similarity: 0, confidence: 0, matched: false };
}getVerificationDecision
typescript
getVerificationDecision(result: FaceComparisonResult): {
decision: 'approved' | 'rejected' | 'needs_review';
reason: string;
} {
if (result.similarity >= 90) {
return { decision: 'approved', reason: 'High confidence match' };
} else if (result.similarity >= 80) {
return { decision: 'approved', reason: 'Good match' };
} else if (result.similarity >= 70) {
return { decision: 'needs_review', reason: 'Borderline match' };
} else {
return { decision: 'rejected', reason: 'Low similarity' };
}
}GeminiOCRService
Location: yeboverify-api/src/services/gemini-ocr.service.ts
Document text extraction using Gemini Vision.
extractPersonalInfo
typescript
interface OCRResult {
success: boolean;
confidence: number; // 0-100
personalInfo: {
surname?: string;
names?: string;
dateOfBirth?: string;
idNumber?: string;
documentType?: string;
nationality?: string;
expiryDate?: string;
};
error?: string;
}
async extractPersonalInfo(imageBuffer: Buffer): Promise<OCRResult> {
const base64Image = imageBuffer.toString('base64');
const prompt = `Analyze this ID document image and extract personal information.
Return a JSON object with:
- surname: Last name
- names: First and middle names
- dateOfBirth: In YYYY-MM-DD format
- idNumber: Document number
- documentType: Type of document (e.g., "National ID", "Passport")
- nationality: Country/nationality if visible
- expiryDate: Expiry date if visible
Also rate your confidence (0-100) in the extraction accuracy.
Return ONLY valid JSON, no markdown.`;
const result = await this.model.generateContent([
{ text: prompt },
{
inlineData: {
mimeType: 'image/jpeg',
data: base64Image,
},
},
]);
const response = result.response.text();
const parsed = JSON.parse(response);
return {
success: true,
confidence: parsed.confidence || 50,
personalInfo: parsed,
};
}StorageService
Location: yeboverify-api/src/services/storage.service.ts
Cloudflare R2 storage operations.
typescript
// Upload file
async uploadFile(buffer: Buffer, key: string, contentType: string): Promise<void> {
await this.s3.send(new PutObjectCommand({
Bucket: this.bucket,
Key: key,
Body: buffer,
ContentType: contentType,
}));
}
// Download file
async downloadBuffer(key: string): Promise<Buffer> {
const response = await this.s3.send(new GetObjectCommand({
Bucket: this.bucket,
Key: key,
}));
return Buffer.from(await response.Body!.transformToByteArray());
}
// Delete file
async deleteFile(key: string): Promise<void> {
await this.s3.send(new DeleteObjectCommand({
Bucket: this.bucket,
Key: key,
}));
}WebhookService
Location: yeboverify-api/src/services/webhook.service.ts
Webhook delivery with HMAC signing.
typescript
interface WebhookPayload {
event: 'verification.completed';
verificationId: string;
externalRef: string | null;
status: 'completed' | 'failed' | 'needs_review';
decision: 'approved' | 'rejected' | 'needs_review';
confidence: 'high' | 'medium' | 'low';
faceScore: number;
ocrConfidence: number;
extractedData: {
surname?: string;
names?: string;
dateOfBirth?: string;
idNumber?: string;
documentType?: string;
};
timestamp: string;
}
async sendWebhook(
url: string,
secret: string | null,
payload: WebhookPayload
): Promise<{ success: boolean; error?: string }> {
const body = JSON.stringify(payload);
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (secret) {
const signature = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
headers['X-YeboVerify-Signature'] = `sha256=${signature}`;
}
const response = await fetch(url, {
method: 'POST',
headers,
body,
});
if (response.ok) {
return { success: true };
}
return { success: false, error: `HTTP ${response.status}` };
}