A multi-tenant biometric authentication platform — face, voice, MRZ, and active liveness — packaged as an embeddable widget. Self-hosted, KVKK-compliant, WebAuthn-first.
Role
Lead engineer — full-stack, ML, and infrastructure · RollingCat Software
Date
Mar 2026 – Jun 2026
Stack
Java 21
Spring Boot 3
Python
FastAPI
PostgreSQL 16
pgvector
React 18
Kotlin Multiplatform
WebAuthn
OAuth 2.0
OIDC
Flyway
Docker
Traefik
Loki + Promtail + Grafana
The problem
Biometric authentication is hard to adopt because integrating it usually means handling raw face and voice data, defeating spoofing attacks, and meeting strict privacy law — work most teams cannot take on. The goal was to make biometric login as easy to drop into a site as reCAPTCHA, without the integrator ever touching the biometric data.
Constraints
▸
Biometric data must never sit unencrypted at rest, and embedding extraction must never be reachable from the public internet.
▸
KVKK (Turkish data-protection law) compliance is a hard requirement, not a later add-on.
▸
Each tenant organization must be isolated from every other tenant at the database level.
Approach
The platform splits into three runtime axes — a Spring Boot identity core as the source of truth, a private FastAPI ML sidecar that owns all biometric processing, and thin clients (React web, Kotlin Multiplatform mobile and desktop, and an embeddable widget). The widget exposes the whole challenge flow the way reCAPTCHA exposes a challenge.
Key decisions
Split the ML sidecar from the identity core as a separate deployable
The biometric processor talks to the API only over a private Docker network and is never exposed publicly. The ML stack can be upgraded without touching the API, and embedding extraction is structurally unreachable from the internet.
Make multi-tenancy a database concern, not an application filter
Each organization is schema-isolated, so there is no "WHERE tenant_id = ?" clause that a developer can forget to add. Tenant isolation cannot be bypassed by an application bug.
Fail fast when the embedding encryption key is missing
Embeddings are encrypted with Fernet at rest and decrypted only in-process at the moment cosine similarity runs. The app refuses to boot without the key, so it can never silently fall back to a default that would invalidate every stored embedding.
Architecture
Browsers and mobile clients reach the Spring Boot identity core through a Traefik reverse proxy. The identity core owns PostgreSQL 16 with pgvector and talks to the FastAPI biometric processor over a private Docker network that the public internet cannot reach. Loki, Promtail, and Grafana observe the whole stack.
flowchart TB
client["Clients<br/>React web · KMP mobile/desktop · embeddable widget"]
traefik["Traefik reverse proxy"]
api["Identity Core API<br/>Spring Boot 3 · Java 21"]
db[("PostgreSQL 16<br/>+ pgvector")]
ml["Biometric Processor<br/>FastAPI · Python"]
obs["Loki · Promtail · Grafana"]
client --> traefik --> api
api --> db
api -. private docker network .-> ml
api --> obs
ml --> obs
Three runtime axes behind a Traefik proxy; the ML sidecar is private-network-only.
Outcome
FIVUCSAS runs in production as a self-hosted multi-tenant platform with an embeddable widget, WebAuthn/FIDO2 passkeys, and a randomized active-liveness challenge that defeats screen-replay and pre-recorded attacks. The repository is private pending a third-party security review; source access is available on request.
By the numbers
55+ Flyway migrations
13 FK-cascaded tables behind the users row
3 Runtime axes (API · ML · clients)
Fernet Embedding encryption at rest
SHA-256 Model delivery integrity
WebAuthn Primary login factor
Deep dive
FIVUCSAS — Face and Identity Verification Using Cloud-based SaaS — began as
my senior engineering project at Marmara University and now ships under
RollingCat Software, the umbrella name I
publish some of my work under. This case study is the architecture story; for
the war stories — the three production incidents the team learned the most
from — see the companion
write-up.
The shape of the system
The core insight that shaped almost every decision was simple to state and hard
to enforce: biometric data must never sit unencrypted at rest, and the
embedding extraction process must never be reachable from the internet.
Everything else fell out of that.
That single constraint is why the ML stack lives in a separate FastAPI sidecar
on a private Docker network rather than inside the Spring Boot API. The identity
core is the authoritative source of truth for tenants, users, sessions, audit
logs, and MFA factors (TOTP, WebAuthn, NFC, biometric). The biometric processor
owns the face mesh, embedding extraction, and active-liveness puzzle scoring,
and it answers only to the API — never to a browser.
Active liveness as the differentiator
The “Biometric Puzzle” is the part I am proudest of. Instead of a single still
frame — which a printed photo or a screen replay can defeat — the widget prompts
a randomized sequence of facial actions: smile, blink, look left, look right.
The randomization is what makes a pre-recorded attack impractical: the attacker
cannot know the sequence in advance. The widget exposes this whole flow the way
reCAPTCHA exposes a challenge, so a tenant integrates against the smallest
possible surface.
Privacy and tenancy by construction
Two design choices keep the platform honest:
Schema-per-tenant isolation. Tenant boundaries are a database concern, not
an application filter. There is no shared table where a forgotten predicate
leaks one organization’s data to another.
Fail-fast configuration. The application refuses to start without the
embedding encryption key. A fail-soft default would silently invalidate every
stored embedding — exactly the kind of irreversible data corruption that is
far better to catch at boot than in production.
Operational posture
The platform is self-hosted on a Hetzner CX43 box behind Traefik, observed with
Loki + Promtail + Grafana, and backed up with pgBackRest WAL archiving for
point-in-time recovery. Security hygiene is part of the workflow, not an
afterthought: gitleaks runs in CI, GitHub secret-scanning and push-protection
are on, and every new OAuth endpoint gets a permitAll-chain grep as part of PR
review.
The source is private until a third-party security review completes. Source
access is available on request.