A production-ready face recognition and vector similarity search engine. Built with Node.js/TypeScript, this API provides a two-step workflow for face detection, customer enrollment, and recognition using InsightFace Buffalo-L embeddings, RetinaFace detection, and PostgreSQL pgvector for efficient similarity search.
📚 For detailed technical documentation, see TECHNICAL_DETAILS.md - comprehensive guide covering image processing pipelines, coordinate transformations, model inference, and performance optimizations.
- ✅ Face Detection - Detect faces with 5 facial landmarks using RetinaFace ResNet50
- ✅ Face Enrollment - Enroll customers with face embeddings for recognition
- ✅ Face Recognition - Identify customers by searching similar face embeddings
- ✅ Multipart Upload - Upload images as files (PNG, JPG, WEBP, AVIF) with automatic normalization and scaling
- ✅ Two-Step Workflow - Detect faces first, then enroll or recognize them
- ✅ Vector Similarity Search - PostgreSQL pgvector with optimized ivfflat indexing
- ✅ 512-dim Embeddings - InsightFace Buffalo-L embeddings for high-accuracy face recognition
- ✅ Docker Support - Easy deployment with Docker Compose
- ✅ TypeScript - Fully typed codebase
flowchart TB
subgraph Client["Client Layer"]
USER[User/Application]
end
subgraph API["API Server (Express + TypeScript)"]
DETECT["`POST /faces/detect
Detect & Store Faces`"]
ENROLL["`POST /faces/enroll
Enroll Customer`"]
RECOGNIZE["`POST /faces/recognize
Recognize Customer`"]
GET["`GET /faces/:face_id
Get Face Image`"]
end
subgraph Models["AI Models (ONNX Runtime)"]
RETINAFACE["`RetinaFace ResNet50
Face Detection`"]
RECOGNITION["`InsightFace Buffalo-L
512-dim Embeddings`"]
end
subgraph Storage["Storage Layer"]
DETECTED[("`detected_faces table
Face metadata`")]
ENROLLED[("`enrolled_customers table
Vector embeddings`")]
RUSTFS["`RustFS S3
Object Storage`"]
end
USER -->|Upload Image| DETECT
DETECT -->|Detect Faces| RETINAFACE
RETINAFACE -->|Bounding Boxes| DETECT
DETECT -->|Store Metadata| DETECTED
DETECT -->|Store Images| RUSTFS
DETECT -->|face_id| USER
USER -->|face_id + customer_info| ENROLL
ENROLL -->|Read Face| RUSTFS
ENROLL -->|Generate Embedding| RECOGNITION
RECOGNITION -->|512-dim Vector| ENROLL
ENROLL -->|Store| ENROLLED
USER -->|face_id| RECOGNIZE
RECOGNIZE -->|Read Face| RUSTFS
RECOGNIZE -->|Generate Embedding| RECOGNITION
RECOGNIZE -->|Vector Search| ENROLLED
ENROLLED -->|Matched Customers| RECOGNIZE
RECOGNIZE -->|Results| USER
USER -->|face_id| GET
GET -->|Read| RUSTFS
RUSTFS -->|Image| USER
style RETINAFACE fill:#e1f5ff
style RECOGNITION fill:#e1f5ff
style DETECTED fill:#fff4e1
style ENROLLED fill:#fff4e1
style RUSTFS fill:#ffe1f5
- Node.js 24+
- Docker + Docker Compose
- PostgreSQL with pgvector extension
# Install dependencies
make install
# Download ONNX models
make models
# Make scripts executable
make chmod-scripts
# Start PostgreSQL database
make db
# Run the API locally
make run# Build and start everything
make up
# Or with Docker Compose directly
docker compose up --build- API runs on
http://localhost:3000/api - Postgres runs on
localhost:5432(DB: face_db)
Detect all faces in an uploaded image, store face crops and metadata, and return face IDs.
POST /api/faces/detect
Request: Multipart form-data
file(required): Image file (PNG, JPG, WEBP, AVIF, max 10MB)identifier(optional): Client-provided identifier for tracking
# Using curl
curl -X POST http://localhost:3000/api/faces/detect \
-F "file=@examples/elon_musk_enroll.jpg" \
-F "identifier=customer_123"
# Using script
./scripts/faces-detect.sh examples/elon_musk_enroll.jpg customer_123Response:
[
{
"face_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"position": {
"x": 450,
"y": 300,
"width": 600,
"height": 850
},
"confidence": 0.998,
"file_name": "a1b2c3d4-e5f6-7890-abcd-ef1234567890.jpg"
}
]Retrieve a stored face image by its face_id.
GET /api/faces/:face_id
# Using curl
curl -X GET http://localhost:3000/api/faces/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
-o face.jpg
# Using script (saves to output/)
./scripts/faces-get-image.sh a1b2c3d4-e5f6-7890-abcd-ef1234567890Response: Binary JPEG image
Enroll a customer by associating a detected face with customer information and generating an embedding.
POST /api/faces/enroll
Request: JSON
face_id(required): UUID from/faces/detectcustomer_identifier(required): Unique customer identifiercustomer_name(optional): Customer's namecustomer_metadata(optional): Additional metadata as JSON
# Using curl
curl -X POST http://localhost:3000/api/faces/enroll \
-H "Content-Type: application/json" \
-d '{
"face_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"customer_identifier": "CUST001",
"customer_name": "John Doe",
"customer_metadata": {"age": 30, "membership": "gold"}
}'
# Using script
./scripts/faces-enroll.sh a1b2c3d4-e5f6-7890-abcd-ef1234567890 CUST001 "John Doe"Response:
{
"customer_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"customer_identifier": "CUST001",
"customer_name": "John Doe",
"created_at": "2024-12-02T10:30:00.000Z"
}Recognize a customer by searching for similar enrolled face embeddings.
POST /api/faces/recognize
Request: JSON
face_id(required): UUID from/faces/detectmin_confidence(optional): Minimum similarity required to return a match. Defaults toFACE_RECOGNITION_CONFIDENCE_THRESHOLD.
confidence_score is cosine similarity, not a calibrated probability. The default threshold of 0.5 is intended to avoid false positives on normal probes. If a real same-person image falls below this value, prefer adding stronger or multiple enrollment images for that person before lowering the global threshold.
# Using curl
curl -X POST http://localhost:3000/api/faces/recognize \
-H "Content-Type: application/json" \
-d '{"face_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "min_confidence": 0.5}'
# Using script
./scripts/faces-recognize.sh a1b2c3d4-e5f6-7890-abcd-ef1234567890 http://localhost:3000 0.5Response:
[
{
"customer_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"customer_identifier": "CUST001",
"customer_name": "John Doe",
"confidence_score": 0.6708
},
{
"customer_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"customer_identifier": "CUST002",
"customer_name": "Jane Smith",
"confidence_score": 0.4123
}
]Results are sorted by confidence score (descending), returns up to 10 matches that pass the configured confidence threshold. An empty array ([]) means the nearest enrolled face did not pass the threshold and should be treated as "no match".
Administrative endpoints for listing and managing detected faces and enrolled customers.
GET /api/management/faces?limit=50&offset=0
Returns paginated list of all detected faces with metadata.
./scripts/management-list-faces.shGET /api/management/customers?limit=50&offset=0
Returns paginated list of all enrolled customers.
./scripts/management-list-customers.shGET /api/management/customers/:customer_id
Returns full customer details including face metadata.
./scripts/management-get-customer.sh <customer_id>GET /api/management/stats
Returns database statistics including orphaned faces count.
./scripts/management-stats.shResponse:
{
"detected_faces": 10,
"enrolled_customers": 5,
"orphaned_faces": 5
}DELETE /api/management/faces/orphaned
Deletes all orphaned faces (faces not enrolled with any customer) to free up storage space. This includes removing both database records and S3 images.
./scripts/management-delete-orphaned.shResponse:
{
"message": "Successfully deleted 5 orphaned face(s)",
"deleted_count": 5,
"failed_count": 0,
"deleted_faces": [
{
"face_id": "...",
"identifier": "...",
"s3_keys": {
"original": "originals/...",
"face": "faces/..."
}
}
]
}DELETE /api/management/faces/:face_id
Deletes a specific face and its S3 images (only if not enrolled with any customer).
./scripts/management-delete-face.sh <face_id>DELETE /api/management/customers/:customer_id
Deletes an enrolled customer record.
./scripts/management-delete-customer.sh <customer_id># 1. Detect faces in an image
./scripts/faces-detect.sh examples/elon_musk_enroll.jpg CUSTOMER_001
# Response: [{"face_id": "abc-123...", ...}]
# 2. Enroll the customer
./scripts/faces-enroll.sh abc-123... CUSTOMER_001 "Elon Musk"
# Response: {"customer_id": "xyz-789...", ...}
# 3. Later, detect face in another image of the same person
./scripts/faces-detect.sh examples/elon_musk_positive.jpg
# Response: [{"face_id": "def-456...", ...}]
# 4. Recognize the customer
./scripts/faces-recognize.sh def-456...
# Response: [{"customer_identifier": "CUSTOMER_001", "confidence_score": 0.6708, ...}]
# 5. Retrieve the original face image
./scripts/faces-get-image.sh abc-123...
# Saves image to: output/retrieved_face.jpgThe examples/ folder includes curated recognition samples:
| Purpose | Image |
|---|---|
| Enroll Elon | examples/elon_musk_enroll.jpg |
| Positive Elon probe | examples/elon_musk_positive.jpg |
| Enroll Jensen | examples/jensen_huang_enroll.jpg |
| Positive Jensen probe | examples/jensen_huang_positive.jpg |
| Negative probe, should not match enrolled examples | examples/xi_jinping_solo.png |
| Multi-format mixed scene | examples/elon_musk_jensen_huang_mixed_large.webp |
| AVIF mixed scene | examples/elon_musk_jensen_huang_mixed_large.avif |
| Hard profile probe | examples/elon_musk_profile.jpg |
Run the smoke test against a running API:
./scripts/verify-recognition-examples.sh
# Optional: custom API URL and threshold
./scripts/verify-recognition-examples.sh http://localhost:3000 0.5Expected result:
- The positive images return the enrolled Elon and Jensen customers.
- The negative image returns
[]. - If the negative image returns a customer, raise
FACE_RECOGNITION_CONFIDENCE_THRESHOLDand inspect the aligned face image. - Profile and small mixed-scene examples are intentionally hard cases. Tests use them to make sure weak crops do not become false positives at the default threshold.
src/__tests__/examples.test.tsruns detection against every image file inexamples/and fails if a new image is added without updating the fixture matrix.
Run the benchmark against a running API:
# Terminal 1
make run
# Terminal 2
make benchmark-performanceThe benchmark measures real API calls for single-face JPEG detection, multi-face JPEG detection, WEBP/AVIF detection, recognition-only lookup, positive detect+recognize flow, and negative detect+recognize flow. It writes JSON and Markdown reports to benchmarks/results/, which is ignored by git.
Latest local benchmark sample:
| Field | Value |
|---|---|
| Platform | Linux x64 |
| Node.js | v24.16.0 |
| Iterations | 3 |
| ONNX execution provider | CPU |
| GPU acceleration measured | - |
| Database | Local Docker PostgreSQL + pgvector |
| Object storage | Local Docker RustFS |
| Operation | Count | P50 ms | P95 ms | Avg ms |
|---|---|---|---|---|
detect_single_face_jpeg |
3 | 2919.14 | 3297.33 | 2622.87 |
detect_multi_face_jpeg |
3 | 2451.76 | 2817.55 | 2515.09 |
detect_webp |
3 | 8725.34 | 10114.76 | 9164.42 |
detect_avif |
3 | 1450.02 | 1491.38 | 1421.55 |
recognize |
3 | 269.03 | 276.40 | 243.17 |
full_positive_flow |
3 | 2722.87 | 2920.38 | 2720.47 |
full_negative_flow |
3 | 2871.55 | 3492.04 | 3068.68 |
Optional arguments:
npm run benchmark -- http://localhost:3000 5 benchmarks/resultsArguments are api_url, iterations, and output_dir.
erDiagram
detected_faces ||--o| enrolled_customers : enrolls
detected_faces {
uuid id PK
text original_image_path
text face_image_path
text identifier
jsonb bounding_box
float confidence
timestamptz created_at
}
enrolled_customers {
uuid id PK
uuid face_id FK
text customer_identifier
text customer_name
jsonb customer_metadata
vector embedding
timestamptz created_at
}
Stores detected face metadata from the detect endpoint.
| Column | Type | Description |
|---|---|---|
| id | uuid | Primary key (face_id) |
| original_image_path | text | Path to original uploaded image |
| face_image_path | text | Path to cropped face image |
| identifier | text | Optional client identifier |
| bounding_box | jsonb | Face position {x, y, width, height} |
| confidence | float | Detection confidence (0-1) |
| created_at | timestamptz | Timestamp |
Stores enrolled customer information with face embeddings.
| Column | Type | Description |
|---|---|---|
| id | uuid | Primary key (customer_id) |
| face_id | uuid | Foreign key to detected_faces |
| customer_identifier | text | Unique customer identifier |
| customer_name | text | Customer name (optional) |
| customer_metadata | jsonb | Additional metadata (optional) |
| embedding | vector(512) | InsightFace face embedding |
| created_at | timestamptz | Timestamp |
Vector search is optimized with ivfflat index on the embedding column.
All uploaded images are normalized through Sharp into auto-rotated JPEG buffers, then scaled down (max 1920px on longest side) to improve processing performance while maintaining aspect ratio. Supports PNG, JPG, WEBP, and AVIF formats up to 10MB.
- Face Detection: RetinaFace ResNet50 with 5 facial landmarks
- Face Recognition: InsightFace Buffalo-L (512-dimensional embeddings)
- Vector Search: PostgreSQL pgvector with ivfflat indexing
- Object Storage: RustFS S3-compatible storage for images
- Image Processing: Jimp for manipulation, multer for uploads
- API Framework: Express.js with TypeScript
- ML Runtime: ONNX Runtime Node
Environment variables in .env:
# Database
DATABASE_URL=postgres://postgres:postgres@localhost:5432/face_db
# API
PORT=3000
# Face Detection (0.0 - 1.0, default: 0.8)
FACE_DETECTION_CONFIDENCE_THRESHOLD=0.8
# Face Recognition (0.0 - 1.0, default: 0.5)
FACE_RECOGNITION_CONFIDENCE_THRESHOLD=0.5
# RustFS S3-Compatible Storage
S3_ENDPOINT=http://localhost:9000
S3_BUCKET=facevector-engine
S3_ACCESS_KEY=rustfsadmin
S3_SECRET_KEY=rustfsadmin
S3_REGION=us-east-1
S3_FORCE_PATH_STYLE=truefacevector-engine/
├── originals/ # Normalized uploaded images
│ └── {uuid}.jpg
└── faces/ # Cropped face images
└── {face_id}.jpg
/tmp/facevector/
└── cropped_faces/ # Temporary cropped faces during processing
└── face_*.jpg # Auto-cleaned on container restart
RustFS Console: Access at http://localhost:9001 with credentials from .env
make install # Install dependencies (production only)
make install-dev # Install all dependencies including dev
make models # Download ONNX models
make chmod-scripts # Make scripts executable
make run # Run API locally (starts db + RustFS)
make lint # Check code style
make lint-fix # Auto-fix code style
npm run build # Compile TypeScriptmake test # Run full test suite (starts db + RustFS)
make test-integration # Run integration tests (full workflow)
make test-management # Run management API tests
make test-examples # Run every examples/ image through detection
make test-storage # Verify RustFS bucket/object operations
make benchmark-performance # Benchmark a running API and write reportsmake up # Build and start full stack (API + db + RustFS)
make down # Stop containers
make logs # Tail container logs
make clean # Remove containers and volumes
make reset # Clean and rebuild from scratch
make up-db # Start PostgreSQL only
make up-rustfs # Start RustFS onlymake clean && make up- Lower
FACE_DETECTION_CONFIDENCE_THRESHOLDin.env(e.g., 0.6) - Ensure clear, front-facing faces in images
- Check image format (PNG, JPG, WEBP, AVIF only)
make models # Downloads to models/ directoryMIT License - free to use for any purpose.