Candidate accounts and talent

Build candidate profiles and resume import, then expose the permitted talent directory.

Build two deliberately separate experiences: a candidate’s private profile editor under board.me, and the board’s public or employer-only talent directory under board.talent.

Prerequisites

  • board.context().features.candidates is true for candidate accounts.
  • A server-owned board-user session for every board.me request.
  • talentDirectoryVisibility checked before creating a talent route.

The examples expect a stateless server client and pass the access token per call.

Load and update the candidate profile

ts
// src/data/candidate-profile.ts
import type { FetchOptions, UpdateCandidateProfileBody } from '@cavuno/board';
import { board } from '../lib/board';
function session(accessToken: string): FetchOptions {
return {
cache: 'no-store',
headers: { authorization: `Bearer ${accessToken}` },
};
}
export function loadCandidateProfile(accessToken: string) {
return board.me.profile.retrieve(undefined, session(accessToken));
}
export function updateCandidateProfile(
accessToken: string,
input: UpdateCandidateProfileBody,
) {
return board.me.profile.update(input, undefined, session(accessToken));
}

update is a merge patch: omitted fields remain unchanged. Check handle availability before submission for responsive feedback, but treat the update response as authoritative because availability can change.

Import a resume

Resume parsing is asynchronous. Upload once, then poll until parseStatus is parsed or failed.

ts
export async function importResume(accessToken: string, file: Blob) {
const options = session(accessToken);
let resume = await board.me.resume.upload(
file,
{ keepResumeOnFile: true, importMode: 'append_only' },
options,
);
while (resume.parseStatus === 'parsing') {
await new Promise((resolve) => setTimeout(resolve, 1_500));
resume = await board.me.resume.retrieve(options);
}
if (resume.parseStatus === 'failed') {
throw new Error(resume.parseFailureReason ?? 'Resume parsing failed');
}
return board.me.profile.retrieve(undefined, options);
}

Use replace_all only after an explicit confirmation. The SDK requires confirmReplaceAll: true for that destructive import mode.

Load the talent directory

ts
export async function loadTalent(
skill: string | undefined,
employerAccessToken?: string,
) {
const context = await board.context();
if (context.talentDirectoryVisibility === 'off') return null;
return board.talent.list(
{ skill, limit: 20 },
employerAccessToken ? session(employerAccessToken) : undefined,
);
}

Only public profiles appear. On an employers_only board, anonymous requests throw a 403 talent_directory_restricted; render an employer sign-in or upsell rather than pretending the directory is empty.

Expected result

The private account loader returns the signed-in candidate’s profile and parsed resume state. The talent loader returns only profiles visible to the current viewer, or no route when the directory is off.

Verify the result

  • Candidate profile responses never enter a shared cache.
  • A profile update returns the edited values from the server.
  • Resume import reaches parsed or shows the returned failure reason.
  • Hidden and logged-in-only profiles do not appear in anonymous talent reads.
  • An employers-only directory distinguishes 403 restricted access from 404 disabled access.

Production cautions

  • Stop polling when the request is aborted or the user leaves the page.
  • Keep short-lived resume download URLs out of logs and durable caches.
  • Do not expose board.me profile data through public loaders.
  • Treat jobSearchStatus: null in a talent profile as an intentional visibility outcome.

Next, let candidates act on jobs with Applications.