Skip to content
Case Study

CVTailor

AI-powered CV tailoring that helps job seekers beat ATS systems and land more interviews.

Go (Gin)AngularPostgreSQLGemini AIRedisDockerAWS S3Stripe
CVTailor dashboard showing resume tailoring interface with ATS compatibility score

Overview

CVTailor is a full-stack SaaS platform that optimizes resumes for specific job descriptions using AI. Users upload their CV, paste a target job description, and the system produces a tailored version — rewritten to match the role's language, weighted toward ATS keyword compliance, and scored for confidence and compatibility. The platform handles the entire lifecycle: resume parsing, job description analysis, AI-driven tailoring with hallucination safeguards, keyword gap reporting, multi-format export (PDF/DOCX), and subscription billing via Stripe and M-Pesa.

The Problem

🚫

ATS Filtering

Most job seekers send the same generic CV to every role. Applicant Tracking Systems filter out ~75% of applications before a human ever reads them. Candidates miss opportunities not because of qualifications, but because their resume doesn't match specific keywords.

Time Drain

Job seekers in competitive European markets submit 50–100+ applications. Manually tailoring a CV per application takes 30–60 minutes — unsustainable at scale. Most candidates also lack the insight to identify which changes will improve ATS rankings.

Bottom line: CVTailor turns a 45-minute manual process into a 30-second automated one — with measurable ATS compatibility scoring.

The Solution

01

Upload & Parse

Users upload a PDF/DOCX resume or build one using the structured editor. The AI-powered parser extracts structured data (experience, skills, education, certifications) from raw documents.

02

Analyze & Match

The system embeds both the CV and job description into vector space using AI embeddings, computes a cosine similarity score for semantic matching, and extracts role-critical keywords to identify gaps.

03

Tailor & Score

The AI rewrites the resume to align with the job description: adjusting bullet points, inserting missing keywords naturally, and matching tone to the seniority level. The output includes a tailored CV, change log, ATS score, and confidence rating.

System Architecture

Client Layer
Angular SPA (Standalone Components)
AuthDashboardResume EditorTailoringExportBillingAnalytics
HTTPS / REST
API Gateway — Gin HTTP Router (v1)
Middleware
CORSJWT AuthRate Limiter
Handlers
AuthResumeTailorExportBillingAnalyze
Business Logic — Use Cases (Clean Architecture)
TailoringUseCase
Quota → Fetch → AI → Validate → Store
AnalyzeUseCase
Embed → Cosine Similarity → Keywords
ResumeUseCase
CRUD, Upload, AI Parse, Versions
AuthUseCase
Login, Register, OAuth, Verify
SubscriptionUseCase
Tiers, Entitlements, Billing
ExportUseCase
PDF/DOCX Generation
Infrastructure Layer
Gemini 2.0 Flash
Tailor & Parse
OpenAI GPT-4o
Embeddings & Keywords
PostgreSQL
JSONB Resume Data
AWS S3
CV File Storage
Redis
Caching & Sessions
Stripe
Card Billing
M-Pesa
Mobile Payments
SMTP
Email Notifications

Data Flow: CV Tailoring Request

  1. 1User submits: { resume_id, job_description, role_level }
  2. 2TailoringUseCase checks subscription quota (tailorings_used < tailorings_limit)
  3. 3Fetches Resume from PostgreSQL (JSONB or file reference)
  4. 4Stores JobDescription entity with raw text + metadata
  5. 5If file-based: Fetch from S3 → Send binary to Gemini. If structured: Send JSON to Gemini
  6. 6Gemini returns: { tailored_resume, changes_made, keyword_analysis, ats_score }
  7. 7Post-generation validation: structural check, name integrity, company integrity (≥80%)
  8. 8Store TailoringSession with full diff, scores, and model metadata
  9. 9Increment subscription usage counter + log audit event
  10. 10Return tailored CV, change log, keyword gap report, ATS score

Domain Model

EntityPurpose
UserAuth identity, profile
ResumeStructured CV (JSONB), file reference, versions
JobDescriptionRaw JD text, parsed keywords, company/title
TailoringSessionResume ↔ JD ↔ AI output with scores and diffs
SubscriptionUsage quotas, tier, billing status
AuditLogImmutable action history per user

Key Features

🤖

AI-Powered CV Parsing

Upload PDF/DOCX and the system extracts structured data using Gemini's multimodal capabilities.

✂️

Intelligent CV Tailoring

Rewrites resume content to match the target job description, adjusting tone based on seniority level.

📊

ATS Compatibility Scoring

Generates a 0–100 ATS score with keyword match/miss report and insertion recommendations.

🔍

Semantic Match Analysis

Embeds CV and JD into vector space and computes cosine similarity combined with keyword extraction.

🛡️

Hallucination Detection

Post-generation validation ensures the AI doesn't fabricate experience or change candidate identity.

📄

Multi-Format Export

Export tailored CVs to PDF and DOCX formats with entitlement-gated access per subscription tier.

💳

Dual Payment Processing

Stripe for international cards and M-Pesa STK Push for mobile money users in East Africa.

📋

Resume Version Tracking

Every edit and AI-tailored output is tracked as a version with change summaries and source metadata.

Technical Highlights

Clean Architecture

The Go backend is structured around domain-driven design with strict layer boundaries. Domain interfaces define contracts that infrastructure implementations satisfy — making it straightforward to swap AI providers or storage backends.

internal/
├── domain/# Entities, interfaces (zero dependencies)
├── usecase/# Application logic, orchestration
├── delivery/# Gin handlers and middleware
├── infrastructure/# AI, storage, billing integrations
├── repository/# Data access (PostgreSQL)
└── jobs/# Background workers (billing renewal)

Concurrent Embedding Pipeline

The match analysis use case leverages Go's goroutines to fetch embeddings in parallel, halving the latency of the scoring pipeline.

// Fire two concurrent embedding requests
ch := make(chan embedResult, 2 )
go func(){ vec, err := ai.GetEmbedding(ctx, cvText); ch <- ... } ()
go func(){ vec, err := ai.GetEmbedding(ctx, jdText); ch <- ... } ()
// Cosine similarity → weighted score
overall = 0.6 × semantic + 0.4 × keyword

JSONB-First Data Model

Resume data stored as PostgreSQL JSONB, enabling flexible schema evolution without migrations while supporting efficient querying with GIN indexes.

Rate Limiting & Quotas

Per-user rate limiter (60 req/min) at middleware, plus use-case-level subscription quota checks before any AI API call. Resume count includes soft-deleted records.

JWT + Google OAuth

Configurable access (12h) and refresh (7d) tokens. Google OAuth for frictionless onboarding. Input validation via Gin's binding tags.

Secure File Handling

S3 file uploads with 10MB hard cap, CORS whitelisting with multi-origin support, and no-cache headers on API responses.

Challenges & Solutions

Challenge

The AI would occasionally fabricate work experience entries, invent company names, or change the candidate's name.

Solution

Implemented a three-layer validation pipeline: structural check (experience count parity), name integrity (exact match), and company integrity (≥80% match rate). Outputs failing any check are rejected before reaching the user.

Impact

30s
CV tailoring time
Down from ~45 minutes
0–100
ATS scoring
Quantified before submission
60/40
Weighted scoring
Semantic + keyword match
2
Payment methods
Stripe + M-Pesa

Future Improvements

Async Job Queue

Move AI tailoring to a Redis-backed background worker to avoid HTTP timeout pressure.

Embedding Cache

Store computed embeddings per resume version so repeat analyses skip the embedding step.

Multi-Model Fallback

Route to Gemini by default, fall back to GPT-4o on errors using a circuit-breaker pattern.

Streaming Responses

Use SSE to stream tailored sections as they generate, improving perceived performance.

Resume Templates

Let users choose professional PDF templates using a Go HTML → PDF rendering pipeline.

Batch Tailoring

Submit one resume against multiple JDs in a single request with worker concurrency.

Observability

Add OpenTelemetry tracing with Grafana dashboards for latency percentiles per AI model.

A/B Testing Prompts

Version prompts alongside models and track ATS score distributions to improve quality.

Lessons Learned

01

AI output is unreliable — validate it

Treating AI responses as untrusted input and adding post-generation validation was the single most important engineering decision.

02

Clean architecture pays off early

Swapping from OpenAI to Gemini required changing one implementation file. Domain and use cases didn't need a single edit.

03

JSONB is underrated for evolving schemas

Storing resume data as JSONB let me add new sections without migrations, while still allowing structured Go type assertions.

04

Quota enforcement belongs in the use-case layer

Checking limits inside the use case (not middleware) means every code path consistently respects the same limits.

05

Dual payment integration is complex but necessary

Stripe + M-Pesa required separate webhook flows, but eliminated a geographic access barrier for East African users.

06

Go concurrency shines for I/O-bound work

Running embedding computations in parallel goroutines halved the latency of the match analysis pipeline.

Interested in working together?

I'm open to opportunities in the European tech market.