Skip to main content

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-gated public.hire_candidate() RPC (moved out of app so 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_candidatepublic.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, dep unpdf) — PDF→text, deterministic identifier extraction (email/phone/PRC never reach a model), redact-before-AI, then extractStructured for non-identifier fields, gated by fail-closed RESUME_AI_ENABLED. Surfaced as a manual PDF-upload prefill on Add Candidate (reviews, never auto-creates). Keep RESUME_AI_ENABLED=false until a signed zero-retention/DPA arrangement is in place (design §10); deterministic parsing runs regardless.
  • Deferred: the Gmail /api/cron/applications auto-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/0055 but not the facility-pipeline migrations (which live only on feat/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 timestamp version, so the numbers are cosmetic). src/db/types.ts was regenerated from live.
  • Ops: set RECRUITING_ENABLED=true to launch; wire the two new pg_cron jobs (go-live runbook §4); set RESEND_API_KEY/EMAIL_FROM for real cadence-email delivery (drafts record-only without them).