NetraID Source Map

Offline facial recognition and liveness detection for NHAI Datalake 3.0. An interactive guide to the codebase, the on-device models, and the integration points.

What this is

A drop-in React Native module. Face recognition and liveness run entirely on the handset. The network is optional and is used only to drain an offline queue.

Capture Detect + landmarks Liveness Align + embed Match Encrypted store

Measured on a Vivo V2246, a 3 GB-class mid-range handset

MetricMeasuredRequirement in the brief
On-device model footprint17.3 MBaround 20 MB, smaller is better
Full verification verdict371 to 604 msunder 1 second
Recognition accuracy99.76 percent on LFWabove 95 percent
Genuine match score on device0.65 to 0.86accept threshold 0.38
Minimum Android8.0, minSdkVersion 26Android 8.0 and above
GPUnot required, CPU delegate onlyno high-end GPU
Licensing. Every component is MIT or Apache-2.0. EdgeFace scores higher than MobileFaceNet on paper and was rejected because its licence forbids commercial use. There is no runtime fee and no per-device cost anywhere in this stack.

Module map

All 22 files under app/src/netraid/ and app/src/screens/. Select one for what it owns, what it exports, and what it connects to. Exports, imports and line counts are extracted from the shipped source, so this cannot drift from the code.

Dependency graph

Generated from the actual import statements in the shipped source rather than drawn by hand: 22 modules, 47 edges. Click any module to isolate it and see exactly what it imports and what imports it.

Nothing selected

Camera-free demo path

Storage and sync

NetraID core

Screens, the host app mounts these

HomeScreen.tsx

EnrollScreen.tsx

VerifyScreen.tsx

PipelineDemoScreen.tsx

index.ts

frameProcessor.ts

liveness.ts

chroma.ts

recognition.ts

align.ts

blazeface.ts

quality.ts

math.ts

types.ts

calibration.ts

screenLamp.ts

store.ts

keys.ts

sync.ts

demoPipeline.ts

demoAssets.ts

imaging.ts

How to read it. An arrow from A to B means A imports B, so B knows nothing about A. types.ts sits at the bottom because everything depends on the configuration and nothing depends back on it. index.ts is the only module a host application calls. The screens depend on the core and the core never depends on a screen, which is what makes this a drop-in module rather than an app.

One verification, start to finish

What runs, in order, and on which thread.

#StepWhereNotes
1Frame arrivesworklet threadEvery second frame is processed; the rest are dropped to bound worklet memory
2Sensor warm-upworkletThe first 1.2 seconds are discarded while exposure and white balance converge
3DetectBlazeFaceBounding box and 6 keypoints, with adaptive gain applied in dim light
4LandmarksFaceLandmarker468 points, giving eye aspect ratio, mouth width ratio and yaw
5Active challengeworklet FSMRandom order. Each step requires a confirmed neutral face, then the gesture no sooner than 350 ms and no later than 4 seconds
6Continuity bindingworkletFrom the last gesture through the capture, the same tracked face must remain in frame
7Passive anti-spoofMiniFASNetBGR in [0,1], crop 2.7x the face box, padded rather than clamped
8Quality gatesworkletFrontal, sharp, correctly exposed, and colour balance settled
9AlignJS thread5-point similarity transform to 112x112
10EmbedMobileFaceNet512-d vector, with flip test-time augmentation
11MatchJS threadCosine against enrolled templates, aggregated across 3 captures with a margin rule
12RecordSQLCipherAttendance row queued locally, encrypted
Liveness is evaluated before identity. A failed liveness verdict short-circuits the attempt: no embedding is computed and no match is attempted.

On-device models

Four TFLite graphs, 17.3 MB together. All run on the CPU delegate, so no GPU is required.

ModelSizeLicenceInput conventionUsed for
blazeface_short_range0.22 MBApache-2.0RGB 128x128, [0,1]Face detection
face_landmarker2.44 MBApache-2.0RGB 256x256468 landmarks, gesture geometry
minifasnet_fp321.67 MBApache-2.0BGR 80x80, [0,1], crop 2.7x face boxPassive anti-spoof
mobilefacenet_f3213.00 MBMITRGB 112x112, aligned512-d face embedding
The MiniFASNet input convention is verified, not assumed. ml/scripts/06_verify_minifasnet_fidelity.py runs identical tensors through the original PyTorch weights and the shipped TFLite file. Fed [0,1] they agree to 3e-8. Fed raw 0-255 they differ by 0.99, and the model then returns P(real) = 0.995 on random noise, which means it is no longer computing anything meaningful. Run this check before changing preprocessing and after any re-conversion.

Reproducing every model from source

cd ml
python scripts/01_download_models.py            # fetch upstream weights
python scripts/02_convert_to_tflite.py          # recognition graph
python scripts/04_export_app_models.py          # stage into app/assets/models
python scripts/05_convert_minifasnet.py         # anti-spoof graph
python scripts/06_verify_minifasnet_fidelity.py # ground-truth check

Configuration reference

Every operating point lives in app/src/netraid/types.ts as DEFAULT_CONFIG. The values below are what ships.

Recognition

FieldValueMeaning
matchThreshold0.38Minimum cosine similarity to accept a match
matchMargin0.08Required gap over the next-best different person
verifyShots3Frames aggregated per verification
enrollShots6Candidate frames per enrollment

Liveness gates

GateOperating pointState
Active challenge2 steps, drawn without replacement, 350 ms to 4 s eachENFORCED
Continuity binding600 ms gap, 0.22 positional jump, 1.7x scale changeENFORCED
passiveModepassiveThreshold 0.15 on the medianMEASURED
screenSpoofModescreenSpoofMax 0.85 on the consensusMEASURED
enrollSpoofModeenrollPassiveThreshold 0.08MEASURED
chromaModechromaThreshold 1.25OFF
Why some gates report rather than reject. A gate is armed only where a measurement on the deployment hardware supports it. On our test handset the passive model's live and attack distributions were not separated stably enough to justify a threshold, so it records its reading on every attempt instead of rejecting. docs/CALIBRATION.md is the procedure for arming it, and it is roughly a thirty-minute job on a target device. A threshold carried over from other hardware is not a security control.

Integration into Datalake 3.0

One module directory, two call sites, and one optional endpoint.

1. Copy the module

cp -r app/src/netraid   <datalake>/src/netraid
cp -r app/assets/models <datalake>/assets/models

2. Peer dependencies

npm install react-native-vision-camera react-native-worklets-core \
            react-native-fast-tflite @op-engineering/op-sqlite \
            react-native-uuid @react-native-community/netinfo

3. The two call sites

import { NetraID } from "./netraid";

await NetraID.init();

// Enrollment, 6 quality-gated shots
await NetraID.enroll({ personId: "NHAI-04821", captures });

// Verification, 3-frame aggregate
const r = await NetraID.verify({ captures, requireLiveness: true });
// r -> { ok, personId, score, liveness, elapsedMs }

captures come from useNetraFrameProcessor, which emits only quality-gated, liveness-passed, aligned 112x112 crops. enroll throws DuplicateFaceError if the face is already enrolled under a different id: one face, one identity.

Alternatively, mount the ready-made screens (HomeScreen, EnrollScreen, VerifyScreen) straight into an existing navigator.

Sync and purge contract

The only network dependency. Authentication itself never needs a server.

Request

POST /v1/attendance/sync
Content-Type: application/json
X-Device-Id: <device uuid>
Authorization: Bearer <JWT>

{ "records": [
    { "id": "<client uuid>", "personId": "NHAI-04821", "ts": 1755500000000,
      "siteId": "TOLL-12", "lat": 19.07, "lng": 72.87,
      "deviceId": "<device uuid>", "livenessPassed": true, "matchScore": 0.86 }
] }

Response

{ "results": [ { "id": "<client uuid>", "status": "ok" } ] }
Why purging is safe. Every record carries a client-generated UUID, and the server writes with ConditionExpression: attribute_not_exists(id), so a retry cannot duplicate a record. The device deletes a local row only after the server returns status: "ok" for that id. Anything not acknowledged stays queued.
The AWS stack is a reference, not a requirement. backend/ implements this endpoint on API Gateway, Lambda and DynamoDB so the contract and its idempotency behaviour are unambiguous. A deployment can point the client at Datalake 3.0's own backend and delete it entirely.

Platform status

Stated precisely, including what has not been verified.

PlatformStatusEvidence
AndroidVERIFIED ON DEVICESigned release APK built from source for all four ABIs. Enrollment, verification and matching exercised on a Vivo V2246
iOSBUILDS IN CIXcode build succeeds in GitHub Actions, with simulator video and screenshots. A device pass on the current revision is pending
iOS, stated plainly. The NetraID core is TypeScript and platform-neutral, and app/ios/ has camera permissions and deep links wired. One native module, ScreenLampModule, is Android-only and is called behind a platform guard, so iOS is unaffected by it. What has not happened is a run of the current revision on physical iOS hardware. The deployment target follows React Native 0.76, which is iOS 15.1; embedding the module sources into a host app built on an older React Native reaches the iOS 12 floor named in the brief. See docs/INTEGRATION.md.

Build from source

cd app && npm install --legacy-peer-deps
cd android && ./gradlew assembleRelease

This produces one APK per ABI plus a universal one. On Windows the native build stages itself at a short path automatically, because a stock React Native 0.76 project exceeds the 260-character path limit on its own. See docs/BUILD.md.