TL;DR: You'll build a full invoice generator by prompting your AI coding tool. The app lets you create invoices with line items, automatically calculates subtotals and tax, generates professional-looking PDFs, and stores everything in a Supabase database. Total cost to run: $0/month on free tiers. Total time: about an hour for the core app, a weekend to polish it.

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.

The fix: Add '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.

The fix: Tell the AI: "Use integer cents for all currency calculations. Store prices as cents (integers) in the database and only convert to dollars for display using (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.

The fix: Tell the AI: "Generate invoice numbers using the format INV-YYYYMMDD-XXXX where XXXX is a random 4-character alphanumeric string. Check for uniqueness before saving." This is simple, readable, and practically collision-proof for small-volume use.

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.

The fix: For a single-user app, you can disable RLS on the tables (Supabase dashboard → Table → disable RLS). For multi-user, tell the AI: "Add RLS policies so users can only read and write their own invoices. Add a user_id column to the invoices table that references auth.users."

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.

The fix: After the app works, do a dedicated design pass. Tell the AI: "Redesign the PDF template to look more professional. Add more whitespace, use a bold sans-serif font for the invoice title, add a subtle accent color for the header, make the totals section stand out with a background, and increase the font size for the grand total." Show it an example invoice from FreshBooks or Stripe if you have one.

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.