Data Ownership & Privacy
The privacy boundary between recruiter-internal data and candidate-visible data is a product contract, not just an implementation detail. It is enforced by a strict projection function in the service layer — not by route-level filtering.
Ownership Diagram
graph LR
subgraph "Recruiter-only — never exposed to candidates"
A[CandidatePipeline.notes]
B[Interview.stageData.aiReport]
C[Interview.stageData.screeningAiReport]
D[Interview.feedbacks]
E[CandidatePipeline.stageProgression.result]
F[Interview.interviewers.email + rsvpStatus]
G[Interview.stageData.screeningResponses.aiScore]
H[Interview.stageData.dsaSubmissions.score]
I[Interview.stageData.conversationalTurns.audioUrl]
end
subgraph "Candidate-visible"
J[Interview.status / schedulingType]
K[Interview.startTime / endTime / expiresAt]
L[Interview.meetingLink]
M[stageData.screeningResponses — own answers + questionText only]
N[stageData.dsaSubmissions — own code + per-test pass/fail]
O[stageData.aiTechnicalResponses — own Q&A transcript]
P[CandidatePipeline.candidateFacingStatus]
Q[CandidatePipeline.stageProgression.candidateStatus]
R[Interview.candidateAggregateScore — single 0-100 after completion]
endComplete Privacy Table
| Data | Recruiter | Candidate |
|---|---|---|
| Job title, org name | ✅ | ✅ |
| Stage name (human label) | ✅ | ✅ |
Stage type key (automated_screening, etc.) | ✅ | ❌ |
Interview status (scheduled, in_progress, completed) | ✅ | ✅ |
startTime / endTime for live rounds | ✅ | ✅ |
expiresAt for async deadlines | ✅ | ✅ |
| Meeting link | ✅ | ✅ |
| Interviewer names | ✅ | ✅ (name only) |
| Interviewer emails | ✅ | ❌ |
| Interviewer RSVP status | ✅ | ❌ |
| Candidate's own screening answers | ✅ | ✅ |
| Candidate's own DSA code + per-test pass/fail | ✅ | ✅ |
| Candidate's own AI conversation transcript | ✅ | ✅ |
| AI overall score / recommendation | ✅ | ❌ |
| AI per-answer analysis | ✅ | ❌ |
| DSA aggregate score (% tests passed) | ✅ | ❌ |
| Audio recordings of sessions | ✅ | ❌ |
| Human interviewer ratings / comments | ✅ | ❌ |
| Human interviewer criteria scores | ✅ | ❌ |
| Human interviewer recommendation | ✅ | ❌ |
Internal stage result (pass / fail / hold) | ✅ | ❌ |
| Aggregate score per completed stage (0–100 single number) | ✅ | ✅ (after stage completes) |
| Candidate-facing stage status (coarse-grained) | ✅ | ✅ |
Recruiter's internal pipeline status (shortlisted, rejected) | ✅ | ❌ |
Candidate-facing global status (advanced, not_selected) | ✅ | ✅ |
| Recruiter notes | ✅ | ❌ |
| Other candidates' data | ✅ | ❌ |
Status Vocabulary Mapping
Recruiters and candidates use different vocabulary to describe the same underlying state. The API applies this mapping automatically — recruiters never manually set candidateFacingStatus.
Global Pipeline Status
Internal status (recruiter) | candidateFacingStatus (candidate) |
|---|---|
active | in_progress |
shortlisted | advanced |
rejected | not_selected |
hired | offer_extended |
withdrawn | withdrawn |
Per-Stage Status
Internal stageProgression[].status (recruiter) | candidateStatus (candidate) |
|---|---|
pending | upcoming |
unlocked | upcoming |
invited | scheduled |
in_progress | in_progress |
completed | completed |
expired | expired |
declined | declined |
skipped | skipped |
stageProgression[].result(pass/fail/hold) is never mapped to a candidate-visible value. The candidate learns of an outcome change only via the globalcandidateFacingStatus(e.g., moving fromin_progress→not_selectedafter a failed stage).
Access Scoping Rules
| Action | Auth required | Scope |
|---|---|---|
| Attend automated screening session | No — screeningToken in URL | That one Interview only |
| Complete a DSA round | No — dsaToken in URL | That one Interview only |
| Join a live meeting | No — meetingLink | That one session |
| View candidate dashboard | Yes — Firebase JWT | All CandidatePipeline where participantId matches |
| View a past interview session | Yes — Firebase JWT | Only if Interview.participantId matches |
| RSVP accept/decline a live invite | Yes — Firebase JWT | Only their own Interview |
| View recruiter notes / AI scores / feedbacks | Never | Privacy boundary enforced in projection layer |
The candidateProjection() Function
Applied in src/utils/candidateProjection.ts before any candidate API response. It is a whitelist projection — only explicitly listed fields pass through.
Strips from CandidatePipeline:
status(internal:shortlisted,rejected— replaced bycandidateFacingStatus)stageProgression[].result(internal:pass,fail,hold)notes[]tags
Strips from Interview:
feedbacks[]stageData.screeningAiReportstageData.aiReportstageData.screeningResponses[].aiScorestageData.screeningResponses[].aiAnalysisstageData.dsaSubmissions[].scorestageData.conversationalTurns[].audioUrlinterviewers[].emailinterviewers[].rsvpStatushostIdresultscreeningTokenstageOverrides