KYC Service — Integrations¶
Inbound: Auth Center (Serverpod)¶
Endpoint: POST /api/v1/process
Authentication: X-Service-Token: {KYC_SERVICE_SECRET} header — must match the KYC_SERVICE_SECRET env var exactly. Returns 401 if missing or wrong.
Request body:
{
"userId": 123,
"kycId": 456,
"passportKey": "kyc/456/passport.jpg",
"selfieKey": "kyc/456/selfie.jpg",
"stampKey": "kyc/456/stamp.jpg"
}
stampKey is optional. userId, kycId, passportKey, selfieKey are all required.
Response: 202 Accepted
If the job kyc-{kycId} already exists in the queue (duplicate submission), returns 202 with the same jobId without re-enqueuing.
Health check: GET /health → 200 { "status": "ok" }
Garage / S3 (Image Storage)¶
Uses @aws-sdk/client-s3 + @aws-sdk/s3-request-presigner to generate presigned GET URLs that expire in 5 minutes. These URLs are passed directly to the AI providers.
| Env var | Purpose |
|---|---|
S3_ENDPOINT |
Garage endpoint (e.g., https://s3.1-wallet.app) |
S3_REGION |
Region identifier (e.g., 1-wallet) |
S3_ACCESS_KEY |
Access key ID |
S3_SECRET_KEY |
Secret access key |
S3_BUCKET |
Bucket name (default: kyc-data) |
forcePathStyle: true is required for Garage compatibility.
Gemini AI (OCR)¶
- SDK:
@google/generative-ai - Model:
gemini-3-flash-preview(for both OCR and face match fallback) - Auth:
GEMINI_API_KEYenv var - The model receives a presigned URL image as an inline data part (base64 encoded in the request)
- OCR prompt (
OCR_PROMPTinsrc/services/prompts.js): instructs the model to extract passport fields and return structured JSON - Response is parsed as JSON after stripping any markdown code fences
- 429 rate limit errors are caught and reraised with retry delay embedded in the error message
CompreFace (Face Matching)¶
CompreFace is a self-hosted face recognition server using ArcFace embeddings.
- Endpoint:
POST {COMPREFACE_URL}/api/v1/verification/verify - Auth:
x-api-key: {COMPREFACE_API_KEY}header - Request: multipart/form-data with
source_image(passport photo) andtarget_image(selfie) - Response: JSON with
result[0].face_matches[0].similarity— cosine similarity score 0.0–1.0 - Health check at startup:
GET {COMPREFACE_URL}/healthcheck(warning only if unreachable — does not block startup)
Error mapping:
- No face in passport photo → INVALID_PHOTO:passport:... (UnrecoverableError — no retry)
- No face in selfie → INVALID_PHOTO:selfie:... (UnrecoverableError — no retry)
- Score below threshold → FACE_MISMATCH:{score}:... (UnrecoverableError — no retry)
- Network error / timeout (15s) → retryable Error
CompreFace requires a named Docker volume (compreface_data) to persist face data across restarts.
| Env var | Purpose |
|---|---|
COMPREFACE_URL |
CompreFace base URL (default: http://localhost:8000) |
COMPREFACE_API_KEY |
API key for the verification service |
FACE_MATCH_THRESHOLD |
Minimum similarity to accept (default: 0.75) |
PostgreSQL (Results Storage)¶
Connection via POSTGRES_URL env var (single connection string).
Table written: kyc_verification (in the public schema, owned by Serverpod)
| Operation | SQL | When |
|---|---|---|
| Success | UPDATE kyc_verification SET status='ocr_complete', "ocrResult"=$1, "reviewedAt"=NOW() WHERE id=$2 |
After successful OCR + face match |
| Failure | UPDATE kyc_verification SET status='ocr_failed', "ocrResult"=$1, "reviewedAt"=NOW() WHERE id=$2 |
After all retries exhausted or unrecoverable error |
| Watchdog | Same as failure with failureCode='processing_timeout' |
Records stuck in in_review for > KYC_STUCK_TIMEOUT_MIN minutes |
ocrResult is a bytea column containing the AES-encrypted JSON result blob.
Column names follow Serverpod camelCase convention and must be double-quoted in raw SQL.
Redis (BullMQ Queue)¶
- Connection:
REDIS_URLenv var - Queue name:
kyc-jobs maxmemory-policyis set tonoevictionat startup to prevent BullMQ job lossmaxRetriesPerRequest: nullis required by BullMQ on the ioredis connection