Skip to main content
RapidDev - Software Development Agency

How to Build a Resume Builder Backend with Lovable

Build a resume builder backend in Lovable with structured section tables (experience, education, skills, projects), multiple visual templates that consume the same data, PDF generation via a Supabase Edge Function, and template preview Cards so users can switch templates without re-entering information.

What you'll build

  • Structured resume data model with separate tables for each section type (experience, education, skills, projects, summary)
  • Multiple shadcn/ui template components that render the same data in different visual styles
  • Template preview Cards so users can compare and select their preferred design
  • PDF generation Edge Function that renders the selected template as HTML and converts to PDF
  • Drag-to-reorder functionality for items within each section using sort_order
  • Live resume preview that updates as users edit their content
  • Download and shareable link for each generated resume
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate14 min read2–2.5 hoursLovable Pro or higherApril 2026RapidDev Engineering Team
TL;DR

Build a resume builder backend in Lovable with structured section tables (experience, education, skills, projects), multiple visual templates that consume the same data, PDF generation via a Supabase Edge Function, and template preview Cards so users can switch templates without re-entering information.

What you're building

A resume builder separates data from presentation. The data (your experience, education, skills) is stored in structured tables. Multiple templates are React components that accept the same data as props and render it differently — one template might be a clean single-column layout, another a two-column design with a colored sidebar. Users switch between templates without losing any data.

The data model uses a resumes table as the root object, with separate tables for each section: resume_experiences, resume_education, resume_skills, resume_projects, and resume_summary. Each table has a resume_id FK and a sort_order for drag-to-reorder. This normalized structure makes it easy to add, edit, or remove individual items without touching the rest of the resume.

PDF generation is handled by a Supabase Edge Function. It receives the resume ID and selected template name, fetches all resume data, renders the template as an HTML string using the same logic as the React component but as plain string interpolation (since Deno cannot run React), and converts to PDF via an HTML-to-PDF API. The PDF is stored in Supabase Storage and a signed URL is returned for download.

Final result

A resume builder where data is entered once and presented through multiple downloadable templates, with live preview and PDF export.

Tech stack

LovableFull-stack app generation
SupabaseDatabase, Auth, Storage, Edge Functions
Supabase StoragePDF file storage
shadcn/uiCards, Tabs, Badge, Separator
react-hook-form + zodSection editing forms
date-fnsDate formatting for employment periods

Prerequisites

  • Lovable Pro account for Edge Function generation
  • Supabase project with Storage enabled
  • An HTML-to-PDF API key (htmlpdfapi.com free tier works)
  • Supabase URL, service role key, and PDF API key saved to Cloud tab → Secrets

Build steps

1

Create the normalized resume data schema

Set up the resume tables. The normalized structure is the key architectural decision — it allows templates to read the same structured data without any per-template database changes.

prompt.txt
1Build a resume builder. Create these Supabase tables:
2
3- resumes: id, user_id, title (e.g. 'Software Engineer Resume'), full_name, email, phone, location, website_url, linkedin_url, github_url, is_public (bool default false), public_slug (text, UNIQUE, for shareable link), selected_template (text default 'minimal'), created_at, updated_at
4
5- resume_summary: id, resume_id (FK resumes UNIQUE), content (text, 2-4 sentence professional summary), created_at
6
7- resume_experiences: id, resume_id (FK resumes), company, title, location, start_date (date), end_date (date nullable, null = present), is_current (bool default false), bullets (text array, list of achievement bullets), sort_order (int), created_at
8
9- resume_education: id, resume_id (FK resumes), institution, degree, field_of_study, gpa (text nullable), start_date (date), end_date (date), highlights (text array), sort_order (int), created_at
10
11- resume_skills: id, resume_id (FK resumes), category (text e.g. 'Programming Languages'), skills (text array), sort_order (int), created_at
12
13- resume_projects: id, resume_id (FK resumes), name, description, tech_stack (text array), url (text nullable), start_date (date nullable), end_date (date nullable), bullets (text array), sort_order (int), created_at
14
15RLS: all tables require user_id = auth.uid() on resumes table; access to section tables via resume_id IN (SELECT id FROM resumes WHERE user_id = auth.uid()).
16
17Create a view resume_full_data that joins all section tables for a given resume_id used by the PDF generator.

Pro tip: Ask Lovable to create an import_from_linkedin function stub in the UI — a text area where users can paste their LinkedIn 'About' section and work history as plain text, and a prompt that asks Lovable's AI to parse and pre-fill the resume sections. This dramatically reduces time-to-first-draft.

Expected result: All six tables are created with the cascade-style RLS. The resume_full_data view is created. The app loads with a resume list page and a 'Create New Resume' button.

2

Build the section editors with live preview

Create the resume editing interface. Each section has its own form component. A live preview panel on the right shows the current template rendering as the user types.

prompt.txt
1Build the resume editor at src/pages/ResumeEditor.tsx:
2
31. Layout: editing panel (left, 45%), preview panel (right, 55%)
42. Editing panel with Tabs for each section: Header, Summary, Experience, Education, Skills, Projects
53. Header tab: form for full_name, email, phone, location, website_url, linkedin_url, github_url (all auto-saving on blur)
64. Experience tab:
7 - List of experience cards showing company + title + date range
8 - 'Add Experience' Button opens an inline form with: company, title, location, date range (start_date/end_date DatePickers, 'Currently working here' Checkbox), bullets (dynamic list type a bullet and press Enter to add another, click x to remove)
9 - Drag handle on each card to reorder (updates sort_order)
10 - Edit and delete buttons per card
115. Similar pattern for Education, Skills, and Projects tabs
126. Preview panel: renders the selected template component with the current resume data. Updates on every change using react-query with a 500ms debounce
137. Template selector: small thumbnail Cards above the preview showing all available templates. Clicking one updates resumes.selected_template and re-renders the preview

Pro tip: Ask Lovable to add an AI-powered bullet point improver: a magic wand icon next to each experience bullet that sends the raw text to an Edge Function calling Claude or OpenAI to rewrite it using action verbs and quantified achievements.

Expected result: The editor shows the two-panel layout. Adding an experience entry shows it in the live preview panel immediately. Switching templates changes the preview layout while keeping all data.

3

Build the template components

Create multiple React template components that each accept the same resume data props and render different visual layouts.

src/components/resume-templates/MinimalTemplate.tsx
1// src/components/resume-templates/MinimalTemplate.tsx
2import { Separator } from '@/components/ui/separator'
3import { Badge } from '@/components/ui/badge'
4import type { ResumeData } from '@/types/resume'
5import { format } from 'date-fns'
6
7interface Props { data: ResumeData; isPdfMode?: boolean }
8
9export function MinimalTemplate({ data, isPdfMode = false }: Props) {
10 const { resume, experiences, education, skills, projects, summary } = data
11 const dateRange = (start: string, end: string | null, isCurrent: boolean) =>
12 `${format(new Date(start), 'MMM yyyy')} — ${isCurrent || !end ? 'Present' : format(new Date(end), 'MMM yyyy')}`
13
14 return (
15 <div className={`bg-white text-gray-900 font-sans ${isPdfMode ? 'p-8' : 'p-6'} max-w-[794px]`}>
16 <div className="text-center mb-6">
17 <h1 className="text-3xl font-bold tracking-tight">{resume.full_name}</h1>
18 <div className="flex justify-center gap-4 text-sm text-gray-500 mt-1 flex-wrap">
19 {resume.email && <span>{resume.email}</span>}
20 {resume.phone && <span>{resume.phone}</span>}
21 {resume.location && <span>{resume.location}</span>}
22 {resume.linkedin_url && <a href={resume.linkedin_url} className="text-blue-600">{resume.linkedin_url.replace('https://linkedin.com/in/', 'linkedin.com/in/')}</a>}
23 </div>
24 </div>
25 {summary && (
26 <>
27 <h2 className="text-lg font-semibold uppercase tracking-wide mb-2">Summary</h2>
28 <Separator className="mb-2" />
29 <p className="text-sm leading-relaxed mb-4">{summary.content}</p>
30 </>
31 )}
32 {experiences.length > 0 && (
33 <>
34 <h2 className="text-lg font-semibold uppercase tracking-wide mb-2">Experience</h2>
35 <Separator className="mb-3" />
36 <div className="space-y-4 mb-4">
37 {experiences.map((exp) => (
38 <div key={exp.id}>
39 <div className="flex justify-between items-start">
40 <div>
41 <span className="font-semibold">{exp.title}</span> <span className="text-gray-600">at {exp.company}</span>
42 {exp.location && <span className="text-gray-500 text-sm ml-2">· {exp.location}</span>}
43 </div>
44 <span className="text-sm text-gray-500 whitespace-nowrap">{dateRange(exp.start_date, exp.end_date, exp.is_current)}</span>
45 </div>
46 <ul className="mt-1 space-y-0.5">
47 {exp.bullets.map((b, i) => <li key={i} className="text-sm text-gray-700 before:content-['•'] before:mr-2 before:text-gray-400">{b}</li>)}
48 </ul>
49 </div>
50 ))}
51 </div>
52 </>
53 )}
54 {skills.length > 0 && (
55 <>
56 <h2 className="text-lg font-semibold uppercase tracking-wide mb-2">Skills</h2>
57 <Separator className="mb-3" />
58 <div className="space-y-1 mb-4">
59 {skills.map((s) => (
60 <div key={s.id} className="flex gap-2 text-sm">
61 <span className="font-medium w-36 shrink-0">{s.category}:</span>
62 <span className="text-gray-600">{s.skills.join(', ')}</span>
63 </div>
64 ))}
65 </div>
66 </>
67 )}
68 </div>
69 )
70}

Expected result: The MinimalTemplate renders the resume data as a clean, print-ready layout. Switching to this template in the editor preview shows the minimal design.

4

Add PDF generation and shareable links

Build the PDF generation Edge Function and the public sharing feature. When a user makes their resume public, it gets a slug-based URL anyone can view.

prompt.txt
1Build two features:
2
31. PDF generation Edge Function at supabase/functions/generate-resume-pdf/index.ts:
4 - Authenticate the user via Authorization header
5 - Accept { resumeId, templateName } in request body
6 - Fetch all resume data using the resume_full_data view (service_role client)
7 - Build the HTML for the selected template using string interpolation (not React Deno can't render JSX)
8 - Call the HTML-to-PDF API with the HTML and A4 dimensions (794x1123px)
9 - Upload the PDF to Supabase Storage at resumes/{userId}/{resumeId}/{templateName}.pdf
10 - Return a signed URL valid for 1 hour
11
122. Public sharing feature:
13 - Add a 'Share Resume' toggle in the editor header
14 - When enabled: set resumes.is_public = true, generate a URL-safe slug from full_name + random 6-char suffix (e.g. 'jane-doe-a3f9k2'), store in public_slug
15 - Create a public route /resume/:slug that fetches resume data WHERE public_slug = slug AND is_public = true (no auth)
16 - Render using the resume's selected_template
17 - Add a 'Download PDF' Button on the public page that calls the generate-resume-pdf Edge Function with the resume owner's service_role (via a special public download endpoint)
18 - Show a 'Copy Link' Button that copies the public URL to clipboard

Expected result: Clicking 'Download PDF' generates a PDF and triggers a browser download. Enabling sharing shows a shareable URL. Opening that URL without being logged in shows the resume in its selected template.

Complete code

src/types/resume.ts
1export interface Resume {
2 id: string
3 user_id: string
4 title: string
5 full_name: string
6 email: string
7 phone: string | null
8 location: string | null
9 website_url: string | null
10 linkedin_url: string | null
11 github_url: string | null
12 is_public: boolean
13 public_slug: string | null
14 selected_template: 'minimal' | 'modern' | 'sidebar' | 'compact'
15 created_at: string
16 updated_at: string
17}
18
19export interface ResumeExperience {
20 id: string
21 resume_id: string
22 company: string
23 title: string
24 location: string | null
25 start_date: string
26 end_date: string | null
27 is_current: boolean
28 bullets: string[]
29 sort_order: number
30}
31
32export interface ResumeEducation {
33 id: string
34 resume_id: string
35 institution: string
36 degree: string
37 field_of_study: string
38 gpa: string | null
39 start_date: string
40 end_date: string
41 highlights: string[]
42 sort_order: number
43}
44
45export interface ResumeSkill {
46 id: string
47 resume_id: string
48 category: string
49 skills: string[]
50 sort_order: number
51}
52
53export interface ResumeProject {
54 id: string
55 resume_id: string
56 name: string
57 description: string
58 tech_stack: string[]
59 url: string | null
60 start_date: string | null
61 end_date: string | null
62 bullets: string[]
63 sort_order: number
64}
65
66export interface ResumeSummary {
67 id: string
68 resume_id: string
69 content: string
70}
71
72export interface ResumeData {
73 resume: Resume
74 summary: ResumeSummary | null
75 experiences: ResumeExperience[]
76 education: ResumeEducation[]
77 skills: ResumeSkill[]
78 projects: ResumeProject[]
79}

Customization ideas

ATS score checker

Add an ATS (Applicant Tracking System) analysis feature. An Edge Function calls an AI API (Claude or OpenAI) with the resume text and a job description the user pastes. The AI returns a compatibility score and specific suggestions: missing keywords, weak action verbs, and formatting issues that ATS systems struggle with.

Version history

Add a resume_versions table that stores a JSONB snapshot of the full resume on each save. Allow users to browse version history and restore a previous version. Show a timeline of versions with timestamps and a diff view highlighting what changed between versions.

Job application tracker

Add a job_applications table linked to resumes. Track which resume version was used, application date, company, role, status (applied/interview/offer/rejected), and notes. Show a Kanban board of applications. Link applications to specific resume versions so users know exactly which resume they sent.

Cover letter generator

Add a cover letters table linked to resumes. An Edge Function accepts the resume ID and job description, then calls Claude or OpenAI to generate a personalized cover letter that references specific experiences from the resume. Users can edit the generated draft and save it alongside their resume.

Common pitfalls

Pitfall: Trying to render React components inside the Edge Function for PDF generation

How to avoid: Build two parallel implementations: the React component for the live preview in the browser, and a plain HTML string builder function for the Edge Function. Both use the same data and produce visually identical output, but the Edge Function version uses string interpolation instead of JSX.

Pitfall: Storing the full resume as a single large JSONB blob

How to avoid: Use the normalized schema from Step 1 with separate tables for each section type. This enables powerful queries (find all users with React experience at companies of 100+ employees) and makes partial updates (change just one experience entry) efficient.

Pitfall: Not setting sort_order correctly after adding a new item

How to avoid: When inserting a new section item, set sort_order to MAX(sort_order) + 1 for that resume_id. On drag-end, update all affected items' sort_order values in a single batch upsert. Ask Lovable to implement the reorder logic using a fractional indexing approach for fewer database writes.

Pitfall: Using CSS classes with Tailwind in the PDF-generated HTML

How to avoid: Use inline styles (style='color: #6b7280') in the PDF HTML string builder function. Alternatively, include a minimal CSS stylesheet in the HTML string's head section. The live preview uses Tailwind classes normally since Tailwind is loaded in the browser.

Best practices

  • Separate data from presentation from the start. The normalized table schema means users can switch templates without any data migration, and you can add new templates without changing the database.
  • Auto-save section forms on blur rather than requiring explicit save button clicks. Resume editing is frequent and accidental data loss is very frustrating. Use react-hook-form's watch() and debounced Supabase updates.
  • Store PDFs in a user-scoped path in Supabase Storage: resumes/{user_id}/{resume_id}/{template}.pdf. This makes it easy to list and delete all PDFs for a user during account deletion.
  • Add a char limit on the bullets array items (e.g. 200 chars per bullet). Resume bullets should be concise. Enforce this in the Zod schema for the experience form with z.string().max(200).
  • Generate the public_slug from the user's name using a slugify function: lowercase, replace spaces with hyphens, remove special characters, append a random 6-character suffix. Ensure uniqueness by checking if the slug exists before saving. Use a Supabase RPC function for the generation to handle retries on collision.
  • Test all templates at A4 dimensions (794px x 1123px) in print preview mode. Template layouts that look good on screen often break at print dimensions. Ask Lovable to add a 'Print Preview' mode that shows the template at A4 size with a shadow border.

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a resume builder where I have normalized tables for experiences, education, skills, and projects. I need to convert the live React template (using Tailwind CSS classes) to a PDF-safe HTML string builder function for a Deno Edge Function. Help me write a TypeScript function buildResumeHtml(data: ResumeData, template: 'minimal' | 'modern'): string that produces valid HTML with inline styles for the minimal template. Include the full_name, email, experiences with date ranges, and skills sections.

Lovable Prompt

Add a 'Suggestions' panel to the resume editor that appears on the right side when the user is editing their experience section. The panel analyzes the current bullets and suggests improvements: checks for action verbs at the start, suggests adding quantified results ('increased sales by X%' instead of 'increased sales'), and flags bullets that are too long (>150 chars). Use pattern matching in TypeScript — no AI API needed for basic suggestions. Highlight flagged bullets in yellow in the editing list.

Build Prompt

In Supabase, create a function get_resume_full_data(p_resume_id uuid) that returns all resume data as a single JSONB object for use in the PDF generation Edge Function. The object should have this shape: { resume: {...}, summary: {...} | null, experiences: [...], education: [...], skills: [...], projects: [...] }. Use row_to_json and json_agg to build the nested structure. The function should be SECURITY DEFINER so the Edge Function can call it with the service_role key.

Frequently asked questions

How do I add a new template design?

Create a new React component at src/components/resume-templates/YourTemplate.tsx that accepts ResumeData as props and renders it differently from the existing templates. Add the template name to the selected_template union type in resume.ts. Add a thumbnail preview Card in the template selector UI. Create a corresponding HTML string builder in the PDF Edge Function for the same template. The data model never changes — only the presentation layer.

Can users have multiple resumes for different job types?

Yes. Each user can have multiple rows in the resumes table. The resumes list page shows all of them. A user might have 'Software Engineer Resume', 'Product Manager Resume', and 'Freelance Consultant Resume' each with different content emphasis and selected templates. The Create New Resume button creates a blank new resume.

What PDF quality can I expect from HTML-to-PDF conversion?

HTML-to-PDF services using headless Chromium (like htmlpdfapi.com or WeasyPrint) produce high-quality output that matches what you see in the browser preview. Fonts render correctly, borders are crisp, and the layout is pixel-accurate. The main limitation is web fonts — use system fonts (Georgia, Arial, Times New Roman) in the PDF HTML for guaranteed rendering. Google Fonts can be referenced via @import in the style tag but may not load in all PDF services.

How do I make the resume ATS-friendly?

ATS (Applicant Tracking System) software has trouble parsing PDFs with complex layouts, columns, tables, and graphics. The Minimal template in this guide uses a single-column layout which is the safest for ATS. Add an 'ATS Mode' toggle that strips all decorative elements and renders plain text only. The customization idea in this guide describes adding an AI-powered ATS score checker for more detailed analysis.

Is the public shareable resume link indexed by Google?

By default, Lovable apps allow search engine crawling. If you want public resume pages to be indexed (for personal branding), no action is needed. If you want them private from search engines, add a meta robots noindex tag to the public resume page: add a noIndex prop to the page's head element. You can also add canonical URLs per resume page for better SEO if you want them indexed.

Can I import data from an existing resume PDF?

Not directly in the base build. Adding PDF import requires an Edge Function that calls an AI API (Claude or GPT-4) with the extracted PDF text to parse it into structured JSON matching your resume schema. This is the 'Resume Parser' related build in this guide. Ask Lovable to add an 'Import from PDF' button after the base resume builder is working.

How do I handle special characters and Unicode in PDF output?

Include a UTF-8 meta charset in the HTML string: a UTF-8 meta charset tag. Use a font that supports the required character ranges — the system fonts Georgia and Arial support most Latin, Cyrillic, and Greek characters. For East Asian characters or Arabic, you'll need to specify a Unicode-capable font and include it in the PDF service request. Test your template with sample data containing the special characters your users will need.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.