{"slug": "how-i-built-an-ai-powered-reverse-hiring-platform-with-next-js-supabase-and", "title": "How I Built an AI-Powered Reverse Hiring Platform with Next.js, Supabase, and OpenAI", "summary": "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.", "body_md": "[# 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)\n\nThe hiring process is fundamentally broken.\n\nCandidates 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.\n\nSo I asked: what if we flipped the entire model?\n\nWhat 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?\n\nThat's what I built. Here's how.\n\n| Layer | Technology |\n|---|---|\n| Framework | Next.js 15 (App Router) |\n| Database + Auth | Supabase (PostgreSQL + RLS + Auth) |\n| AI | OpenAI GPT-4o-mini + text-embedding-3-small |\n| Hosting | Vercel (serverless) |\n| Resend | |\n| PDF Parsing | unpdf (WASM-based, works serverless) |\n| Styling | Tailwind CSS + Framer Motion |\n| State | Zustand |\n\nThis is the most interesting part technically. Here's how match scoring works:\n\n``` js\nexport async function parseResume(text: string) {\n  const response = await openai.chat.completions.create({\n    model: 'gpt-4o-mini',\n    temperature: 0.2,\n    response_format: { type: 'json_object' },\n    messages: [\n      {\n        role: 'system',\n        content: `You extract structured profile data from resumes. \n        Return JSON with: headline (actual job title from resume, max 80 chars), \n        bio (2-3 sentence summary preserving substance, anonymous), \n        skills (array, max 15), experience_years (integer), \n        industry, location. \n        Important: Do NOT genericize. A \"Senior Engineering Manager\" \n        should NOT become \"Software Engineer\".`\n      },\n      { role: 'user', content: text }\n    ],\n  })\n  return JSON.parse(response.choices[0].message.content || '{}')\n}\n```\n\nKey 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.\n\n```\nexport async function generateEmbedding(text: string): Promise<number[]> {\n  const response = await openai.embeddings.create({\n    model: 'text-embedding-3-small',\n    input: text,\n  })\n  return response.data[0].embedding\n}\n\nexport function cosineSimilarity(a: number[], b: number[]): number {\n  let dot = 0, magA = 0, magB = 0\n  for (let i = 0; i < a.length; i++) {\n    dot += a[i] * b[i]\n    magA += a[i] * a[i]\n    magB += b[i] * b[i]\n  }\n  return dot / (Math.sqrt(magA) * Math.sqrt(magB))\n}\n```\n\nThe 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).\n\nAfter getting the similarity score, GPT generates 3-5 short reasons why they match:\n\n``` js\nconst response = await openai.chat.completions.create({\n  model: 'gpt-4o-mini',\n  messages: [\n    {\n      role: 'system',\n      content: `Given a candidate and company, return JSON with \"reasons\": \n      array of 3-5 short reasons (max 8 words each) why they match.`\n    },\n    {\n      role: 'user', \n      content: `Candidate: ${candidateProfile}\\nCompany: ${companyNeeds}\\nScore: ${score}%`\n    }\n  ],\n})\n```\n\nThis gives employers actionable context: \"Skills aligned\", \"Salary fit\", \"Remote compatible\" — not just a number.\n\nThis was my biggest headache. `pdf-parse`\n\n(the standard Node.js PDF library) **does not work on Vercel serverless**. It depends on a test file that Vercel strips during deployment.\n\nWhat happens: the PDF buffer arrives fine, but `pdf-parse`\n\nsilently fails. The fallback reads raw binary as text, sends garbled data to GPT, and GPT hallucinates a completely made-up profile.\n\n**The fix:** I switched to `unpdf`\n\n— a WASM-based PDF text extraction library specifically designed for serverless environments:\n\n``` js\nimport { extractText } from 'unpdf'\n\nconst buffer = Buffer.from(await file.arrayBuffer())\nconst { text } = await extractText(new Uint8Array(buffer))\nconst extracted = Array.isArray(text) ? text.join('\\n') : text\n```\n\nWorks perfectly on Vercel. Lesson: always test your PDF parsing on the actual deployment target, not just locally.\n\nSupabase RLS is powerful but tricky with multi-role apps. My setup:\n\n`SECURITY DEFINER`\n\nfunction:\n\n``` bash\nCREATE OR REPLACE FUNCTION is_admin()\nRETURNS boolean AS $$\n  SELECT EXISTS (\n    SELECT 1 FROM profiles \n    WHERE user_id = auth.uid() AND role = 'admin'\n  );\n$$ LANGUAGE sql SECURITY DEFINER;\n\nCREATE POLICY \"Admins can read all profiles\" ON profiles\n  FOR SELECT USING (auth.uid() = user_id OR is_admin());\n```\n\nThe `SECURITY DEFINER`\n\nprevents infinite recursion — without it, the policy checks itself while checking itself.\n\nThe AI estimates salary based on candidate's location:\n\n```\ncontent: `You are a compensation analyst. Given a professional profile, \nestimate their market value based on their location. Return JSON with: \nestimated_salary (integer, annual in local currency), \ncurrency (3-letter code), demand_score (0-100), reasoning (one sentence). \nAdjust for country/city cost of living and local market rates. \nA senior engineer in San Francisco earns more than one in Helsinki.`\n```\n\nThe currency is then mapped on the frontend based on their country selection — so US candidates see \"$150,000\" and Finnish candidates see \"€130,000\".\n\nThe single most impactful feature for engagement: email notifications via Resend.\n\nEvery meaningful action triggers an email:\n\nWithout these, users sign up and forget. With them, they come back.\n\nIf you're a developer tired of the application grind: [jobnex.io](https://jobnex.io) — free during beta, 2 minutes to set up.\n\nIf you're hiring: browse the talent pool directly.\n\nHappy to answer questions about the technical implementation in the comments.\n\n*Tags: #nextjs #ai #startup #webdev #supabase*", "url": "https://wpnews.pro/news/how-i-built-an-ai-powered-reverse-hiring-platform-with-next-js-supabase-and", "canonical_source": "https://dev.to/jobnex/how-i-built-an-ai-powered-reverse-hiring-platform-with-nextjs-supabase-and-openai-26hp", "published_at": "2026-07-09 10:20:46+00:00", "updated_at": "2026-07-09 10:41:30.019784+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-products", "developer-tools", "natural-language-processing"], "entities": ["Next.js", "Supabase", "OpenAI", "Vercel", "GPT-4o-mini", "text-embedding-3-small", "Tailwind CSS", "Framer Motion"], "alternates": {"html": "https://wpnews.pro/news/how-i-built-an-ai-powered-reverse-hiring-platform-with-next-js-supabase-and", "markdown": "https://wpnews.pro/news/how-i-built-an-ai-powered-reverse-hiring-platform-with-next-js-supabase-and.md", "text": "https://wpnews.pro/news/how-i-built-an-ai-powered-reverse-hiring-platform-with-next-js-supabase-and.txt", "jsonld": "https://wpnews.pro/news/how-i-built-an-ai-powered-reverse-hiring-platform-with-next-js-supabase-and.jsonld"}}