Employer self-service
Let approved employers claim companies, manage jobs, and review applicants.
A
JBuild an employer portal around board.me.companies. Every method is authenticated and scoped to a company membership; public company data remains under board.companies.
Prerequisites
board.context().features.employersistrue.- A server-owned employer session.
- A protected account area with
cache: 'no-store'. - UI states for
approved,pending_work_email,awaiting_admin, andrejectedmemberships.
Claim a company
123456789101112131415161718192021222324252627282930313233// src/data/employer.tsimport type { FetchOptions } from '@cavuno/board';import { board } from '../lib/board';function session(accessToken: string): FetchOptions {return {cache: 'no-store',headers: { authorization: `Bearer ${accessToken}` },};}export async function claimCompany(accessToken: string,companySlug: string,) {return board.me.companies.claim(companySlug,session(accessToken),);}export async function sendWorkEmail(accessToken: string,companySlug: string,workEmail: string,) {return board.me.companies.workEmail.verify(companySlug,{ workEmail },session(accessToken),);}
If the membership is pending_work_email, send the verification link. The confirmation method uses the emailed token as authorization and does not require a board-user session:
1await board.me.companies.workEmail.confirm(companySlug, { token });
Do not unlock management screens until the returned membership is approved.
Create and publish a job
123456789101112131415161718192021222324export async function createEmployerJob(accessToken: string,companySlug: string,) {const options = session(accessToken);const draft = await board.me.companies.jobs.create(companySlug,{title: 'Staff Engineer',description: 'Build reliable systems for our customers.',applicationUrl: 'https://acme.example/careers/staff-engineer',employmentType: 'full_time',remoteOption: 'remote',remotePermits: [{ type: 'worldwide', value: 'worldwide' }],},options,);return board.me.companies.jobs.publish(companySlug,draft.id,options,);}
New jobs are held drafts. publish works only while an existing paid window permits it. A held draft that needs payment must use jobs.checkout; branch on checkout, published, or invoice_sent and never infer publication from a browser redirect.
Load the applicant pipeline
12345678910111213141516171819202122232425262728export async function loadPipeline(accessToken: string,companySlug: string,jobId: string,) {return board.me.companies.applicants.list(companySlug,{ job: jobId },session(accessToken),);}export async function moveApplicant(accessToken: string,companySlug: string,applicationId: string,stageId: string,) {const options = session(accessToken);await board.me.companies.applicants.move(companySlug,applicationId,{ stageId },options,);// Applicant mutations return 204; re-read the pipeline for fresh state.}
Use the stage order returned by the pipeline. Do not maintain a second ordering model in local storage.
Expected result
An employer sees membership state before any private controls. Approved members can create held drafts, publish or check them out, and read a pipeline whose stages and applicants come from one server response.
Verify the result
- A pending or rejected membership cannot edit a company or job.
- Claim confirmation produces the membership state shown in the portal.
- A created job appears as a draft before publication or checkout.
- Applicant writes re-read the pipeline after the 204 response.
- One employer cannot access another company’s private records.
Errors and edge cases
- Claiming can approve immediately on a domain match or enter a pending state.
- Work-email links expire after 24 hours.
- Employer job updates are merge patches; omitted fields remain unchanged.
- Remote timezone values auto-derive on create when omitted, but do not re-derive on update.
- Custom stages are capped and protected stages cannot be removed or arbitrarily reordered.
Production cautions
- Treat applicant data, notes, and signed resume URLs as private.
- Keep every employer response out of public caches and logs.
- Use the API’s authorization decision even if the UI already hid a control.
- Re-read state after void-returning mutations rather than simulating server changes.
Next, add an anonymous purchase path with Job posting and checkout.