cd /news/artificial-intelligence/how-i-built-an-ai-powered-reverse-hi… · home topics artificial-intelligence article
[ARTICLE · art-52402] src=dev.to ↗ pub= topic=artificial-intelligence verified=true sentiment=↑ positive

How I Built an AI-Powered Reverse Hiring Platform with Next.js, Supabase, and OpenAI

A developer built an AI-powered reverse hiring platform using Next.js, Supabase, and OpenAI. The platform flips the traditional hiring model by requiring companies to find candidates, disclosing salary upfront, and keeping candidates anonymous until they engage. The system uses OpenAI's GPT-4o-mini for resume parsing and match scoring, with embeddings and cosine similarity to determine compatibility.

read4 min views6 publishedJul 9, 2026

# How I Built an AI-Powered Reverse Hiring Platform with Next.js, Supabase, and OpenAI

The hiring process is fundamentally broken.

Candidates send hundreds of applications into a void of automated rejections. Companies pay €25,000 per hire to recruiting agencies who copy-paste LinkedIn profiles into emails. Everyone works harder, nobody gets better results.

So I asked: what if we flipped the entire model?

What if companies had to find candidates — not the other way around? What if salary was disclosed before the first conversation? What if candidates stayed anonymous until they chose to engage?

That's what I built. Here's how.

Layer Technology
Framework Next.js 15 (App Router)
Database + Auth Supabase (PostgreSQL + RLS + Auth)
AI OpenAI GPT-4o-mini + text-embedding-3-small
Hosting Vercel (serverless)
Resend
PDF Parsing unpdf (WASM-based, works serverless)
Styling Tailwind CSS + Framer Motion
State Zustand

This is the most interesting part technically. Here's how match scoring works:

export async function parseResume(text: string) {
  const response = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    temperature: 0.2,
    response_format: { type: 'json_object' },
    messages: [
      {
        role: 'system',
        content: `You extract structured profile data from resumes. 
        Return JSON with: headline (actual job title from resume, max 80 chars), 
        bio (2-3 sentence summary preserving substance, anonymous), 
        skills (array, max 15), experience_years (integer), 
        industry, location. 
        Important: Do NOT genericize. A "Senior Engineering Manager" 
        should NOT become "Software Engineer".`
      },
      { role: 'user', content: text }
    ],
  })
  return JSON.parse(response.choices[0].message.content || '{}')
}

Key lesson: the prompt must explicitly say "do not genericize." Without that instruction, GPT-4o-mini tends to normalize every title to "Software Engineer" and writes generic bios.

export async function generateEmbedding(text: string): Promise<number[]> {
  const response = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: text,
  })
  return response.data[0].embedding
}

export function cosineSimilarity(a: number[], b: number[]): number {
  let dot = 0, magA = 0, magB = 0
  for (let i = 0; i < a.length; i++) {
    dot += a[i] * b[i]
    magA += a[i] * a[i]
    magB += b[i] * b[i]
  }
  return dot / (Math.sqrt(magA) * Math.sqrt(magB))
}

The candidate's profile text and the company's hiring needs are both converted to embeddings, then compared via cosine similarity. A 60%+ score is a "Good Match" in embedding space (it rarely goes above 90% even for near-identical texts).

After getting the similarity score, GPT generates 3-5 short reasons why they match:

const response = await openai.chat.completions.create({
  model: 'gpt-4o-mini',
  messages: [
    {
      role: 'system',
      content: `Given a candidate and company, return JSON with "reasons": 
      array of 3-5 short reasons (max 8 words each) why they match.`
    },
    {
      role: 'user', 
      content: `Candidate: ${candidateProfile}\nCompany: ${companyNeeds}\nScore: ${score}%`
    }
  ],
})

This gives employers actionable context: "Skills aligned", "Salary fit", "Remote compatible" — not just a number.

This was my biggest headache. pdf-parse

(the standard Node.js PDF library) does not work on Vercel serverless. It depends on a test file that Vercel strips during deployment.

What happens: the PDF buffer arrives fine, but pdf-parse

silently fails. The fallback reads raw binary as text, sends garbled data to GPT, and GPT hallucinates a completely made-up profile.

The fix: I switched to unpdf

— a WASM-based PDF text extraction library specifically designed for serverless environments:

import { extractText } from 'unpdf'

const buffer = Buffer.from(await file.arrayBuffer())
const { text } = await extractText(new Uint8Array(buffer))
const extracted = Array.isArray(text) ? text.join('\n') : text

Works perfectly on Vercel. Lesson: always test your PDF parsing on the actual deployment target, not just locally.

Supabase RLS is powerful but tricky with multi-role apps. My setup:

SECURITY DEFINER

function:

CREATE OR REPLACE FUNCTION is_admin()
RETURNS boolean AS $$
  SELECT EXISTS (
    SELECT 1 FROM profiles 
    WHERE user_id = auth.uid() AND role = 'admin'
  );
$$ LANGUAGE sql SECURITY DEFINER;

CREATE POLICY "Admins can read all profiles" ON profiles
  FOR SELECT USING (auth.uid() = user_id OR is_admin());

The SECURITY DEFINER

prevents infinite recursion — without it, the policy checks itself while checking itself.

The AI estimates salary based on candidate's location:

content: `You are a compensation analyst. Given a professional profile, 
estimate their market value based on their location. Return JSON with: 
estimated_salary (integer, annual in local currency), 
currency (3-letter code), demand_score (0-100), reasoning (one sentence). 
Adjust for country/city cost of living and local market rates. 
A senior engineer in San Francisco earns more than one in Helsinki.`

The currency is then mapped on the frontend based on their country selection — so US candidates see "$150,000" and Finnish candidates see "€130,000".

The single most impactful feature for engagement: email notifications via Resend.

Every meaningful action triggers an email:

Without these, users sign up and forget. With them, they come back.

If you're a developer tired of the application grind: jobnex.io — free during beta, 2 minutes to set up.

If you're hiring: browse the talent pool directly.

Happy to answer questions about the technical implementation in the comments.

Tags: #nextjs #ai #startup #webdev #supabase

── more in #artificial-intelligence 4 stories · sorted by recency
── more on @next.js 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/how-i-built-an-ai-po…] indexed:0 read:4min 2026-07-09 ·