How We Shipped a Production AI Recruitment Platform in 3 Weeks

How We Shipped a Production AI Recruitment Platform in 3 Weeks

December 19, 2025
Sune Pedersen

A technical case study from Pedersen Consulting

Published: December 2025


TL;DR

  • Timeline: 3 weeks from concept to production
  • Stack: Nuxt 4, Prisma, PostgreSQL, Claude 4.5 Sonnet (Anthropic)
  • Scale: 500+ CV evaluations, 23 languages, 100+ job categories
  • Key Innovation: AI-powered CV evaluation with GDPR-compliant data management

The Challenge

A recruitment agency approached us with a problem: their manual CV screening process was taking 30-45 minutes per candidate, and they were missing "hidden gem" candidates who didn't fit traditional criteria.

They needed a system that could:

  1. Evaluate CVs against job descriptions using AI - not just keyword matching
  2. Process multiple languages - French, German, Spanish, Arabic, etc.
  3. Maintain GDPR compliance - automatic data deletion, export, user control
  4. Scale from zero to production - handle real users immediately
  5. Ship fast - they needed it running in weeks, not months

Why Speed Mattered

The client was bleeding time and money on manual screening. Every week of development delay meant:

  • 40+ hours of manual CV review per recruiter
  • Lost candidates to competitors with faster processes
  • Missed revenue from unfilled positions

We had to ship something usable fast, then iterate based on real user feedback.


Technical Architecture Decisions

1. Framework: Nuxt 4 (Vue 3 + Server-Side)

Why Nuxt:

  • Full-stack in one codebase (no separate backend/frontend repos)
  • Server-side API routes for security-sensitive operations
  • Built-in SSR for SEO (critical for their public job pages)
  • Fast development with file-based routing

Alternative considered: Next.js - rejected because team had Vue expertise

// Example: API route with built-in auth and database access
export default defineEventHandler(async (event) => {
  const user = await serverSupabaseUser(event)
  const candidate = await prisma.candidate.findUnique({
    where: { id: params.id }
  })
  return { candidate }
})

2. Database: PostgreSQL + Prisma

Why Prisma:

  • Type-safe database queries (caught bugs at compile time)
  • Schema migrations handled automatically
  • Supports complex relations without writing SQL

Schema highlight - GDPR compliance built in:

model PublicEvaluation {
  id                      String   @id @default(cuid())
  createdAt               DateTime @default(now())
  expiresAt               DateTime // Auto-delete after 30 days

  // GDPR fields
  gdprConsent             Boolean
  gdprConsentAt           DateTime
  accessToken             String   // For data export/deletion
  accessTokenExpiresAt    DateTime

  // Evaluation data
  jobTitle                String
  aggregateScore          Float
  recommendation          String
  // ... other fields
}

3. AI: Claude 4.0 Sonnet (Anthropic)

Why Claude over GPT-4:

  • Better at structured output (JSON with specific schema)
  • 200K token context window (can fit entire CVs + job descriptions)
  • Stronger reasoning for nuanced evaluation
  • More consistent scoring across evaluations
  • Superior multilingual understanding (critical for our 23-language support)

Prompt engineering approach:

const prompt = `
You are evaluating a candidate's CV against a job description.

Score the candidate on 7 dimensions (1-10):
1. Qualification Match - Education, certifications, years of experience
2. Capability & Confidence - Technical skills, demonstrated expertise
3. Situational Stability - Job tenure, career trajectory
4. Reward Expectations - Salary alignment, benefits fit
5. Culture Fit - Work style, values alignment
6. Career Trajectory - Growth potential, ambition alignment
7. Compensation Expectations - Salary range compatibility

Return JSON with this exact structure:
{
  "scores": { "qualification": 8, ... },
  "summary": "...",
  "recommendation": "strong_yes|yes|maybe|no|strong_no",
  "confidence_level": "high|medium|low"
}
`

Result: 95%+ consistency in scoring, with detailed reasoning for every decision.

4. File Processing: PDF/DOCX → Text Extraction

Challenge: Users upload CVs in various formats (PDF, DOCX, scanned images)

Solution: Multi-stage extraction pipeline

async function extractTextFromCV(buffer: Buffer, mimeType: string) {
  if (mimeType === 'application/pdf') {
    // Try pdf-parse first (works for text-based PDFs)
    const text = await pdfParse(buffer)

    // Fallback: OCR for scanned PDFs (future enhancement)
    if (text.length < 100) {
      throw new Error('CV appears to be image-based or encrypted')
    }

    return text
  } else if (mimeType === 'application/docx') {
    // Use mammoth.js for DOCX extraction
    const result = await mammoth.extractRawText({ buffer })
    return result.value
  }
}

Learning: 12% of CVs failed initial extraction due to encryption or image-only PDFs. We added clear error messages guiding users to re-save files.


Key Features Built in Week 1

Free CV Checker (Public Tool)

Goal: Marketing funnel + product validation

Implementation:

  • No authentication required
  • Rate limited by IP (30 evaluations/day)
  • Automatic data deletion after 30 days
  • GDPR-compliant consent flow

Code snippet - Rate limiting:

function hashIP(ip: string): string {
  return crypto.createHash('sha256')
    .update(ip + 'salt_tanova_2024')
    .digest('hex')
}

async function checkRateLimit(ipHash: string) {
  const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000)

  const count = await prisma.publicEvaluation.count({
    where: {
      ipAddress: ipHash,
      createdAt: { gte: oneDayAgo }
    }
  })

  return count < 30
}

Result: 500+ evaluations in first 3 weeks, zero spam/abuse.

Agency Dashboard (Authenticated)

Features:

  • Upload multiple CVs
  • Bulk evaluation (process 10+ candidates at once)
  • Share evaluation links with clients
  • Export to CSV for further analysis

Technical challenge: Handling concurrent AI requests without rate limits

Solution: Queue system with concurrency control

const queue = new PQueue({ concurrency: 3 })

const results = await Promise.all(
  candidates.map(candidate =>
    queue.add(() => evaluateCandidate(candidate))
  )
)

Key Features Built in Week 2

Public Job Application Pages

Goal: Let recruiters post jobs publicly, candidates apply with instant AI screening

Implementation:

  1. Job posting with public slug:
model Job {
  id          Int     @id @default(autoincrement())
  title       String
  isPublic    Boolean @default(false)
  publicSlug  String? @unique // e.g., "senior-developer-startup-berlin"
  // ...
}
  1. Public application flow:
Candidate visits: /apply/senior-developer-startup-berlin
  ↓
Uploads CV (no account required)
  ↓
AI evaluates CV vs job requirements
  ↓
Results emailed with GDPR management link
  ↓
Recruiter sees application in dashboard
  1. Email automation with GDPR controls:

Every applicant receives:

  • Evaluation results
  • Downloadable data (JSON export)
  • One-click deletion link
  • Automatic expiration notice (30 days)

Multi-language Support

Challenge: Initial prompt was English-only, failed on French/German CVs

Solution: Language-agnostic prompting

const prompt = `
Evaluate this CV against the job description.
Both may be in different languages (English, French, German, Spanish, etc.).

Provide your analysis in the SAME LANGUAGE as the job description.
If job description is in French, respond in French.
If job description is in English, respond in English.

[... rest of prompt ...]
`

Result: 89.9/100 score for French electrician CV, 78/100 for German product manager - AI understood context across languages.


Key Features Built in Week 3

Hidden Gem Detection

Insight from data: 15% of high-scoring candidates would be auto-rejected by traditional ATS

Implementation: Secondary AI analysis for "Maybe" candidates

if (recommendation === 'maybe' && aggregateScore > 65) {
  const hiddenGemAnalysis = await analyzeHiddenGemPotential({
    cvText,
    jobDescription,
    scores
  })

  // Flags: "rapid learner", "unconventional background",
  //        "unique skill combination", "high growth trajectory"
}

Real example:

  • Junior dev with 1.5 years → Score: 35/100 for CTO role
  • Flagged: "Exceptional product thinking, rapid technical growth"
  • Recommendation: Not CTO, but excellent for Senior IC role

Rejection Email Templates with AI Personalization

Problem: Recruiters spend 10-15 minutes writing rejection emails

Solution: Templates + AI personalization

const template = `
Dear {{candidateName}},

Thank you for applying to {{jobTitle}} at {{companyName}}.

After careful review, we've decided to move forward with other candidates
whose experience more closely matches our current needs.

{{ai_personalized_feedback}}

We wish you the best in your job search.
`

// AI fills in personalized feedback based on evaluation
const personalizedFeedback = await generateRejectionFeedback({
  candidateStrengths,
  jobRequirements,
  gapAreas
})

Result: Rejection emails sent in 30 seconds vs. 10 minutes, with better candidate experience.


GDPR Compliance (Built Throughout)

This wasn't an afterthought - we built it into every feature:

1. Data Minimization

// Anonymous analytics (no PII)
await prisma.anonymousEvaluation.create({
  data: {
    aggregateScore: 85,
    jobCategory: 'Software Engineer', // Generalized
    experienceLevel: 'mid',           // No specific years
    industry: 'Technology',           // No company names
    // NO: email, name, IP address, CV text
  }
})

2. Automatic Deletion

// Cron job runs daily
export async function deleteExpiredEvaluations() {
  const deleted = await prisma.publicEvaluation.deleteMany({
    where: {
      expiresAt: { lte: new Date() }
    }
  })

  console.log(`Deleted ${deleted.count} expired evaluations`)
}

3. User Data Export (Article 20)

export default defineEventHandler(async (event) => {
  const { token } = getQuery(event)

  const evaluation = await prisma.publicEvaluation.findFirst({
    where: { accessToken: token }
  })

  // Export everything in JSON format
  return {
    personal_data: {
      email: evaluation.candidateEmail,
      name: evaluation.candidateName,
      uploaded_at: evaluation.createdAt
    },
    evaluation: {
      scores: { ... },
      summary: evaluation.summary,
      recommendation: evaluation.recommendation
    }
  }
})

4. Right to Erasure (Article 17)

// One-click deletion via emailed link
export default defineEventHandler(async (event) => {
  const { token } = getQuery(event)

  await prisma.publicEvaluation.delete({
    where: { accessToken: token }
  })

  // Send confirmation email
  await sendDeletionConfirmation(evaluation.candidateEmail)

  return { deleted: true }
})

Performance Optimizations

Problem 1: AI Evaluation Taking 45-60 Seconds

Initial approach: Synchronous API call to Claude

Issue: User waiting on blank screen for 60 seconds = bad UX

Solution: Streaming response with progress indicators

export default defineEventHandler(async (event) => {
  const stream = createEventStream(event)

  stream.push({ stage: 'extracting_cv', progress: 10 })
  const cvText = await extractTextFromCV(cvFile)

  stream.push({ stage: 'analyzing', progress: 40 })
  const evaluation = await evaluateWithAI(cvText, jobDescription)

  stream.push({ stage: 'generating_report', progress: 80 })
  await saveEvaluation(evaluation)

  stream.push({ stage: 'complete', progress: 100, data: evaluation })
})

Result: Users see progress, perceived wait time reduced 40%

Problem 2: Database Queries on Every Page Load

Issue: Fetching user + agency + credits + evaluations on dashboard = 4 queries

Solution: Prisma includes + caching

// Before: 4 separate queries
const user = await prisma.user.findUnique({ where: { id } })
const agency = await prisma.agency.findUnique({ where: { id: user.agencyId } })
const credits = await getAgencyCredits(agency.id)
const evaluations = await prisma.evaluation.findMany({ where: { agencyId: agency.id } })

// After: 1 query with includes
const user = await prisma.user.findUnique({
  where: { id },
  include: {
    agency: {
      include: {
        evaluations: { take: 20, orderBy: { createdAt: 'desc' } },
        _count: { select: { evaluations: true } }
      }
    }
  }
})

Result: Dashboard load time: 2.3s → 0.4s


What We Learned

1. AI Consistency Requires Structured Prompts

Early prompts were conversational. Results varied wildly:

  • Same CV evaluated 3 times → scores: 72, 84, 79
  • Recommendation logic inconsistent

Fix: JSON schema enforcement + examples

const prompt = `
Return EXACTLY this JSON structure (no additional text):
{
  "scores": {
    "qualification": <1-10>,
    "capability_confidence": <1-10>,
    ...
  },
  "recommendation": "<strong_yes|yes|maybe|no|strong_no>",
  "confidence_level": "<high|medium|low>"
}

Example output:
{
  "scores": { "qualification": 8, ... },
  "recommendation": "yes",
  "confidence_level": "high"
}
`

Result: Score variance reduced to ±2 points on repeat evaluations

2. GDPR Should Be Built In, Not Bolted On

We designed data deletion from day 1:

  • Every table has expiresAt or deletedAt
  • No hard-coded references to personal data
  • All PII behind access tokens

Benefit: When GDPR deletion request comes, it's one DELETE query, not a multi-week audit.

3. Users Will Test Your Limits

  • One user submitted 23 CVs for "waitress" role in 8 days (testing/iteration)
  • Someone uploaded their CV as BOTH the job description and application (confused)
  • Multiple users tried to bypass rate limits with VPNs (we added rate limit messaging)

Learning: Build for abuse from day 1. Every input is adversarial.

4. Multilingual AI Is Production-Ready

We were skeptical Claude could evaluate French/German/Spanish CVs accurately.

Reality: It worked perfectly. No translation layer needed.

Example: French CV for electrician in Congo scored 89.9/100 with detailed technical analysis in French.


The Results After 3 Weeks

Product Metrics

  • ✅ 500+ evaluations processed
  • ✅ 23 languages evaluated (English, French, German, Spanish, Arabic, Italian, etc.)
  • ✅ Average evaluation time: 42 seconds (vs. 30 minutes manual)
  • ✅ Score consistency: ±2 points on repeat evaluations
  • ✅ Zero GDPR complaints (built-in compliance worked)

Business Impact

  • ✅ Client onboarded 3 agencies in first 2 weeks
  • ✅ 97% time savings on CV screening
  • ✅ 15% more "hidden gem" candidates identified vs. manual screening
  • ✅ 1 enterprise trial signed (500+ candidates/month)

Technical Debt

  • ⚠ No automated testing (shipped fast, paying for it now)
  • ⚠ Manual deployment (need CI/CD pipeline)
  • ⚠ Basic error monitoring (need better observability)

Next sprint: Testing infrastructure, deployment automation, error tracking (Sentry).


Tech Stack Summary

Layer Technology Why
Frontend Vue 3 + Nuxt 4 Full-stack framework, SSR for SEO
Backend Nuxt Server API Same codebase as frontend, type-safe
Database PostgreSQL + Prisma Relational data, type-safe queries
AI Claude 4.0 Sonnet (Anthropic) Best reasoning, 200K context, structured output
Auth Supabase Auth OAuth (Google, LinkedIn), magic links
Storage AWS S3 CV files stored in secure S3 buckets
Deployment Railway Fast deploys, PostgreSQL included, $20/month
Email Nodemailer + SMTP Transactional emails (GDPR notifications)

Would We Do It Differently?

What Worked

✅ Nuxt 4 - Single codebase = fast iteration
✅ Prisma - Type safety caught 20+ bugs before production
✅ Claude 4.0 - Consistently better than GPT-4 for structured tasks
✅ GDPR-first design - Saved weeks of retrofitting
✅ Ship fast, iterate - Real users beat internal testing

What We'd Change

❌ Add tests from day 1 - Now retrofitting 50+ files
❌ Use feature flags - Hard to test in production without them
❌ Better error tracking - Lost time debugging production issues
❌ Document AI prompts - Multiple versions in git history now


Key Takeaways for Your Next AI Project

  1. Structure your prompts like API contracts - Define exact JSON schemas
  2. AI consistency > AI creativity - For production apps, repeatability matters more than cleverness
  3. GDPR is easier if you don't collect data - Minimize PII from day 1
  4. Test edge cases with real users - Internal QA won't find the weird stuff
  5. Ship fast, but not sloppy - We got away with no tests for 3 weeks. Don't push it to 6.

Interested in AI-Powered Development?

At Pedersen Consulting, we build production AI applications in weeks, not months. Our approach:

  • AI-first architecture - Design for LLM integration from day 1
  • Rapid iteration - Ship MVPs in 2-4 weeks, iterate based on real usage
  • GDPR compliance built in - No retrofitting privacy controls
  • Modern stack - Nuxt, Next.js, Prisma, PostgreSQL, Claude/GPT-4

Building something with AI? Let's talk: sune@pedersendev.com


This case study was written by Sune Pedersen, founder of Pedersen Consulting and Tanova. Code examples simplified for clarity.