Skip to content

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}` };
}

One chat. Everything done.