One Gemini call per candidate, in parallel
Each resume gets its own call to gemini-2.5-flash with a constrained responseSchema — the model is forced to return shape-correct JSON so the parser is trivial.
Background work via next/server after()
POST /api/screenings inserts the rows and schedules the analysis via Next.js after(). The request returns the screening ID in ~80ms; the model runs to completion in the same lambda invocation after the response is sent.
Calibrated rubric anchors the scores
System prompt anchors scoring bands explicitly — 90+ is exceptional and rare. Without anchoring, every candidate trends toward 78-85. The rubric is what makes the rankings actionable.
Honesty over flattery
The prompt forbids generic praise. 'Strong communicator' isn't a strength; 'led the Kafka migration at 40k tx/sec' is. Gaps are stated plainly. If a required skill is absent, the model says so.
RLS on, writes via service role
Public-read on jobs / screenings / candidates — anyone with a URL can view, no one with the anon key can write. Every mutation goes through a server route with the service role.
The exact prompt, rubric, and JSON schema the model sees for every candidate. Nothing hidden.
90+ exceptional, top 5% — clear evidence of senior-level impact in directly relevant work 75–89 strong fit, top quartile of qualified applicants 60–74 serviceable, worth a phone screen with caveats 40–59 weak fit, multiple meaningful gaps relative to requirements <40 pass, material misalignment with the role
Stack
Pieces in motion
Framework
Next.js 16 · App Router
Model
Gemini 2.5 Flash · free tier
Database
Supabase Postgres · RLS on
Style
Tailwind v4 · OKLCH tokens