feat(rayverify): consented selfie identity verification at clock-in - #193
Merged
Conversation
RayVerify's identity layer was marketed as coming soon and had no implementation. This builds the real thing: a caregiver enrolls a reference selfie once, and each clock-in selfie is compared against it through AWS Rekognition, which is already the project's BAA-covered vendor for SES, Bedrock, and PHI document storage. The consent gate is the point, not a formality. A face image and anything derived from it is a biometric identifier: PHI under HIPAA, and separately governed by state biometric statutes (Illinois BIPA, Texas CUBI, Washington and a growing list) that require informed consent BEFORE collection and destruction on withdrawal. BIPA carries a private right of action. So consent is a table, not a checkbox: it stores the exact text agreed to and its version, both capture endpoints refuse to store anything without a live consent row, and withdrawing consent deletes the enrollment and the stored image, reporting failure rather than claiming a deletion that did not happen. The client echoes back the consent version it displayed. A mismatch is refused, because a consent record whose wording we cannot vouch for is not evidence of anything. Captures are kept only when they do NOT match. A matched selfie is biometric data with no remaining purpose, and storing one per visit would build a face archive nobody needs; a mismatch is something an agency may have to review. LIVENESS IS NOT IMPLEMENTED, and nothing here pretends otherwise. Rekognition Face Liveness needs the Amplify FaceLivenessDetector client SDK, which requires a custom native build the managed Expo app does not have. Without it a photograph of a photograph passes, so this verifies WHO is in the frame and says nothing about whether they were physically present. Every result carries livenessChecked: false and the status endpoint reports livenessSupported: false, so no caller can quietly assume otherwise. The unconfigured fallback reports not_configured, never a pass. A verification product that reports success when it verified nothing is worse than one that reports nothing at all. Marketing copy moves identity from "Rolling out" to "Live" and keeps liveness and device trust as rolling out, per the guardrail in docs/rayverify-integration.md §7, with the presence-versus-identity distinction stated rather than blurred.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
RayVerify's identity layer was marketed as coming soon and had no implementation. This builds the real thing: a caregiver enrolls a reference selfie once, and each clock-in selfie is compared against it through AWS Rekognition, already the project's BAA-covered vendor for SES, Bedrock, and PHI document storage.
The consent gate is the point, not a formality
A face image and anything derived from it is a biometric identifier: PHI under HIPAA (§164.514(b)(2)(i)(P) lists biometric identifiers and full-face photographs), and separately governed by state biometric statutes — Illinois BIPA, Texas CUBI, Washington and a growing list — that require informed consent BEFORE collection and destruction on withdrawal. BIPA carries a private right of action, which is why it produces the litigation it does.
So consent is a table, not a checkbox:
Captures are kept only when they do NOT match
A matched selfie is biometric data with no remaining purpose, and storing one per visit would build a face archive nobody needs. A mismatch is something an agency may have to review, so that one is retained.
Liveness is NOT implemented, and nothing pretends otherwise
Rekognition Face Liveness requires the Amplify
FaceLivenessDetectorclient SDK, which needs a custom native build the managed Expo app does not have. Without it, a photograph of a photograph passes.So this verifies who is in the frame and says nothing about whether they were physically present. Every result carries
livenessChecked: false; the status endpoint reportslivenessSupported: false; the module and route headers state it; and the marketing FAQ says it in plain language. Shipping something that quietly implied presence would have been worse than shipping nothing.The unconfigured fallback reports
not_configured, never a pass. A verification product that reports success when it verified nothing is worse than one that reports nothing at all.Threshold
90% similarity, stricter than AWS's 80 default. This gates starting a paid shift, so a false accept is worse than a false reject; a rejected check goes through the existing exception path.
Marketing
Identity moves from "Rolling out" to "Live". Liveness and device trust stay "Rolling out", per the guardrail in
docs/rayverify-integration.md§7, and the FAQ now states the presence-versus-identity distinction rather than letting it blur.Migration and config
2026-08-04-add-identity-verification, three tables plus two columns onevv_visits. Needs applying to prod.IDENTITY_VERIFICATION_PROVIDER=rekognitionturns matching on; anything else leaves the no-op. Before enabling: you need an AWS BAA covering Rekognition, and a published retention/destruction schedule. The code enforces the consent gate; the schedule and the consent wording are yours to own.Tests
23 new: consent required before enroll and before verify, stale consent version refused, revocation deletes the image, revocation failure does not report success, capture retained only on mismatch, not-enrolled rather than comparing against nothing, unconfigured surfaced as its own outcome, similarity threshold behavior, "different person" separated from "no face in frame", unreadable source treated as retake not error. All four suites pass, plus typecheck, lint,
sql:scan,security:scan.Not included
The mobile capture UI. The API, storage, consent model, and matching are complete and tested, but no camera screen ships in this PR — it needs
expo-camera(a new native dependency) and device testing that cannot be done from here. Until that lands, the endpoints are reachable but nothing in the app calls them. I would rather land the verified server half than pair it with a camera flow nobody has run on a phone.Also not included: device trust signals, and the standalone RayVerify service integration described in
docs/rayverify-integration.md§2. The provider interface here means that service can later be swapped in behind the same contract.