Skip to content

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

mermaid
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]
    end

Complete Privacy Table

DataRecruiterCandidate
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)
activein_progress
shortlistedadvanced
rejectednot_selected
hiredoffer_extended
withdrawnwithdrawn

Per-Stage Status

Internal stageProgression[].status (recruiter)candidateStatus (candidate)
pendingupcoming
unlockedupcoming
invitedscheduled
in_progressin_progress
completedcompleted
expiredexpired
declineddeclined
skippedskipped

stageProgression[].result (pass / fail / hold) is never mapped to a candidate-visible value. The candidate learns of an outcome change only via the global candidateFacingStatus (e.g., moving from in_progressnot_selected after a failed stage).


Access Scoping Rules

ActionAuth requiredScope
Attend automated screening sessionNo — screeningToken in URLThat one Interview only
Complete a DSA roundNo — dsaToken in URLThat one Interview only
Join a live meetingNo — meetingLinkThat one session
View candidate dashboardYes — Firebase JWTAll CandidatePipeline where participantId matches
View a past interview sessionYes — Firebase JWTOnly if Interview.participantId matches
RSVP accept/decline a live inviteYes — Firebase JWTOnly their own Interview
View recruiter notes / AI scores / feedbacksNeverPrivacy 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 by candidateFacingStatus)
  • stageProgression[].result (internal: pass, fail, hold)
  • notes[]
  • tags

Strips from Interview:

  • feedbacks[]
  • stageData.screeningAiReport
  • stageData.aiReport
  • stageData.screeningResponses[].aiScore
  • stageData.screeningResponses[].aiAnalysis
  • stageData.dsaSubmissions[].score
  • stageData.conversationalTurns[].audioUrl
  • interviewers[].email
  • interviewers[].rsvpStatus
  • hostId
  • result
  • screeningToken
  • stageOverrides