42. Recruitment pipeline — supply-side ATS
Date: 2026-06-17
Status
Accepted
Context
The Facility Pipeline (ADR-0040) covers the demand side (winning SNF clients). We lacked the supply side: a way to source, screen, score, nurture, and hire the Philippine-based, PRC-licensed nurses who staff those contracts as outsourced contractors. Recruiting was manual and memory-driven.
A v1 design was red-teamed (recorded in docs/recruitment-pipeline/), producing a hardened v2. A
second adversarial review — grounded against the live Supabase schema and the real src/ code,
not the design's self-description — raised 48 findings, 44 verified. It confirmed the v2 migration's
apply-safety but exposed real defects the paper review missed: a cleartext-PII audit leak, a
null-requisition dedup hole, a compliance-gate bypass, an app-schema hire RPC that PostgREST can't
call, a 'video' cadence channel that would fail the migration, and that the "reuse the follow_up_*
engine" claim had no runner to reuse (it was schema-only). Full record:
docs/recruitment-pipeline/ADVERSARIAL-REVIEW-AND-CHANGELOG.md.
Decision
Build the recruitment pipeline as a flag-gated (RECRUITING_ENABLED, fail-closed) feature that
mirrors the CRM/contractor conventions and reuses the contractor-onboarding tail.
- Schema (
0054+0055, applied to the dev DB): 11 tables on a person (recruitment_candidates) / application (candidate_applications) split; DB-enforced PRC + lawful-basis advance gate; a transactional, compliance-gatedpublic.hire_candidate()RPC (moved out ofappso PostgREST can call it); in-migration PII masking of candidate audit rows (audit_row_candidate) with a CI guardrail; partial unique indexes closing the dedup holes; retention seeds. - Pure libs (
src/lib/recruiting/): role-aware scoring with an explicit role→profile map, deterministic redact-before-AI, the candidate→201 hire mapper, the funnel constants — all unit-tested. - Backend (
src/server/{services,actions}/recruiting.ts): typed, company-scoped reads + actions (find-or-create dedup, score recompute, the gated hire via RPC). - UI (
/admin/recruiting, behind the flag): Pipeline Board, All Candidates, Add Candidate, and Candidate 360 (PRC + compliance gate cards, scored, with verify/consent/assess/move/reject/hire and draft-approval + erasure), built on house components so mobile card-stack, status-not-by-colour, 44px targets, and type-to-confirm destructive actions hold. - Automation: a net-new cadence runner (drafts emails, never auto-sends) + referral day-90 release
on
/api/cron/follow-ups; an RA 10173 retention purge on/api/cron/retention-purge; DSAR erasure.
The app.hire_candidate → public.hire_candidate move (0055) is because the app invokes RPCs over
PostgREST, which only exposes public; internal trigger/helper functions stay in app.
Consequences
- Positive: a working, CI-green, dark-launchable supply-side ATS that hands hires into the existing contractor onboarding atomically; the compliance posture (masked audit, enforced consent/PRC gates, retention purge, DSAR) is real, not paper.
- Résumé parser: built (
services/resumeParser.ts, depunpdf) — PDF→text, deterministic identifier extraction (email/phone/PRC never reach a model), redact-before-AI, thenextractStructuredfor non-identifier fields, gated by fail-closedRESUME_AI_ENABLED. Surfaced as a manual PDF-upload prefill on Add Candidate (reviews, never auto-creates). KeepRESUME_AI_ENABLED=falseuntil a signed zero-retention/DPA arrangement is in place (design §10); deterministic parsing runs regardless. - Deferred: the Gmail
/api/cron/applicationsauto-fetch cron that pulls résumés from the inbox into the parser +application_raw. Needs Gmail wiring; the parser it would call is done. - Process: the branch carries
0054/0055but not the facility-pipeline migrations (which live only onfeat/facility-pipeline, applied to the dev DB, unmerged). Merge order: facility-pipeline → main first, then recruitment; confirm the migration filename integers then (the runner orders by timestampversion, so the numbers are cosmetic).src/db/types.tswas regenerated from live. - Ops: set
RECRUITING_ENABLED=trueto launch; wire the two new pg_cron jobs (go-live runbook §4); setRESEND_API_KEY/EMAIL_FROMfor real cadence-email delivery (drafts record-only without them).