Personal project · pre-launch
ApplyWise: CV Tailoring Where Every Line Cites a Fact
A web app, not launched yet, that breaks a CV into small editable facts, matches them to a job description, and writes a tailored CV and cover letter where each line cites the facts behind it. A verifier (rules first, then an LLM judge) strips anything unsupported. There is also a free ATS checker, an application tracker and a Chrome extension.
The problem
- Tailoring a CV by hand takes me 30 to 60 minutes per job, and generic AI tools happily add skills the candidate doesn't have.
- Applicant tracking systems quietly drop CVs that use columns, tables or images.
- A CV is personal data, so it has to be encrypted, exportable and deletable.
Architecture and key decisions
- Next.js 15 web app, a NestJS 11 API with Prisma 6, and a Go service that parses PDFs, produces the ATS report and renders an ATS-safe single-column PDF.
- Clean architecture by convention: the domain classes (fact extractor, match scorer, hybrid retriever, generator, verifier) import nothing from Nest or Prisma, and controllers and repositories are thin adapters.
- One LLM interface with Anthropic, Gemini and Ollama adapters plus a deterministic fake. The real adapters are unit-tested against mocks and haven't been run against a live model yet. Structured outputs, a versioned prompt registry with fingerprints, and a per-user cost ledger with a monthly budget.
- Hybrid retrieval with pgvector (HNSW) and Postgres full-text search, fused with Reciprocal Rank Fusion. The vectors come from a deterministic 512-dimension hashing embedder, so there is no embedding vendor to pay.
- Personal data is encrypted with AES-256-GCM before it reaches Postgres, emails are looked up through an HMAC blind index, and there are export and delete endpoints. Google sign-in uses OIDC with PKCE, state and nonce.
- A BullMQ worker classifies application emails you upload or paste (.eml) and moves the card on the kanban tracker. There is no inbox integration. Plans and usage metering sit behind a payment port (only a fake provider so far), and quota is reserved under a Postgres advisory lock.
- Terraform for AWS in ap-south-1: eight modules (KMS, S3, SQS, Secrets Manager, IAM, ECR, network, ECS Fargate + RDS), with least-privilege IAM checked by terraform tests.
Results
- An eval gate of 30 golden CV and job-description pairs runs in CI and fails the build if 1% or more of shipped lines are fabricated. It runs on a deterministic fake model that plants three fabrications in every pair.
- Latest report: the verifier removed all 90 planted claims, and 0 of 570 shipped claims were fabricated. A run on a real model comes next, once I have a paid API key.
- Six parallel tailorings on a free plan give exactly 3 successes and 3 HTTP 402s, on real Postgres.
- S3, SQS, Secrets Manager, KMS and IAM were applied, checked and destroyed on LocalStack. ECS, ECR, RDS and the VPC are planned only.
- Go tests write golden JSON that a shared zod schema checks on the TypeScript side, and the API validates every live parser response.
Tech stack
- TypeScript
- Next.js 15
- NestJS 11
- Prisma
- PostgreSQL
- pgvector
- Redis
- BullMQ
- Go
- LLM evals
- Anthropic / Gemini / Ollama adapters
- OIDC
- Chrome Extension
- Terraform
- AWS (LocalStack)
- Playwright
- Jest
Links
- Architecture notes and code excerpts: github.com/jani358/applywise-showcase. The main repo is private. Full source available on request.
- Case study by Janindu Gayanga, full-stack software engineer in Colombo, Sri Lanka
- Profile on GitHub and LinkedIn
Other projects
- AskLK: Bilingual AI Assistant with RAG, Agents and MCP: Multi-tenant RAG assistant that answers in Sinhala or English, with citations
- BookLK: Event-Driven Service Booking Marketplace: Multi-vendor booking with Kafka, a Go realtime service, payments and Kubernetes
- QueueLK: Real-Time Queue Management for Clinics: Live queue display, phone page and Expo patient app in Sinhala, Tamil and English
- iGaming Platform: Go Microservices, Payments and Compliance: Real-money gaming platform with 12,299 registered players, moving from a Node.js monolith to Go