I've been working as a full stack developer and kept seeing the same problem — people spend hours tweaking resume formatting instead of focusing on content. Most resume tools are either expensive, locked behind subscriptions, or require you to create an account just to see a template.
So I built CVForge — an AI-powered resume builder where you upload your CV, GPT-4 rewrites it for ATS and recruiters, you pick a template, and download as PDF or Word. No account required.
The Stack
- Next.js 15 (App Router) — handles both frontend and API routes in one codebase
- OpenAI API (GPT-4o) — resume extraction and AI optimization
- Razorpay — payments (UPI, cards, wallets for Indian users)
- MongoDB + Mongoose — payment persistence and admin panel
- pdf-lib + docx — generating PDF and Word files server-side
How It Works
- User uploads a PDF or Word resume
- OpenAI extracts all structured data (name, experience, skills, education)
- User picks from 4 professional templates and previews instantly
- Optionally pays ₹15 for AI optimization — GPT-4 rewrites bullet points to be ATS-friendly and keyword-rich
- Pays ₹5 to download as PDF or Word
The Hard Parts
File generation server-side was trickier than expected. Generating pixel-perfect PDFs and DOCX files with proper formatting, tables, and 2-column layouts required a lot of trial and error with the docx package. Font sizes are in half-points, colors can't have # prefix — small things that cost hours.
iOS Safari downloads were a pain. Safari kills the user gesture activation after an async chain (like a Razorpay payment popup), so window.open() gets blocked. Fix: open window.open('about:blank', '_blank') synchronously before any await, then navigate it after the fetch completes.
// Open window synchronously before any await
const iosWin = window.open('about:blank', '_blank')
// ... do your async work ...
// Then navigate it after fetch completes
iosWin.location.href = url
Razorpay on mobile — on iOS specifically, skip auto-download after payment and show a toast asking the user to tap the download button again with a fresh gesture.
What I'd Do Differently
- Start with SSR in mind from day one. I had all my landing page content in a
'use client'component wrapped inSuspensewithuseSearchParams— which meant crawlers saw an empty page. Fixed it by passingsearchParamsas props from the server component instead. - Build the admin panel earlier. Flying blind without usage stats is frustrating.
Result
Live at cvforge.in — just launched on Product Hunt today.
If you're building something similar or have feedback, drop a comment. Always happy to talk Next.js + AI integrations.
Top comments (2)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.