The Candidate Portal closes the loop between "I applied" and "you got hired." Where v0.5 left the candidate with only the thank-you screen and silence, v0.6 gives them a workspace-scoped account, an applications dashboard, self-serve interview scheduling, and a friendly status timeline.
The candidate journey end-to-end
- Discover. A candidate finds your job board at
/{slug}/jobsand clicks Apply on a role. - Sign up — CV first. They're redirected to
/{slug}/candidate/signup. The form opens with an optional CV upload as the very first step: drop a PDF and we (a) store it so the apply step reuses it, and (b) best-effort parse first name, last name, email, and phone to pre-fill the rest of the form. Every pre-filled field stays editable, and the guest can skip the upload entirely and type everything by hand. They then complete nationality + password. We create aPerson(statusCANDIDATE, withcvUrlset if a CV was uploaded) and aUser(roleCANDIDATE) atomically, setemailVerifiedAtso the next login works immediately, and sign them in. Extraction is quota-gated and degrades silently to blank fields if the PDF is unreadable or the AI quota is exhausted. - Apply. They're returned to the original apply page, now pre-filled and read-only above a slim CV-upload + cover-note form. If they uploaded a CV at signup it's already on file (
Person.cvUrl), so the apply form needs only a cover note — no second upload. Submitting creates thePipelineEntry, kicks the existing CV-parse pipeline, and routes them to/{slug}/candidate/applications/[id]. - Follow up. From their dashboard at
/{slug}/candidatethey see open applications, upcoming interviews, the latest status update, and a browse-jobs CTA. Clicking through to an application detail shows the friendly status badge, the role description, and stage-specific affordances — slot picker if interviewing, offer download if offered. - Hired. When HR moves them to
HIRED, the existing handoff flipsPerson.statustoEMPLOYEEandUser.rolefromCANDIDATEtoEMPLOYEE. Their next login lands on/me. The candidate portal URL still resolves and shows their application history, but the role redirect at the layout level sends them to the employee surface by default.
Assessments: two entities, many chapters
Once an application passes screening, HR may assign assessments. No matter how many individual assessments are assigned, the candidate sees exactly two entities at /{slug}/candidate/assessments:
- General Assessment — every
GENERAL-type assessment, grouped into one card. - Technical Assessment — every
TECHNICAL-type assessment, grouped into one card.
Each card treats its underlying assessments as chapters of one long assessment. The card shows a progress bar ("2 of 3 chapters completed"), a numbered chapter list with per-chapter status (not started / in progress / completed), and a single Start or Continue button that jumps to the first incomplete chapter.
The run experience is continuous: finishing one chapter flows straight into the next incomplete chapter of the same entity instead of bouncing back to the list, with a "Technical Assessment · Chapter 2 of 3" sub-heading for orientation.
Once every chapter of an entity is completed, the card collapses into a single record: just the entity name (General Assessment / Technical Assessment), a Completed badge, and a short reassurance line ("Submitted — the hiring team is reviewing your results."). The progress bar, the chapter list, and the Start/Continue button all disappear — there is nothing left for the candidate to do. "Completed" is a derived display state, not a stored field, so the underlying per-assessment scoring and reporting are unchanged.
Candidates never see an assessment report. No scores, no narrative, no PDF, and no chapter-level breakdown are exposed to the candidate at any point — not on the list, not on the completion screen, and not via the report route (which redirects back to the list). Assessment results stay with the hiring team in the recruiter/manager surfaces. The completion screen after submitting the final chapter simply confirms "Your responses have been received."
A card only appears if the candidate has at least one assessment of that type; a candidate with no assessments sees the empty state.
Technical assessments may include multiple formats. Within the Technical Assessment card, a chapter may be an MCQ bank, a written case study, or a scheduled interview — depending on which formats the admin included when assigning the template. The candidate completes each format as a separate chapter: MCQ and case study are self-serve (open and submit when ready); the interview chapter shows a slot-picker to book a time. The Technical Assessment card does not show as Completed until every format — including any pending case study review by HR and any scheduled interview — has reached a terminal state.
Navigation badges
The portal nav shows a small count badge on tabs that need attention. Two kinds:
- Assessments — live count. Shows the number of unfinished assessments (
ASSIGNED+IN_PROGRESS). It only goes down as the candidate completes work; opening the tab does not clear it. - Applications & Interviews — unseen notification. These clear the moment the candidate opens the tab and re-appear only when newer activity arrives — a new application (
Applications) or a newly offered/confirmed interview (Interviews). The "opened" moment is recorded in a per-browser cookie stamped by middleware, so the badge clears on the very render that serves the tab.
The same mechanism powers the employee portal (/me — live Assessments count, unseen Interviews badge) and the trainer portal (/trainer — unseen Applications badge). Shared helper: lib/portal/navSeen.ts.
Per-workspace identity (v0.6)
A candidate who applies to three companies on Jadiroon will sign up three times. Each User is workspace-scoped — the same email can exist as separate User rows across workspaces, each with its own password and role. Cross-workspace candidate identity is on the v0.7 watch list; v0.6 keeps tenant isolation crisp at the cost of repeated signup friction.
What the candidate can see vs what HR sees
The visibility contract is enforced by lib/candidate/applications.ts, which whitelists fields via explicit Prisma select clauses. The list and detail routes never return:
screeningScorescreeningNarrative(the LLM-generated fit narrative)cvParseStatusPipelineEntry.notes(cover letter is held there today — not returned)InterviewNote.rating/InterviewNote.text(recruiter feedback after the interview)
Stage names are mapped to friendly labels via lib/candidate/friendly-labels.ts:
Internal Stage | English label | Arabic label |
|---|---|---|
APPLIED | Application received | تم استلام طلبك |
SCREENED | Under review | قيد المراجعة |
INTERVIEWING | In interview process | في مرحلة المقابلات |
OFFERED | Offer pending your response | عرض بانتظار ردك |
HIRED | Welcome aboard | أهلاً بك في الفريق |
REJECTED | Not selected this time | لم يتم اختيارك هذه المرة |
HR side keeps showing the raw Stage enum on the Kanban — no change to the existing hire surface.
Apply-once enforcement
PipelineEntry @@unique([pipelineId, personId]) is the database-level guarantee that a candidate cannot have two entries on the same role. The apply API returns 409 with the existing entry id, and the apply page renders an Already applied view with a link to the application detail page.
What HR sees that's new
- The candidate notification (
CANDIDATE_APPLIED) still fires for every new application. - A new application from a logged-in candidate has the same shape on the Kanban as any other — there's no badge differentiating "logged-in" from "anonymous" since the anonymous flow is gone.
- The hire transition (drag to
HIRED) callshandoffCandidateToEmployee()which now ALSO flips the candidate'sUser.roleif one exists. No additional click needed.
Limits to know about (v0.6)
- No candidate-side email notifications. Status changes show up in the dashboard only. HR-side acknowledgement/rejection/offer emails still fire as before.
- No self-withdraw. Candidates can't pull their own application; HR must move them to
REJECTED. - No saved searches or job alerts. Browse Jobs is a static, filterable list.
- No OAuth signup. Credentials provider only (email + password).
- CV re-upload doesn't re-parse. Profile-side CV updates write to
Person.cvUrlbut do not re-triggercvParseStatus. Parsing runs only on apply submission.
See docs/superpowers/specs/2026-05-13-v06-candidate-portal-design.md for the full design and …-decisions.md for the locked architectural decisions.
Deleting your account
A candidate can permanently delete their own account from Profile → Delete account. Confirm by re-entering your password. This immediately and irreversibly removes your account and all related data — your applications, assessments, interviews, growth plans, and uploaded files (CV and documents). Your login is deleted, and the workspace's HR admins and line managers are notified that you deleted your account.