Skip to content

feat(rayverify): consented selfie identity verification at clock-in - #193

Merged
SishirP17 merged 1 commit into
mainfrom
feat/rayverify-identity
Aug 4, 2026
Merged

feat(rayverify): consented selfie identity verification at clock-in#193
SishirP17 merged 1 commit into
mainfrom
feat/rayverify-identity

Conversation

@SishirP17

Copy link
Copy Markdown
Collaborator

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:

  • It stores the exact text agreed to plus a version. A consent record that cannot show what was presented is not evidence of anything.
  • Both capture endpoints refuse to store anything without a live consent row. Enforced in code, because a policy nobody executes is not a defense.
  • The client echoes back the version it displayed; a mismatch is refused rather than recorded as a consent whose wording we cannot vouch for.
  • Withdrawing consent deletes the enrollment and the stored image, and reports failure rather than claiming a deletion that did not happen. Telling somebody their biometric data is gone when it is not would be the worst lie this feature could tell.

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 FaceLivenessDetector client 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 reports livenessSupported: 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 on evv_visits. Needs applying to prod.

IDENTITY_VERIFICATION_PROVIDER=rekognition turns 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.

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.
@SishirP17
SishirP17 requested a review from durga710 as a code owner August 4, 2026 23:55
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
rayhealth-evv-platform-app Ready Ready Preview Aug 4, 2026 11:55pm

@SishirP17
SishirP17 merged commit 56f4a3a into main Aug 4, 2026
14 checks passed
@SishirP17
SishirP17 deleted the feat/rayverify-identity branch August 4, 2026 23:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant