Skip to content

Hackbits/HireIQ

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

59 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

HireIQ — AI-Powered Resume Screening

Screen hundreds of resumes in seconds using AI. HireIQ scores candidates, identifies skill gaps, generates interview questions, and tracks candidate pipelines — so you can focus on interviewing the best talent.

Built with Next.js 16, Firebase, Google Gemini AI, and Tailwind CSS v4.


Features

  • Instant AI Scoring — Upload a job description and candidate resumes. HireIQ returns a 0–100 match score with breakdown across technical skills, experience, education, and role match.
  • Skill Gap Analysis — See exactly which required skills a candidate has and which are missing.
  • AI Interview Questions — Auto-generated technical, behavioral, and role-specific questions with difficulty levels.
  • Batch Uploads — Upload multiple PDF resumes at once for parallel screening.
  • Candidate Pipeline — Track candidates through applied → screened → shortlisted → interview → hired/rejected.
  • Recommendation Filters — Sort candidates by Strong Fit, Possible Fit, or Not a Fit.
  • Privacy-First — PII (names, emails, phones, addresses, age/gender) is removed client-side before AI processing. Configurable auto-delete of old resumes.
  • Analytics Dashboard — Score distribution, skills frequency, and monthly screening trends.
  • CSV Export — Download candidate results as CSV with all columns including status and score breakdown.
  • Plan-based Quotas — Free tier (20 screens/month) and Pro tier (unlimited, ₹999/month) via Razorpay.
  • Dark/Light Mode — Class-based theming via next-themes (light default).

Architecture

┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│   Browser    │    │  Vercel CDN  │    │  UploadThing │
│  (Next.js)   │◄──►│ (Edge Cache) │    │  (File Host) │
└──────┬───────┘    └──────────────┘    └──────▲───────┘
       │                                       │
       │ Firebase Auth                          │ Upload files
       ▼                                       │
┌───────────────────────────────────────────────────┐
│              Next.js App Router                    │
│  ┌──────────┐  ┌──────────┐  ┌─────────────────┐  │
│  │ Dashboard │  │Analytics │  │  API Routes     │  │
│  │ Pages     │  │Page      │  │  /screen        │  │
│  │           │  │          │  │  /candidates/*  │  │
│  │Privacy    │  │Billing   │  │  /cron/cleanup  │  │
│  │Page       │  │Page      │  │  /razorpay/*    │  │
│  └──────────┘  └──────────┘  │  /uploadthing/*  │  │
│                               └─────────┬───────┘  │
└───────────────────────────────────────────┼─────────┘
       │            │            │          │
       ▼            ▼            ▼          ▼
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Firebase │ │  Google  │ │ Upstash  │ │ Razorpay │
│ Firestore│ │ Gemini AI│ │  Redis   │ │ Payment  │
│ (DB)     │ │ (Screening)│ │(RateLimit)│ │ Gateway  │
└──────────┘ └──────────┘ └──────────┘ └──────────┘

Data Flow (Resume Screening)

Upload PDF → UploadThing (hosts file)
    ↓
Extract text via pdf.js (client-side)
    ↓
Anonymize PII (client-side, before sending to AI)
    ↓
POST /api/screen → verifyAuth (Firebase Admin)
    ↓
Rate limit check (Upstash Redis / in-memory fallback)
    ↓
Monthly quota check (Firestore usage doc)
    ↓
Google Gemini AI → structured JSON response
    ↓
Save candidate doc + screening_results subcollection + audit log
    ↓
Increment usage counters (Firestore transaction)
    ↓
Return result to client

Tech Stack

Layer Technology
Framework Next.js 16 (App Router) + React 19 + TypeScript
Styling Tailwind CSS v4 (@theme inline)
Auth Firebase Authentication (email/password + Google)
Database Cloud Firestore
AI Google Gemini API (gemini-2.0-flash default)
Rate Limiting Upstash Redis (in-memory fallback)
Payments Razorpay (₹999/month Pro)
File Upload UploadThing (PDF, max 8MB)
PDF Parsing pdf.js (local worker, pinned version)
Testing Vitest (unit) + Playwright (E2E)
Fonts Plus Jakarta Sans, JetBrains Mono
Icons Lucide React
Deployment Vercel

Firestore Schema

Collections

users/{userId}

Field Type Description
email string User's email address
name string? Display name
role string "owner" | "admin" | "member"
companyId string? References companies/{companyId}
plan string "free" | "pro"
screensUsed number Lifetime screens counter
screensLimit number Max screens allowed
createdAt timestamp Account creation time

companies/{companyId}

Field Type Description
name string Company name
ownerId string References users/{userId}
createdAt timestamp Company creation time

jobs/{jobId}

Field Type Description
title string Job title
description string Full job description text
createdBy string References users/{userId}
companyId string References companies/{companyId}
createdAt timestamp Job creation time

candidates/{candidateId}

Field Type Description
jobId string References jobs/{jobId}
name string Candidate name (from resume)
resumeUrl string? UploadThing URL
score number (0-100) Overall match score
scoreBreakdown map { technicalSkills, experience, education, roleMatch }
matchedSkills string[] Skills matching the job
missingSkills string[] Required skills the candidate lacks
summary string 2-sentence AI-generated summary
recommendation string "strong_fit" | "possible_fit" | "not_fit"
interviewQuestions array [{ question, type, difficulty }]
candidateStatus string "applied" | "screened" | "shortlisted" | "interview" | "rejected" | "hired"
createdBy string References users/{userId}
companyId string References companies/{companyId}
processedAt timestamp Screening completion time

candidates/{candidateId}/screening_results/{resultId}

Field Type Description
candidateId string References candidates/{candidateId}
score number (0-100) Overall match score
scoreBreakdown map { technicalSkills, experience, education, roleMatch }
matchedSkills string[] Skills matching the job
missingSkills string[] Required skills the candidate lacks
summary string 2-sentence AI-generated summary
recommendation string "strong_fit" | "possible_fit" | "not_fit"
interviewQuestions array [{ question, type, difficulty }]
createdAt timestamp Result creation time

subscriptions/{subscriptionId}

Field Type Description
userId string References users/{userId}
razorpaySubscriptionId string Razorpay subscription ID
razorpayPaymentId string? Razorpay payment ID
status string "pending" | "success" | "failed" | "expired"
plan string "free" | "pro"
expiryDate timestamp Subscription expiry
createdAt timestamp Subscription creation time

audit_logs/{logId}

Field Type Description
userId string User who performed the action
action string Action identifier (e.g. "screen_resume", "update_candidate_status", "cron_cleanup")
details string? Human-readable description
timestamp timestamp When the action occurred

usage/{userId}_{month}

Field Type Description
userId string References users/{userId}
month string "YYYY-MM" format
screeningsUsed number Screens used this month
updatedAt timestamp Last update time

privacy_settings/{uid}

Field Type Description
autoDelete boolean Auto-delete old resumes
retentionDays number Days to retain (default: 90)

API Documentation

All API routes require authentication via Authorization: Bearer <Firebase ID Token> header unless noted.

POST /api/screen

Screen a resume against a job description.

Request Body:

{
  "resumeText": "string (extracted PDF text)",
  "jobDescription": "string",
  "jobId": "string",
  "candidateName": "string (optional)",
  "resumeUrl": "string (optional)"
}

Response 200:

{
  "success": true,
  "result": {
    "score": 85,
    "matchedSkills": ["JavaScript", "React"],
    "missingSkills": ["TypeScript"],
    "summary": "2-sentence summary",
    "recommendation": "strong_fit",
    "scoreBreakdown": {
      "technicalSkills": 32,
      "experience": 25,
      "education": 14,
      "roleMatch": 14
    },
    "interviewQuestions": [
      { "question": "...", "type": "technical", "difficulty": "intermediate" }
    ],
    "removedPII": ["email", "phone"]
  }
}

Errors: 400 (missing fields), 401 (auth), 404 (user not found), 429 (rate limited), 403 (quota exceeded), 500 (AI/DB error)

Rate Limits: 60 requests/minute per user. Monthly quota: 20 (free), unlimited (pro).


POST /api/candidates/status

Update a candidate's pipeline status.

Request Body:

{
  "candidateId": "string",
  "status": "applied | screened | shortlisted | interview | rejected | hired"
}

Response 200: { "success": true }

Errors: 400 (missing/invalid status), 401 (auth), 403 (not owner), 404 (candidate not found)


POST /api/razorpay/create-subscription

Create a Razorpay Pro subscription (₹999/month).

Request Body:

{
  "email": "string (optional, defaults to auth email)"
}

Response 200: { "url": "razorpay checkout short_url" }

Errors: 400 (email required), 401 (auth), 500 (Razorpay config/API error)


POST /api/razorpay/webhook

Razorpay webhook handler for subscription lifecycle events.

Auth: Validates x-razorpay-signature using RAZORPAY_WEBHOOK_SECRET.

Events handled: payment.captured, subscription.authenticated, subscription.activated, subscription.charged, subscription.completed, subscription.pending, subscription.halted, subscription.paused


POST /api/uploadthing

UploadThing file upload route. See UploadThing docs.

Constraints: PDF only, max 8MB.

Auth: Bearer token verified in middleware before upload starts.


GET /api/cron/cleanup

Daily cron job to delete candidates older than the retention period.

Auth: Authorization: Bearer <CRON_SECRET> (custom secret, not Firebase)

Response: { "deleted": 5, "retentionDays": 90, "message": "..." }

Configure in vercel.json:

{
  "crons": [
    {
      "path": "/api/cron/cleanup",
      "schedule": "0 0 * * *"
    }
  ]
}

Environment Variables

Variable Required Default Description
NEXT_PUBLIC_FIREBASE_API_KEY Yes Firebase Web API key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN Yes Firebase Auth domain
NEXT_PUBLIC_FIREBASE_PROJECT_ID Yes Firebase project ID
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET Yes Firebase storage bucket
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID Yes Firebase sender ID
NEXT_PUBLIC_FIREBASE_APP_ID Yes Firebase app ID
FIREBASE_CLIENT_EMAIL Yes Firebase Admin service account email
FIREBASE_PRIVATE_KEY Yes Firebase Admin private key
FIREBASE_PROJECT_ID Yes Firebase Admin project ID
GEMINI_API_KEY Yes Google Gemini API key
GEMINI_MODEL No gemini-2.0-flash Gemini model name
UPLOADTHING_TOKEN Yes UploadThing API token
RAZORPAY_KEY_ID No Razorpay API key ID
RAZORPAY_KEY_SECRET No Razorpay API secret
RAZORPAY_WEBHOOK_SECRET No Razorpay webhook signature secret
NEXT_PUBLIC_APP_URL No http://localhost:3000 Public app URL for redirects
UPSTASH_REDIS_REST_URL No Upstash Redis REST URL (rate limiting)
UPSTASH_REDIS_REST_TOKEN No Upstash Redis REST token
CRON_SECRET No Secret for cron endpoint auth
RESUME_RETENTION_DAYS No 90 Days before auto-deleting candidates

Note: Upstash and Razorpay variables are optional. When absent, rate limiting falls back to in-memory, and billing features are disabled.


Getting Started

Prerequisites

  • Node.js 20+
  • npm
  • Firebase project (with Auth + Firestore enabled)
  • Google Gemini API key
  • UploadThing account
  • Razorpay account (optional, for payment processing)

Installation

git clone <https://github.com/Hackbits/HireIQ.git>
cd resume-screener
npm install

Environment Setup

Copy .env.example to .env.local and fill in your values:

cp .env.example .env.local

Then start the development server:

npm run dev

Open http://localhost:3000.

Production Build

npm run build
npm start

Deployment (Vercel)

  1. Push to a GitHub repository.
  2. Go to vercel.com and import the repo.
  3. Set the Framework to Next.js.
  4. Add all environment variables from the table above in the Vercel dashboard.
  5. Deploy.

Critical: Firebase Private Key

The FIREBASE_PRIVATE_KEY must use actual newlines. In Vercel, paste the key as-is (with \n characters) — Vercel handles the escaping. Do not wrap it in quotes.

Cron Job Setup

In your Vercel project:

  1. Go to SettingsCron Jobs.
  2. Add a cron job with path /api/cron/cleanup, schedule 0 0 * * * (daily midnight).
  3. Set the CRON_SECRET environment variable.
  4. The cron sends Authorization: Bearer <CRON_SECRET> automatically.

Razorpay Webhook

  1. In the Razorpay dashboard, go to SettingsWebhooks.
  2. Add a webhook URL: https://your-domain.vercel.app/api/razorpay/webhook
  3. Subscribe to events: payment.captured, subscription.*
  4. Set the webhook secret and add it as RAZORPAY_WEBHOOK_SECRET in Vercel.

Firestore Security Rules

Deploy the provided firestore.rules:

firebase deploy --only firestore:rules

Security

Authentication Flow

  • Firebase ID tokens are verified server-side via Firebase Admin SDK in every API route.
  • The user's uid is always derived from the verified token, never trusted from the request body.
  • UploadThing middleware verifies Firebase ID tokens before accepting uploads.

Rate Limiting

  • Production: Upstash Redis sliding window (60 requests/minute/user).
  • Fallback: In-memory rate limiter when Upstash env vars are absent.
  • Returns Retry-After header and X-RateLimit-Remaining on 429 responses.

PII Handling

  • PII is removed client-side before resume text is sent to the Gemini API.
  • Removed fields: emails, phone numbers, street addresses, age/gender indicators, candidate names.
  • The API response includes a removedPII array listing which fields were stripped.
  • Old resumes are auto-deleted via daily cron (configurable retention, default 90 days).

Firestore Security

  • Company-scoped access: users can only read candidates within their company.
  • Writes are limited to document owners or company members.
  • Audit logs are append-only (no updates/deletes allowed in rules).

Project Structure

resume-screener/
├── app/
│   ├── (auth)/
│   │   ├── login/page.tsx
│   │   └── signup/page.tsx          # Creates company doc on signup
│   ├── api/
│   │   ├── candidates/status/route.ts   # Candidate pipeline status
│   │   ├── cron/cleanup/route.ts        # Privacy auto-delete cron
│   │   ├── razorpay/
│   │   │   ├── create-subscription/route.ts
│   │   │   └── webhook/route.ts
│   │   ├── screen/route.ts              # AI screening (auth, rate-limit, quota)
│   │   └── uploadthing/{core,route}.ts  # File upload with auth
│   ├── billing/page.tsx
│   ├── dashboard/
│   │   ├── [jobId]/page.tsx             # Candidate results (search, filter, CSV)
│   │   ├── layout.tsx
│   │   └── page.tsx                     # Job listing dashboard
│   ├── privacy/page.tsx                 # Privacy settings (retention, export)
│   ├── profile/page.tsx
│   ├── globals.css                      # Design tokens & utilities
│   ├── layout.tsx
│   └── page.tsx                         # Landing page
├── components/
│   ├── ui/                              # shadcn-style primitives
│   │   ├── badge.tsx, button.tsx, card.tsx, input.tsx, skeleton.tsx
│   │   └── __tests__/
│   ├── CandidateCard.tsx                # Score ring, status, interview prep
│   ├── CompareView.tsx                  # Side-by-side comparison
│   ├── Navbar.tsx
│   ├── OnboardingTour.tsx
│   ├── PlanGate.tsx
│   ├── Sidebar.tsx                      # Dashboard, Analytics, Privacy, Billing
│   ├── UpgradeModal.tsx
│   └── UploadForm.tsx                   # PDF validation, progress bar
├── lib/
│   ├── __tests__/
│   │   ├── anonymizer.test.ts           # 20 tests: PII removal
│   │   ├── auth-middleware.test.ts      # 9 tests: token verification
│   │   ├── gemini.test.ts               # 12 tests: AI response parsing
│   │   ├── rate-limit.test.ts           # 6 tests: rate limiter
│   │   ├── utils.test.ts                # 6 tests: utilities
│   │   └── setup.ts                     # Vitest global setup
│   ├── anonymizer.ts                    # PII removal (email, phone, address, age/gender, name)
│   ├── auth-context.tsx                 # Auth provider with role, companyId
│   ├── auth-middleware.ts               # verifyAuth, verifyOptionalAuth, handleAuthError
│   ├── firebase.ts                      # Firebase client config
│   ├── firebase-admin.ts               # Firebase Admin SDK singleton
│   ├── firestore-schema.ts              # Full Firestore TypeScript interfaces
│   ├── gemini.ts                        # screenResume + generateInterviewQuestions
│   ├── rate-limit.ts                    # Deprecated in-memory limiter
│   ├── rate-limit-upstash.ts            # Upstash Redis + in-memory fallback
│   ├── types.ts                         # Shared user-facing types
│   ├── use-toast.tsx
│   └── utils.ts
├── e2e/
│   └── happy-path.spec.ts               # Playwright smoke tests
├── public/pdf.worker.min.mjs            # Local PDF.js worker
├── firestore.rules                      # Security rules
├── vitest.config.ts                     # Vitest configuration
├── playwright.config.ts                 # Playwright configuration
└── .env.example

Scripts

Script Description
dev Start development server
build Production build
start Start production server
lint Run ESLint
test Run all unit tests (vitest run)
test:watch Run tests in watch mode (vitest)
test:e2e Run Playwright E2E tests

Current test coverage: 94 unit tests across 13 files + 7 Playwright E2E tests (see lib/__tests__/ and e2e/).


Design System

The visual language is light, precise, and trustworthy — inspired by Linear, Vercel, and Razorpay.

  • Light canvas (#F8FAFC) with generous whitespace
  • Blue-600 (#2563EB) as the single primary action color
  • Flat surfaces with hairline borders — no gradients, glows, or decorative noise
  • Typography-driven hierarchy with Plus Jakarta Sans
  • See DESIGN.md for the full design system documentation.

About

AI-powered resume screener. Upload a JD + PDFs, get instant match scores, skill gap analysis, and candidate summaries.

Resources

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors