Utkrushta · Engineering Design Doc · For team review
Rev 2 · signals-only · own-domain iframeMake a candidate's identity continuous — the person on the resume, the person who sits the assessment, and the person who joins should be the same human — and give recruiters the signals to see it.
The problem. Hiring is now remote and global, and the weakest link is identity continuity. A résumé can be real, a background check can pass, and the interview can go well — yet the person who actually sits our assessment (and later joins) may not be the same human. Proxy test-takers, credential-sharing, and AI-era fraud (deepfakes, synthetic "candidates") make this a live risk: Gartner projects 1 in 4 candidate profiles will be fake by 2028. Our own candidates now span four continents, which multiplies both the exposure and the legal surface.
What this is — and isn't. This is not a background-verification product, and not a deepfake-detection lab. It's an identity-continuity + fraud-signals layer built into our assessment runtime: capture a verified identity at the moment of the test, surface integrity signals to the recruiter, and (next) re-verify that same identity at the offer→join step.
Buy the commodity biometrics, build the orchestration and signal-fusion that is our IP. Inform recruiters with signals; never block a candidate.
Every assessment carries an identity signal (verified? face-match? liveness? document authenticity?) plus fraud signals (VPN / datacenter IP, location mismatch, wholesale paste). The recruiter gets evidence they can act on — proxy candidates and location-maskers become visible.
Signals, not gates — never block, never judge the candidate in-flow. One global vendor, not a per-country roster. Tier by fraud signals, never nationality. Store no raw biometrics — only vendor references + scores. Reuse existing app patterns (consent, DeviceCheck, webhooks).
The end-state binds one identity across the whole funnel. This doc delivers the middle and sets up the right; the far edges are deliberately deferred.
the claimed person
BGV — out of scopethe test-taker
This spec · Phase 1–3the joiner
Offer→Join toolLater, deferred: continuous in-assessment re-verification (anti hand-off) and deepfake / camera-injection detection. We reach for a specialist vendor there only when an enterprise customer funds it.
This design wasn't top-down. It was shaped step by step by five founder directions. Reading the arc explains why the choices are what they are.
dev) + backend (main) — including the offer→join tool.DeviceCheck screen + consent JSONB are the reuse points; red_flags must NOT hold identity/IP data (it auto-disqualifies candidates + blocks certificates); coding runs in remote E2B (browser can't see coding pastes).consent JSONB; add 2 purpose-built tables (never red_flags); scope paste-signals to in-browser surfaces.Identity verification is another fraud signal shown to the recruiter, like IP intel and paste signals. The candidate always proceeds — no blocking, no candidate-facing pass/fail, no “In Review” state, no new session status.
Didit's inline iframe embed, not the hosted redirect. The candidate never leaves our page, and raw biometrics still go browser → Didit, never touching our servers.
Now includes a current-vs-future flow (§5), faithful screen renderings from the real app components (§6), and inline option-pickers with our pick marked (§4).
Every option stays visible; the highlighted card is our recommendation. Push back on any of them.
Send the candidate to verify.didit.me. Simplest — but they leave our domain mid-assessment.
@didit-protocol/sdk-web, embedded:true. Candidate stays on our page; biometrics go browser → Didit, never our servers.
Standalone API — we render the camera, then POST raw images to Didit → we become a biometrics processor (BIPA/GDPR). Avoid.
Never blocks. Result → recruiter panel only. No new states, simplest, legally safest.
Proceed but show a candidate-facing “under review”. Adds states + support load for no pilot benefit.
Block until verified. Needs a non-biometric alternative + appeal path; highest friction & legal load.
Just look at the camera. One frame, no blink/turn/smile. Lowest friction; iBeta L1.
Colored light pulses on screen, hold still. Stronger anti-spoof, still near-passive. Easy toggle later.
Turn your head + flash. Strongest, but adds an explicit action & more retries.
Liveness is a per-workflow toggle in Didit — start Passive, dial up if fraud data justifies it. Mechanic: Didit reads the camera and its anti-spoof model decides live-vs-spoof; "3D Flash" bounces colored light off the face to detect a real 3D surface vs a photo/screen. (Confirm the iBeta certificate — pages say Level 1.)
Exactly one non-blocking step is inserted. Nothing else in the candidate journey changes.
invite / resume
tasks only
camera + mic + consent
optional
test / task
invite / resume
tasks only
camera + mic + consent
ID + selfie + liveness
NEW · non-blockingoptional
test / task
It sits right after Device Check (which releases its camera stream at "Continue") and before proctor recording starts — so only one thing uses the camera at a time. Because it never blocks, we don't move provisioning behind it; sandbox/droplet provisioning fires as it does today while the candidate does the ~1-minute capture. The candidate advances the instant capture completes; Didit's decision arrives async and becomes a recruiter signal.
Mockups reuse the app's actual tokens — brand green #1B6740, Poppins, 8px cards, the DeviceCheck skeleton. Blue-dashed = the Didit iframe.
A quick check (~1 min) — scan a government ID and take a selfie.
Processed by Didit. We never store your ID images.
URL bar still ours. The candidate never leaves the assessment.
Passive by default. (Optional step-up: colored-flash or head-turn.)
Continuing to your assessment…
Static mockups for review — colors, cards, buttons, and the consent row match the live app tokens (§12).
The whole point: everything above becomes informational signals in the report. No candidate is blocked; the recruiter weighs the evidence, alongside the existing score/proctoring surface.
Mixed states shown on purpose — signals inform, they don't auto-reject.
Service: new FastAPI router fastapi_service/routers/v2/identity_verifications.py.
FE → Next proxy → FastAPI POST /v2/identity-verifications calls Didit POST /v3/session/ (x-api-key, server-only) with workflow_id + vendor_data=<our session_id>. Returns session_token + url. Insert an identity_verifications row PENDING; idempotent in-flight lookup protects the 500/mo quota.
DiditSdk.shared.startVerification({ url, configuration:{ embedded:true, embeddedContainerId } }). Document + selfie + liveness run in the iframe; raw biometrics never touch our servers. On didit:completed we advance the candidate — we do not wait on the decision.
Didit → FastAPI POST /v2/identity-verifications/webhook (public). Verify X-Signature-V2 (HMAC-SHA256; reject if |now − ts| > 300s). Idempotent upsert on provider_session_id keyed by event_id. Store a trimmed decision (scores, doc country) — no raw images, only Didit signed-URL refs. This row is the recruiter signal.
Confirm iframe camera permission across target mobile browsers (Didit flags iframe camera as occasionally problematic → graceful redirect fallback). Confirm exact iBeta level (pages say L1). Package is @didit-protocol/sdk-web — not the similarly-named web3 wallet SDK.
For each store: what, why it's required, and which alternatives were rejected. Signals-only means the identity status is pure internal plumbing — never a candidate-facing state.
Add key identity: ISO|null
recordSessionConsent()consent JSONBWhy: 1:1 with the session, needs a provable timestamp, already has a home — SessionConsent = { camera, screenshare } written by recordSessionConsent() (testSessionsOperations.ts:1088). Add identity to the type + merge line.
Rejected: a biometric_consents table (over-engineering a timestamp that already has a home); position-level consent (wrong grain).
identity_verificationsWhy a table, not columns on the session rows:
varchar, task_sessions is uuid; the dual-FK report_comments table already solves this.extension_captures.Rejected: red_flags (see iii); task_sessions.result_analysis (OCR payload, wrong provenance); columns on both tables.
session_risk_signals — NOT red_flagsred_flags is an active disqualification channel: competency_leaderboard_dag.py::has_disqualifying_red_flags treats any non-empty flag as disqualifying; certificate_generation_dag.py blocks certificates on it. A VPN or identity signal there would silently disqualify legit candidates + block certs.
Why a table: 1:N per session, written for every candidate at session-start. Separate = zero blast radius.
Rejected: red_flags; a single JSONB column (loses append-only + clean RLS).
Capture X-Forwarded-For at session-start; enrich with proxycheck.io (free) + self-hosted MaxMind GeoLite2 ASN+City (geoip2 dep + GEOIP_DB_PATH already wired). Store session_risk_signals; surface the Integrity panel. Ships independently of the vendor.
(a) Strengthen the existing OCR paste_overlap.py wholesale-paste flag. (b) Live onPaste on in-browser surfaces only (tiptap + MCQ) via trackEvent. (c) E2B coding is out of browser scope. No new-tab detection (AI allowed).
FE: IdentityCheck.tsx (mirror DeviceCheck) + inline @didit-protocol/sdk-web; new non-blocking step in page.tsx; extend SessionConsent + writers. BE: didit_client.py, v2 router + webhook, DAO + model, migration. No enforcement flag (signals-only).
See §10 — after Phase 3.
Periodic in-assessment face-presence vs mid-test hand-off.
Buy-or-defer (iProov/FaceTec class); procedural defenses only for now.
Sequence: P1 (no vendor dep) → P3 (needs Didit account) → P2 (parallel) → Offer→Join. Total ≈ 16–24 eng-days.
Goal: a recruiter gives their client a single-use link; the joiner takes a selfie re-verified against the assessment-time enrolment — binding joiner == test-taker (Stage 3 of the §1 diagram).
Recruiter mints a single-use join_token (clone the existing recruiter-utkrusht share-report flow — UUID bearer token + OTP-gate + WhatsApp delivery). The hosted selfie page (utkrushta-assessment/src/app/verify-join/[token], public) calls Didit POST /v3/session/ with a biometric-auth workflow_id + the enrolment's portrait_image reference; Didit returns a face-match 0–100. Result lands via the same webhook into an identity_verifications row with is_enrolment=false + reference_verification_id.
Pull the reference portrait from Didit's signed URL at re-verify time rather than warehousing the selfie (confirm signed-URL lifetime; fallback = store the portrait). This is why the Phase-3 table carries is_enrolment + reference_verification_id + the null-session CHECK.
Open questions: joiner's own consent tick · token delivery channel · fallback when the candidate skipped identity at assessment time (no enrolment → standalone verification) · recruiter-utkrusht backend-call helper not yet fully traced.
red_flags silently disqualifies candidates + blocks certs. Separate tables prevent this; enforce in review.ProctorTusRecorder + DeviceCheck all want the camera; sequence identity before proctor recording (DeviceCheck releases its stream @464).allow="camera" + permissions-policy; graceful redirect fallback where blocked.X-Signature-V2 on raw bytes, idempotent by event_id, 200 on unknown.sandbox_scenario) — assert the iframe renders inline (no redirect), capture-complete advances without waiting on the decision, the webhook writes the signal, idempotent create (double-click → one session), and X-Signature-V2 rejection on a tampered body. Drive via the test-candidate-flow skill.session_risk_signals correctness + Integrity panel.is_enrolment=false row updates via the shared webhook; single-use + expiry.red_flags.Separate biometric-consent tick · signals-only (no gate → lower "freely given" risk) · DPIA before launch · store no raw biometric images (Didit references only) · targeted retention/erasure via the dedicated table · Illinois BIPA notice/retention.