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

> Source: <https://dev.to/jobnex/how-i-built-an-ai-powered-reverse-hiring-platform-with-nextjs-supabase-and-openai-26hp>
> Published: 2026-07-09 10:20:46+00:00

[# How I Built an AI-Powered Reverse Hiring Platform with Next.js, Supabase, and OpenAI](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiikm8lf9ycuz7nb85fll.png)

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:

``` js
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:

``` js
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:

``` js
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:

``` bash
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](https://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*
