All projects
CupidFaces Live

JobHunt

AI-powered job application platform aggregating 8 job sources with Gmail OAuth sending

8
Job sources
6 (Gemini 2.5 Flash Lite)
AI features
Formal / Casual / Creative
Cover letter styles
Zero (all free tiers)
Infrastructure cost
AI/LLMSaaSNext.jsFull-stackWebAuthn

Overview

JobHunt is a job application platform I designed and built to consolidate the entire job search workflow into one place. It pulls listings from 8 job boards simultaneously, deduplicates them, and lets you filter by job type (remote/hybrid/onsite), how recently posted, and keywords. You manage applications through 6 stages: Draft, Applied, Interview, Offer, Rejected, Withdrawn.

The AI layer is powered by Gemini 2.5 Flash Lite. From a job listing, you can: tailor your CV to the role, generate a cover letter in three styles (formal, casual, or creative), and generate 10 interview Q&As across technical, behavioural, company, and situational categories with hints for each. You can also analyze your CV to get a score and improvement suggestions, analyze your LinkedIn profile to get a rewritten headline and About section, and rebuild CV text that came out garbled from a badly structured PDF.

Every piece of AI output is editable before it goes anywhere. That was a deliberate decision because AI writing is a first draft, not a finished product. When you are ready to apply, you connect your Gmail via OAuth2 and send the email directly from your own address with the CV as an attachment. The platform tracks the Gmail message ID for reconciliation.

Auth uses WebAuthn/Passkeys for biometric login (Face ID, fingerprint) alongside email/password. The WebAuthn challenge is stored in the database rather than in-memory, which makes it work correctly on Vercel serverless where in-memory state does not persist between requests. CV management supports multiple CVs per user with custom labels, an active CV selector, PDF and .docx upload with a custom parser, and AI-powered CV rebuilding for garbled extractions.

My Role

  • Designed and built the entire product as sole developer and TPM: architecture, data model, API integrations, AI prompting, and frontend
  • Built integrations with 8 job sources (Remotive, RemoteOK, Adzuna, Jobicy, Himalayas, Arbeitnow, The Muse, Working Nomads) with a normalisation layer that maps every source to a consistent internal schema
  • Designed the Gemini AI layer: 6 separate AI features, each with tailored prompts, graceful degradation when the model returns unusable output, and rate limiting per user per feature
  • Implemented WebAuthn/Passkeys biometric auth with a serverless-safe challenge storage approach using the database rather than in-memory session
  • Built a custom PDF parser using Node.js zlib FlateDecode decompression instead of pdfjs-dist, because pdfjs-dist requires browser globals unavailable on Vercel serverless edge functions
  • Designed the Gmail OAuth flow: connect once, send applications directly from the user's own email address, track sent message IDs
  • Shipped the 4-step onboarding flow: profile, job preferences, LinkedIn capture, CV upload

Architecture

Next.js 16 App Router with React 19 and TypeScript. Supabase handles the PostgreSQL database with Row Level Security enforcing user data isolation at the database level. All user data is scoped by user_id and RLS policies mean a query that accidentally omits the user filter returns no data rather than someone else's data.

The job aggregation runs via API route on demand and via Vercel cron in the background. Each source is fetched in parallel and results are upserted by (user_id, source, external_id) unique constraint, so re-fetching never creates duplicates. The normalisation layer maps each source's response shape to a consistent internal Job type.

The AI features use Gemini 2.5 Flash Lite specifically because it is on the free tier. Each feature (tailor CV, cover letter, interview prep, CV analysis, LinkedIn analysis, CV rebuild) is a separate function in src/lib/ai/ with its own prompt and output validation. Rate limiting uses Vercel KV with an in-memory fallback for local development.

The PDF parser reads the raw PDF binary and extracts text streams by decompressing FlateDecode-encoded content with Node.js zlib. This works on Vercel serverless without the browser-global dependencies that pdfjs-dist requires. The .docx parser uses Mammoth which has no such constraint.

Gmail OAuth stores the access token and refresh token in the gmail_connections table. Before sending an email, the route checks token expiry and refreshes automatically if needed. The sent email's Gmail message ID is stored on the application record for tracking.

Key Decisions

AI outputs are first drafts, always editable
Every piece of AI-generated content on the platform can be edited before it goes anywhere. The cover letter, the tailored CV, the interview questions. This was a deliberate decision: AI writing needs a human pass to sound right for the specific person applying. Locking the output would have made the product feel risky to use and would have produced worse applications. The AI writes a starting point, not a finished product.
Storing WebAuthn challenges in the database for serverless compatibility
The standard WebAuthn implementation stores the challenge in the server session. On Vercel serverless, a new function instance handles each request, so there is no shared in-memory state between the challenge generation request and the verification request. Storing challenges in a database table with expiry timestamps solved this without any other changes to the WebAuthn flow.
Custom PDF parser instead of pdfjs-dist
pdfjs-dist is the standard PDF parsing library in the JavaScript ecosystem. It requires browser globals like window and document that do not exist in a Node.js serverless environment. Rather than adding a compatibility shim or a separate service, I built a minimal parser using Node.js zlib to decompress FlateDecode streams and extract text content. It handles the majority of real-world PDFs correctly and runs natively on Vercel without any workarounds.
Sending applications from the user's own Gmail account
Sending from a platform address (no-reply@jobhunt.app) means the hiring manager's first impression is a platform they may not recognise, and replies go somewhere the applicant might not check. Sending from the user's own Gmail address means the email looks like a normal job application, replies go to the right inbox, and the applicant controls the thread. That is a meaningfully better outcome for the user.
Zero monthly infrastructure cost by design
The platform runs entirely on free tiers: Supabase free tier, Gemini API free tier, Vercel free tier, and job sources that do not require paid API keys except Adzuna which has a generous free quota. This was a deliberate constraint. A job hunting tool is used most intensively during periods of unemployment, which is precisely when paying a monthly subscription is hardest. Keeping infrastructure cost at zero means the product can remain free to users indefinitely.

Challenges

Eight job sources, eight different data shapes
Every job source returns data differently. Salary ranges exist in some sources and not others. Company logos, apply URLs, job types, and location fields are all formatted differently per source. The normalisation layer maps every source response to a single internal Job type. Adding a new source means writing a new mapper, not changing the application code. Deduplication is handled at the database level by a unique constraint on (user_id, source, external_id).
PDF text extraction is unreliable across different PDF generators
PDFs generated by word processors, design tools, and CV builders all encode text differently. Some use standard FlateDecode compression, some use encoding schemes that the custom parser cannot handle cleanly. The CV rebuilding feature exists precisely for this case: when the extracted text comes out garbled, the AI uses the noisy text as input and reconstructs a clean, properly formatted CV. It is not a perfect solution but it handles the common cases.
Rate limiting AI features without killing the experience
Each Gemini API call has a cost and a rate limit. Without per-user limits, a single heavy user could exhaust the free quota for everyone. Per-user per-feature rate limits (tracked in Vercel KV) handle this without requiring users to manage tokens or credits. The limits are generous enough that normal use never hits them, and the error message when they do is friendly rather than a generic 429.

Stack & Integrations

Next.js 16React 19 + TypeScriptSupabase + RLSGemini 2.5 Flash LiteGmail OAuth2WebAuthn / PasskeysTailwind CSS 4Radix UIZustandMammoth (.docx)Custom PDF Parser (zlib)Vercel + Cron