ZepIris splits face authentication into a CRUD API and a four-model inference service
No OTPs. No registers. No buddy punching. Just a selfie. ZepIris is Zepto's purpose-built face authentication platform - engineered for scale, tuned for budget phones.
At a glance
- What is it?
- ZepIris is Zepto's open-sourced face verification stack: a FastAPI service for enrolment, search and deletion, a second service holding four PyTorch models in memory for embeddings, NSFW, spoof and blur checks, and Milvus, Etcd and MinIO underneath. The interesting part is the split, and the licensing note on the default model is the part to read twice.
- Who is it for?
- ZepIris suits a team that already runs containers and wants attendance or attendance-adjacent verification with liveness and image-quality gates rather than an OTP flow. It does not suit a pilot: docker-compose starts five containers, the three quality model weights are not in the repository, and InsightFace's buffalo_l weights are licensed for non-commercial research only, so check which model you are shipping before you pick one.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 135 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two FastAPI services over Milvus, Etcd and MinIO
The design decision is the split. ZepIris is two independent FastAPI services talking over HTTP, not one application with a module boundary.
The main API listens on port 8000 and owns everything user-facing:
POST /v1/faces/search
POST /v1/faces/insert
POST /v1/faces/upsert
DELETE /v1/faces/delete
GET /v1/faces/get/{face_id}
GET /healthz
GET /readyzIt handles image uploads to MinIO, coordinates with the ML service for quality checks and embedding extraction, and indexes the resulting vectors in Milvus with per-tenant isolated namespaces. Every response carries a requestId for traceability. Its dependencies are FastAPI, Uvicorn and Pydantic on the application side, with pymilvus, minio and httpx underneath.
The ML inference service on port 8001 exists so that GPU-shaped work can scale on its own without taking the CRUD API with it. The stated reason for the HTTP boundary is independent scaling, and the stated benefit is model reuse across requests instead of loading weights per call.
Four PyTorch models stay resident in one process
The ML service holds four models in memory for the life of the process. The face embedding model turns a 640 by 640 input into a 512-dimensional vector, and it is chosen at start-up: AuraFace-v1 or InsightFace's buffalo_l, selected by the ML_SERVICE_FACE_MODEL flag in .env. The other three are fixed architectures: MobileNetV2 as a two-class NSFW classifier, MobileNetV3-Large for spoof and liveness detection, and ResNet18 for blur and image-quality assessment.
Endpoints exist for individual inferences or for a combined assessment, and the combined path runs the three quality checks concurrently through a ThreadPoolExecutor. That matters at a queue, since a request that needs NSFW, spoof and blur answers waits for the slowest one otherwise.
The embedding choice propagates all the way into the vector store, because everything downstream assumes 512 dimensions. .env.example sets ZEPIRIS_MILVUS_EMBEDDING_DIM=512 and a search threshold of 0.5, so switching models means checking the index you already built.
Nothing here is trained by the project. These are off-the-shelf architectures with thresholds you tune.
The default model matters because buffalo_l is research-licensed
.env.example sets ML_SERVICE_FACE_MODEL=auraface, and the comment next to it carries the warning. Setting it to buffalo_l switches to InsightFace's model, and the note states that those weights are licensed for non-commercial research purposes only, with commercial use requiring a separate licence.
So the default is the safer of the two, and the alternative that sounds more standard is the one with a restriction attached. Anyone deploying this for attendance or onboarding has to settle that licence question for whichever model they ship, and the README leaves it open.
The repository's own licensing is murkier still. The metadata records no recognised licence identifier while a LICENSE file exists at the root, alongside THIRD-PARTY-NOTICES.md, SECURITY.md and a CODE_OF_CONDUCT.md. The tree also carries models/, so the weights directory is part of the layout even though the quality models are not filled in by default.
Attribution here is Zepto's own: the four listed authors and two maintainers all use zepto.com addresses, and the project page is a Zepto engineering blog post rather than a standalone site.
The three quality models are not in the box
This is the gap between a one-command demo and a working deployment. The environment example configures NSFW, spoof and blur each with ML_SERVICE_*_MODEL_SOURCE set to local, an empty HF_REPO_ID, an HF_MODEL_FILE name, a local path under /app/models, and a threshold of 0.5.
With the repository id left blank and the source set to local, nothing is fetched for you. You have to place nsfw_model.pth, spoof_model.pth and blur_model.pth at those paths yourself, or point the variables at somewhere they will be fetched from. Nothing in the README says where to obtain compatible weights for those three classifiers, and nothing checks at start-up that the files are the right ones.
The thresholds are the tuning surface afterwards: blur sensitivity, the spoof decision and the NSFW confidence all sit at 0.5 out of the box, which is a starting point rather than a validated setting for your cameras and lighting.
Everything else is a genuine consequence of the low-light, budget-phone target, so poor images are meant to be caught by these gates rather than by the embedding.
MinIO ships with minioadmin credentials and a latest tag
docker-compose.yml sets an explicit project name so image tags become zepiris-api and zepiris-ml-inference rather than inheriting the directory you cloned into. It then starts MinIO, Etcd, Milvus, the API and the ML service.
The pinned dependencies are Milvus at v2.6.12 and Etcd at v3.5.5. MinIO is minio/minio:latest, which is the one moving part in a file that otherwise pins versions, and it comes with MINIO_ROOT_USER and MINIO_ROOT_PASSWORD both set to minioadmin in the compose file and the same values repeated in .env.example as ZEPIRIS_MINIO_ACCESS_KEY and ZEPIRIS_MINIO_SECRET_KEY. Those are the documented MinIO defaults, which is fine on a laptop and not fine anywhere else.
Host ports are shifted rather than mirrored: MinIO's API is published as 9002 to container 9000, the console on 9001, Milvus's gRPC API on 19530 and 9091 alongside it. So the .env on your laptop points at localhost:9002 while the compose network talks to minio:9000, which is a detail that breaks in the first hour for almost everyone.
Etcd is configured with revision-based auto compaction at 1000 revisions and a snapshot count of 50000, sensible defaults for Milvus metadata rather than for Etcd itself.
The API image installs main only, and copies a .env into the layer
The API Dockerfile is python:3.12.3-slim. It installs curl plus the OpenCV headless runtime libraries, copies pyproject.toml, poetry.lock, poetry.toml, README.md and .env into the image, installs Poetry 2.1.3, runs poetry install --only main --no-root, installs the project with pip, then uninstalls Poetry.
--only main is the point. The ml extra carries torch, torchvision, insightface, onnx and onnxruntime, and none of it lands in this image. That is what keeps the CRUD service light, and it is why the second Dockerfile exists: ml_inference.Dockerfile builds the service that holds the models.
Two details deserve a look before you build. The COPY line includes .env, so the build expects an environment file to exist in the build context even though .dockerignore is present; check what that file excludes before you build on a machine holding real credentials. And the Poetry source block pins torch and torchvision to the pytorch-cpu wheel index with explicit priority, which matches ML_SERVICE_ML_DEVICE defaulting to cpu.
Runtime hygiene is otherwise decent: a non-root appuser at uid 1000, PYTHONDONTWRITEBYTECODE and PYTHONUNBUFFERED set, and a healthcheck curling /healthz every thirty seconds.
One compose command, five containers, ten gigabytes
The prerequisites are stated plainly: Python 3.10 to 3.14, tested across that range, Poetry 2.x, and Docker with Docker Compose v1.29 or newer, with Compose recommended for the all-in-one setup. The resource floor is 4GB of RAM for Milvus and 10GB of free disk.
Check the toolchain before anything else:
python3 --version
poetry --versionThen the whole stack:
git clone <repository-url>
cd zepiris
docker-compose up -d
curl http://localhost:8000/healthz
docker-compose downInteractive documentation is served at /docs, generated from the OpenAPI spec, and the README notes that URL paths are versioned separately from the package: /v1/... on the wire while the Python package and the OpenAPI info.version follow semver, currently 1.0.0. Two console scripts are installed as entry points, zepiris-api and zepiris-ml-inference-api.
The version split is a small trap for anyone scripting against it: the API is versioned in two places that are documented together but not derived from each other.
Against calling InsightFace directly
The obvious alternative is to skip all of this. insightface is already a dependency here, and a hundred lines calling it, storing vectors in Postgres pgvector or a plain NumPy array, and gating on your own quality heuristics would be less infrastructure for a single-tenant internal tool.
What you get by staying with ZepIris is the part that is genuinely hard to rebuild: multi-tenant enrolment and search with isolated namespaces, four models resident instead of reloaded, a combined quality gate that runs three checks in parallel, delete and upsert on face records, image archival in S3-compatible storage, and a requestId threaded through every response. That is a service, not a snippet.
What it costs is five containers, three model files you have to source yourself, and a licence question on the embedding weights. For a company that runs attendance across stores and wants liveness rather than an OTP, that trade comes out ahead. For a team that needs one match on one camera, it is a lot of Milvus.
Either way, the enrolment data is biometric. ZepIris documents deletion and nothing about retention.
Editorial conclusion
ZepIris suits a team that already runs containers and wants attendance or attendance-adjacent verification with liveness and image-quality gates rather than an OTP flow. It does not suit a pilot: docker-compose starts five containers, the three quality model weights are not in the repository, and InsightFace's buffalo_l weights are licensed for non-commercial research only, so check which model you are shipping before you pick one. Two things to verify before any real enrolment: the licence on the weights you choose, since the repository itself reports no recognised licence identifier, and what your retention and deletion story is, because delete on a face record is irreversible and the collection is biometric data. Released as v1.0.0 on 2026-05-25, last pushed on 2026-05-29.
Frequently asked questions
How do I run ZepIris locally?
Clone the repository, run docker-compose up -d, and verify with curl http://localhost:8000/healthz. The compose stack starts MinIO, Etcd, Milvus, the API on port 8000 and the ML inference service on port 8001. It needs 4GB of RAM and 10GB of free disk.
Which face embedding models does ZepIris support?
AuraFace-v1 or InsightFace's buffalo_l, both producing 512-dimensional vectors from a 640 by 640 input, chosen with ML_SERVICE_FACE_MODEL. The .env comment notes that the buffalo_l weights are licensed for non-commercial research purposes only and that commercial use needs a separate licence.
What models does the ZepIris ML inference service hold?
Four PyTorch models in memory: the face embedding model, MobileNetV2 for NSFW classification, MobileNetV3-Large for spoof and liveness detection, and ResNet18 for blur detection. The combined image-quality path runs the three quality checks in parallel through a ThreadPoolExecutor.
Why does ZepIris need a second service on port 8001?
So the ML workloads scale independently of the CRUD API. The main service on port 8000 handles the /v1/faces endpoints, image storage in MinIO and indexing in Milvus, and calls the inference service over HTTP for embeddings and quality checks.
Where does ZepIris keep its face images and vectors?
Images go to MinIO as S3-compatible object storage, and embeddings are indexed in Milvus with COSINE similarity and per-tenant isolated namespaces. The example environment sets a 512 embedding dimension, a collection named zepiris_faces and a search threshold of 0.5.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/zepto-labs-zepiris)