Why Build an Invoice Generator?
Here's a dirty secret about freelancing: the tools cost almost as much as the headache of finding clients.
FreshBooks charges $15–55/month. QuickBooks Self-Employed is $15/month. Wave is "free" but sells your data and pushes you toward their payment processing. Even simple invoicing tools nickel-and-dime you once you need PDF exports or more than 5 clients.
Meanwhile, you're a vibe coder. You've got Claude or Cursor open right now. And an invoice generator is one of those perfect projects that:
- Solves a real problem you actually have. You need to get paid. Invoices make that happen.
- Teaches you how real apps work. Forms, state management, database operations, PDF generation — this project touches all of it.
- Ships in a single session. No weeks of building. You'll have a working app before dinner.
- Saves you real money. At $15/month, that's $180/year. Your self-built version costs $0–20/month to host.
If you've already built smaller things — a SaaS app, a personal site — this is the perfect next project. It's practical, it's scoped, and you'll actually use it.
And if this is your first real project? Even better. There's nothing more motivating than building something that makes you money.
What You'll Build
By the end of this tutorial, your invoice generator will:
- Create invoices with your business info, client details, and multiple line items
- Calculate automatically — subtotals per line item, overall subtotal, tax, and grand total
- Generate PDFs that look professional enough to send to any client
- Save to a database so you can track what's been sent, what's paid, and what's overdue
- Mark payment status — draft, sent, paid, overdue
- List all invoices with filtering and search
The stack: Next.js for the app (built on React), Supabase for the database, @react-pdf/renderer for generating PDFs, and Tailwind CSS for styling. Everything deploys to Vercel for free.
The Prompt
This is the prompt that kicks everything off. Copy it, paste it into Claude, Cursor, or whatever AI coding tool you use, and let it generate the project.
Build me a full-stack invoice generator application with Next.js 14 (App Router), TypeScript, Tailwind CSS, Supabase, and @react-pdf/renderer.
Core features:
- Create new invoices with: my business name/address/email, client name/address/email, invoice number (auto-generated), invoice date, due date, and multiple line items (description, quantity, unit price)
- Auto-calculate: line item totals (qty × price), subtotal, tax amount (configurable tax rate, default 0%), and grand total
- Generate a professional PDF of any invoice using @react-pdf/renderer with clean typography and layout
- Save all invoices to Supabase with status tracking: draft, sent, paid, overdue
- Dashboard page listing all invoices with status badges, totals, and a search/filter bar
- Invoice detail page with edit capability and a "Download PDF" button
Database schema (Supabase):
- invoices table: id (uuid), invoice_number (text, unique), status (enum: draft/sent/paid/overdue), from_name, from_email, from_address, to_name, to_email, to_address, invoice_date, due_date, tax_rate (decimal), notes (text), created_at, updated_at
- line_items table: id (uuid), invoice_id (fk → invoices), description, quantity (decimal), unit_price (decimal), created_at
Design: Clean, minimal, professional. White background, subtle borders, good typography. The PDF should look like something you'd actually send to a client — not a developer prototype.
Include: Supabase migration SQL, environment variable setup instructions, and a README with setup steps.
That's a detailed prompt. That's intentional. The more specific you are about what you want — especially the database schema and the feature list — the closer the AI gets on the first try. Vague prompts get vague apps.
What AI Generated
When you run that prompt, your AI will generate a project structure that looks something like this:
invoice-generator/
├── app/
│ ├── layout.tsx ← Root layout with nav
│ ├── page.tsx ← Dashboard (invoice list)
│ ├── invoices/
│ │ ├── new/
│ │ │ └── page.tsx ← Create invoice form
│ │ └── [id]/
│ │ ├── page.tsx ← Invoice detail/edit
│ │ └── pdf/
│ │ └── page.tsx ← PDF preview/download
├── components/
│ ├── InvoiceForm.tsx ← The big form component
│ ├── InvoiceList.tsx ← Dashboard table
│ ├── InvoicePDF.tsx ← PDF template
│ ├── LineItemRow.tsx ← Single line item input
│ └── StatusBadge.tsx ← Draft/sent/paid badges
├── lib/
│ ├── supabase.ts ← Database client
│ ├── types.ts ← TypeScript types
│ └── utils.ts ← Helpers (format currency, generate invoice numbers)
├── supabase/
│ └── migrations/
│ └── 001_create_tables.sql
├── .env.local.example
└── README.md
This is a well-organized project. Let's break down what each part does and why it matters.
Understanding Each Part
The Database Schema
The migration file is the foundation of everything. If you're new to migrations, they're just SQL files that set up your database — check out What Are Database Migrations? for the full picture.
Here's what the AI generates:
-- Create invoice status enum
CREATE TYPE invoice_status AS ENUM ('draft', 'sent', 'paid', 'overdue');
-- Invoices table
CREATE TABLE invoices (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
invoice_number TEXT UNIQUE NOT NULL,
status invoice_status DEFAULT 'draft',
from_name TEXT NOT NULL,
from_email TEXT,
from_address TEXT,
to_name TEXT NOT NULL,
to_email TEXT,
to_address TEXT,
invoice_date DATE DEFAULT CURRENT_DATE,
due_date DATE,
tax_rate DECIMAL(5,2) DEFAULT 0,
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Line items table
CREATE TABLE line_items (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
invoice_id UUID REFERENCES invoices(id) ON DELETE CASCADE,
description TEXT NOT NULL,
quantity DECIMAL(10,2) DEFAULT 1,
unit_price DECIMAL(10,2) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Auto-update the updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER invoices_updated_at
BEFORE UPDATE ON invoices
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
Two tables. That's it. The invoices table stores the header info — who's sending, who's receiving, dates, tax rate. The line_items table stores each individual thing you're billing for. They're connected by invoice_id — every line item belongs to one invoice.
The ON DELETE CASCADE part is important: if you delete an invoice, its line items get deleted automatically. Without this, you'd have orphaned line items cluttering your database forever.
The Invoice Form
This is where most of the complexity lives. The AI generates a form component that manages:
- Your business info — name, email, address (saved so you don't re-type it)
- Client info — name, email, address
- Line items — a dynamic list where you can add/remove rows
- Calculations — automatic subtotals, tax, and grand total
The key piece is how line items work. In React, when you have a list of things that can change — like line items on an invoice — you store them in "state" (the app's memory). When you add a row, change a quantity, or delete a line, the state updates and React re-renders the calculations.
// This is the heart of the line items logic
const [lineItems, setLineItems] = useState<LineItem[]>([
{ description: '', quantity: 1, unit_price: 0 }
]);
// Add a new empty line item
const addLineItem = () => {
setLineItems([...lineItems, { description: '', quantity: 1, unit_price: 0 }]);
};
// Calculate totals
const subtotal = lineItems.reduce(
(sum, item) => sum + (item.quantity * item.unit_price), 0
);
const taxAmount = subtotal * (taxRate / 100);
const total = subtotal + taxAmount;
You don't need to memorize this. You need to understand the concept: the app keeps a list of line items in memory, and every time you change one, it recalculates the math. If the calculations ever seem wrong, this is the code you'll point your AI toward.
PDF Generation
This is the part that makes it feel like a real app. The AI uses @react-pdf/renderer — a library that lets you build PDF documents using React-like components. Instead of writing to a file, you describe what the PDF should look like:
import { Document, Page, Text, View, StyleSheet } from '@react-pdf/renderer';
const InvoicePDF = ({ invoice, lineItems }) => (
<Document>
<Page size="A4" style={styles.page}>
<View style={styles.header}>
<Text style={styles.title}>INVOICE</Text>
<Text>{invoice.invoice_number}</Text>
</View>
{/* From / To section */}
<View style={styles.parties}>
<View>
<Text style={styles.label}>From</Text>
<Text>{invoice.from_name}</Text>
<Text>{invoice.from_address}</Text>
</View>
<View>
<Text style={styles.label}>Bill To</Text>
<Text>{invoice.to_name}</Text>
<Text>{invoice.to_address}</Text>
</View>
</View>
{/* Line items table */}
{lineItems.map((item, i) => (
<View key={i} style={styles.row}>
<Text style={styles.description}>{item.description}</Text>
<Text style={styles.qty}>{item.quantity}</Text>
<Text style={styles.price}>${item.unit_price.toFixed(2)}</Text>
<Text style={styles.amount}>
${(item.quantity * item.unit_price).toFixed(2)}
</Text>
</View>
))}
{/* Totals */}
<View style={styles.totals}>
<Text>Subtotal: ${subtotal.toFixed(2)}</Text>
<Text>Tax ({invoice.tax_rate}%): ${taxAmount.toFixed(2)}</Text>
<Text style={styles.grandTotal}>
Total: ${total.toFixed(2)}
</Text>
</View>
</Page>
</Document>
);
It looks like HTML, but it generates a PDF. That's the magic of @react-pdf/renderer — you describe the layout in familiar syntax, and the library turns it into a downloadable PDF file. No servers needed, no third-party API calls, no per-document fees.
The Dashboard
The dashboard is your home base — a list of all invoices with their status, client name, amount, and date. The AI generates a table with clickable rows, status badges (colored labels showing draft/sent/paid/overdue), and usually a search bar or filter dropdown.
This is where the Supabase queries live. When the dashboard loads, it fetches all invoices from the database:
const { data: invoices } = await supabase
.from('invoices')
.select(`
*,
line_items (*)
`)
.order('created_at', { ascending: false });
That select('*, line_items(*)') is a join — it fetches each invoice along with all its line items in a single query. Supabase makes this easy because it knows about the foreign key relationship you set up in the migration.
Status Tracking
Every invoice has a status: draft (you're still working on it), sent (you've sent it to the client), paid (money received), or overdue (past due date, not paid). The AI generates a simple dropdown or button to change status, plus logic to automatically flag invoices as overdue when they pass their due date.
What AI Gets Wrong
The AI gets you 85% of the way there. Here's the 15% you'll need to fix.
1. PDF Rendering on the Server
This is the #1 issue. @react-pdf/renderer works great in the browser, but the AI often tries to render PDFs on the server side (in a Server Component). You'll get errors like Cannot use import statement outside a module or document is not defined.
'use client' at the very top of any file that uses @react-pdf/renderer. This tells Next.js to run that code in the browser, where the PDF library actually works. If the AI didn't do this, tell it: "The PDF component needs to be a client component. Add 'use client' to the top of InvoicePDF.tsx and the PDF download page."
2. Floating Point Math on Currency
Computers are famously bad at decimal math. 0.1 + 0.2 = 0.30000000000000004 in JavaScript. The AI usually does currency calculations with regular floating point numbers, which means you might see invoices with totals like $1,247.5300000001.
(amount / 100).toFixed(2)." Alternatively, always apply .toFixed(2) at the display layer. The cents approach is more robust, but .toFixed(2) works fine for a freelancer invoice tool.
3. Invoice Number Generation
The AI usually generates invoice numbers with something like INV-001 plus an auto-incrementing counter. The problem? If two invoices get created at the same time, or if the counter logic has a race condition, you get duplicate numbers. For a solo freelancer this is unlikely, but it's sloppy.
4. Missing Supabase Row-Level Security
If you ever make this multi-user (or even if you just want good habits), the AI almost always forgets to set up Row-Level Security (RLS) policies in Supabase. Without RLS, anyone with your Supabase URL and anon key can read all invoices in the database.
5. The PDF Looks Like a Developer Made It
This is subjective, but real: the AI's first-pass PDF design usually looks like a wireframe. Gray text on white, no visual hierarchy, cramped spacing. It's functional but not something you'd feel confident sending to a client who's paying you $5,000.
How to Debug Common Issues
Here are the errors you're most likely to hit, and exactly what to tell your AI to fix them.
"Module not found: @react-pdf/renderer"
You forgot to install the package. Run npm install @react-pdf/renderer in your terminal. If the AI set up the project but didn't include this in the install instructions, that's a common oversight.
"TypeError: Cannot read properties of undefined (reading 'map')"
This means the line items data hasn't loaded from the database yet, but the code is trying to loop through it. Tell the AI: "Add a loading check before mapping over line_items. If the data is null or undefined, show a loading spinner or return early."
PDF Downloads as Blank or Corrupted
Usually a server-side rendering issue. Make sure the PDF component is a client component ('use client'). Also check that you're using <PDFDownloadLink> from the library — the AI sometimes tries to use renderToStream or other server-side methods that don't work in a Next.js client component.
Supabase Returns Empty Arrays
Three things to check: (1) Your .env.local file has the correct Supabase URL and anon key. (2) The tables actually exist — run the migration SQL in the Supabase SQL editor. (3) RLS isn't blocking reads — check the RLS settings on each table.
Calculations Are Slightly Off
Floating point issue. See the "Floating Point Math on Currency" section above. Quick fix: wrap all displayed amounts in .toFixed(2). Better fix: store all amounts as integer cents.
Getting It Running: Step by Step
Once the AI generates your code, here's the setup process:
Step 1: Create a Supabase Project
Go to supabase.com, create a free account, and start a new project. Note your Project URL and anon key from Settings → API. You'll need these in a moment.
Step 2: Run the Migration
Go to the SQL Editor in your Supabase dashboard. Paste in the migration SQL that the AI generated (the CREATE TABLE statements from above). Click "Run." Your tables are live.
Step 3: Set Up Environment Variables
Copy .env.local.example to .env.local and fill in your Supabase credentials:
NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbG...
Step 4: Install and Run
npm install
npm run dev
Open http://localhost:3000. You should see your invoice dashboard. Click "New Invoice" and create your first one.
Step 5: Deploy
Push to GitHub, connect to Vercel, add your environment variables in the Vercel dashboard, and deploy. Done. Your invoice generator is live on the internet.
Stretch Goals: Make It Yours
The basic version works. Now make it yours. Here are prompts for your next AI sessions:
🎨 Custom Branding
"Add a logo upload feature. Let me upload my business logo and have it appear in the top-left of every PDF invoice. Store the logo in Supabase Storage."
📧 Email Invoices Directly
"Add a 'Send Invoice' button that emails the PDF to the client using Resend (resend.com). Include a professional email template with a 'View Invoice' link and the PDF attached."
💰 Payment Integration
"Add a Stripe payment link to each invoice. When a client opens the invoice, they can click 'Pay Now' to pay with a credit card. Update the invoice status to 'paid' automatically via webhook."
📊 Revenue Dashboard
"Add a dashboard section showing total revenue this month, outstanding invoices, average time to payment, and a simple bar chart of monthly revenue for the past 6 months."
🔁 Recurring Invoices
"Add the ability to mark an invoice as recurring (weekly, monthly, quarterly). Automatically generate new invoices based on the schedule using a Supabase edge function or cron job."
🧾 Expense Tracking
"Add an expenses table and page. Let me log business expenses with category, amount, date, and receipt photo upload. Show a profit/loss summary on the dashboard (revenue from paid invoices minus expenses)."
Each one of these is a single AI prompting session. You're not rebuilding the app — you're extending it. That's the power of having a well-structured codebase as your foundation. If you've done the Build a SaaS App project, you'll recognize the pattern: start simple, ship, iterate.
Frequently Asked Questions
The core app — yes. You'll have a form that creates invoices, calculates totals, generates PDFs, and saves to a database. Getting it polished with your branding, email delivery, and all the edge cases handled takes a weekend. But the "create invoice and download PDF" loop? About an hour with Claude or Cursor.
Not in advance. The AI generates all the React code. It helps to understand what components are (reusable UI pieces) and what state means (data your app remembers), but you don't need to write React from scratch. If something breaks, understanding the basics helps you describe the problem. Check out What Is React? for the essentials.
Three reasons: cost, control, and learning. FreshBooks is $15–55/month. Your version costs $0–20/month to host. You own the code, the data, and the design — no vendor lock-in. And you learn how real apps work (forms, databases, PDF generation), which makes every future AI project easier.
For freelancers and small businesses sending 5–50 invoices a month, absolutely. Line items, tax, PDF export, payment tracking — it's all there. For enterprise needs (multi-currency, complex tax jurisdictions, automated reminders, accounting integrations), you'd add features incrementally. The foundation supports it.
On free tiers: $0/month. Vercel's free tier handles hosting, Supabase's free tier gives you 500MB of storage (thousands of invoices), and PDFs generate in the browser — no extra cost. A custom domain is $12/year. Even past free tiers, you're looking at $20–25/month — still cheaper than one month of FreshBooks.