Skip to main content
RapidDev - Software Development Agency
App Featurespayments-commerce20 min read

How to Add Invoice Generation to Your App (Copy-Paste Prompts Included)

Invoice generation needs a PDF renderer (@react-pdf/renderer on a server-side Edge Function), a structured database schema for invoices and line items, a sequential invoice number that never gaps, and an email delivery layer (Resend). With Lovable or v0 you can ship a working invoice system in 4-8 hours. Running costs start at $0 and scale to $50-75/mo at 10,000 users.

4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members

Feature spec

Intermediate

Category

payments-commerce

Build with AI

4-8 hours with Lovable or v0

Custom build

1-2 weeks custom dev

Running cost

$0/mo up to ~100 users · $25-75/mo at scale

Works on

WebMobile

Everything it takes to ship Invoice Generation — parts, prompts, and real costs.

TL;DR

Invoice generation needs a PDF renderer (@react-pdf/renderer on a server-side Edge Function), a structured database schema for invoices and line items, a sequential invoice number that never gaps, and an email delivery layer (Resend). With Lovable or v0 you can ship a working invoice system in 4-8 hours. Running costs start at $0 and scale to $50-75/mo at 10,000 users.

What Invoice Generation Actually Is

Invoice generation is the ability to produce a branded, legally formatted PDF document summarising a transaction or service agreement — and optionally email it to the customer. It sits at the intersection of your billing data, PDF rendering, file storage, and email delivery. The core challenge is not creating a pretty document; it is ensuring invoice numbers never duplicate, PDFs render consistently across recipients' email clients and PDF viewers, tax is calculated correctly for the applicable jurisdiction, and status (draft / sent / paid / overdue) stays in sync with your payment processor. AI tools can wire all of this in a single prompt if you tell them exactly which libraries and patterns to use.

What users consider table stakes in 2026

  • Instant PDF download from any invoice view — no waiting, no blank screen
  • Branded invoice: company logo, brand colours, legal address, and contact details on every page
  • Unique sequential invoice numbers (e.g. INV-2026-0042) that never skip or repeat
  • Itemised line items with quantity, unit price, subtotal, tax rate, and total
  • Email-invoice button that sends a PDF attachment directly from the app without leaving the page
  • Invoice status tracking (draft, sent, paid, overdue) visible at a glance with colour-coded badges

Anatomy of the Feature

Seven components work together. AI tools handle the form and PDF generation well on the first prompt; the invoice number sequencer and webhook-based status transitions are where most first builds break.

Layers:UIDataBackendService

Invoice builder form

UI

A shadcn/ui Form powered by react-hook-form and zod renders the invoice header (customer name, email, address, issue date, due date, tax rate) and a dynamic line-items table. useFieldArray manages adding and removing rows; quantity × unit_price is computed in real time as users type.

Note: Zod validation ensures no row has a zero quantity or negative price before the form can be submitted.

PDF renderer

Backend

@react-pdf/renderer (React-PDF) runs inside a Supabase Edge Function or Next.js API route. It accepts the invoice object, renders a JSX layout template, and returns a binary PDF buffer. Fonts are embedded as base64 assets so the output is identical everywhere.

Note: Must run server-side. Client-side alternatives like jsPDF cannot embed custom fonts reliably and produce text-as-images in some PDF viewers.

Invoice storage

Data

Supabase Storage bucket (invoices/) holds the rendered PDF file, keyed by a non-guessable UUID path. The invoices and invoice_items PostgreSQL tables hold the structured data — line items, totals, status — that powers the in-app list view and status updates.

Note: The Storage bucket should be private. Surface files to users via signed URLs with a 7-day expiry, not short-lived 60-second defaults.

Invoice number sequencer

Backend

A PostgreSQL SEQUENCE (CREATE SEQUENCE invoice_number_seq) provides atomic, gap-free invoice numbers. The Edge Function calls nextval('invoice_number_seq') inside the INSERT statement so concurrent requests can never receive the same number.

Note: Avoid MAX(invoice_number)+1 in application code — it is not safe under concurrent inserts and will eventually produce duplicates.

Email delivery

Service

The Resend SDK is called from the Edge Function after the PDF is generated. The PDF is passed as a base64-encoded attachment. Resend's free tier covers 3,000 emails/mo with 100/day — sufficient for most early-stage apps.

Note: SendGrid Mail API is a direct alternative. Both must be called from server-side code; attaching a PDF from client-side code would expose the Supabase service_role key.

Tax calculation layer

Backend

For simple apps a percentage-based tax rate field on the invoice is computed server-side (subtotal × tax_rate). For multi-jurisdiction apps, Stripe Tax API auto-detects VAT, GST, and US sales tax rates by customer address and returns the correct rate to apply.

Note: Stripe Tax is included in Stripe Billing at 0.5-0.8% per transaction with no separate monthly fee.

Status state machine

Data

An invoice status enum column tracks the lifecycle: draft → sent → paid → overdue. The paid_at timestamp is set either by a Stripe webhook (payment_intent.succeeded for one-time charges, invoice.paid for subscriptions) or by a manual mark-as-paid button. Overdue detection runs server-side: due_date < now() AND status = 'sent'.

Note: Stripe's webhook event names differ between one-time charges and subscriptions — confirm which you are using before wiring the handler.

The data model

Two tables model the invoice and its line items, with RLS restricting each user to their own records. The Edge Function uses the service_role key to bypass RLS for PDF upload and email dispatch. Paste this into the Supabase SQL editor and run it before prompting your AI tool.

schema.sql
1-- Invoice number sequence (atomic, gap-free)
2create sequence if not exists invoice_number_seq start 1;
3
4-- Main invoices table
5create table public.invoices (
6 id uuid primary key default gen_random_uuid(),
7 user_id uuid references auth.users(id) on delete cascade not null,
8 customer_name text not null,
9 customer_email text not null,
10 customer_address text,
11 invoice_number text not null unique default ('INV-' || to_char(now(), 'YYYY') || '-' || lpad(nextval('invoice_number_seq')::text, 4, '0')),
12 issue_date date not null default current_date,
13 due_date date not null,
14 status text not null default 'draft' check (status in ('draft', 'sent', 'paid', 'overdue')),
15 subtotal numeric(12, 2) not null default 0,
16 tax_rate numeric(5, 4) not null default 0,
17 tax_amount numeric(12, 2) not null default 0,
18 total numeric(12, 2) not null default 0,
19 pdf_url text,
20 paid_at timestamptz,
21 created_at timestamptz not null default now(),
22 updated_at timestamptz not null default now()
23);
24
25-- Line items
26create table public.invoice_items (
27 id uuid primary key default gen_random_uuid(),
28 invoice_id uuid references public.invoices(id) on delete cascade not null,
29 description text not null,
30 quantity numeric(10, 2) not null default 1,
31 unit_price numeric(12, 2) not null default 0,
32 line_total numeric(12, 2) generated always as (quantity * unit_price) stored
33);
34
35-- Row-level security
36alter table public.invoices enable row level security;
37alter table public.invoice_items enable row level security;
38
39create policy "Users manage own invoices"
40 on public.invoices for all
41 using (auth.uid() = user_id)
42 with check (auth.uid() = user_id);
43
44create policy "Users manage own invoice items"
45 on public.invoice_items for all
46 using (
47 exists (
48 select 1 from public.invoices
49 where invoices.id = invoice_items.invoice_id
50 and invoices.user_id = auth.uid()
51 )
52 )
53 with check (
54 exists (
55 select 1 from public.invoices
56 where invoices.id = invoice_items.invoice_id
57 and invoices.user_id = auth.uid()
58 )
59 );
60
61-- Indexes
62create index invoices_user_status_idx on public.invoices (user_id, status);
63create index invoices_user_due_idx on public.invoices (user_id, due_date);
64create index invoice_items_invoice_idx on public.invoice_items (invoice_id);
65
66-- Auto-update updated_at
67create or replace function update_updated_at()
68returns trigger language plpgsql as $$
69begin
70 new.updated_at = now();
71 return new;
72end;
73$$;
74
75create trigger invoices_updated_at
76 before update on public.invoices
77 for each row execute function update_updated_at();

Heads up: The invoice_number default uses nextval() so the sequence increments atomically on INSERT. The line_total column is a generated column computed at the database level — never trust client-supplied totals. Run this SQL in the Supabase SQL editor before wiring the Edge Function.

Build it — pick your path

Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.

Full-stack, non-technical friendlyFit for this feature:

Best all-round path: Lovable connects Supabase Auth, Edge Functions, and Storage in one project. You get the PDF generation, Resend email, and invoice list wired together without manual environment variable juggling.

Step by step

  1. 1Create a new Lovable project and enable Lovable Cloud so Supabase is provisioned automatically
  2. 2Open the Supabase SQL editor via the Cloud tab and run the data model SQL from this page to create the invoices, invoice_items tables, and the invoice_number_seq sequence
  3. 3Paste the prompt below into Agent Mode and let it scaffold the invoice builder form, PDF Edge Function, Storage bucket, and Resend email action
  4. 4Add your Resend API key in the Cloud tab under Secrets as RESEND_API_KEY — the Edge Function reads it via Deno.env.get()
  5. 5Test by creating a draft invoice, clicking Send, and verifying the PDF arrives in your inbox with the correct line items and totals
  6. 6Click Publish and share the HTTPS URL — PDF download requires the live domain, not the preview
Paste into Lovable
Build an invoice generation feature using Supabase as the backend. Use the existing 'invoices' and 'invoice_items' tables (already created in the database). Invoice builder page: a form with react-hook-form and zod for customer name, email, address, issue date, due date, and tax rate percentage. Below the header fields, a dynamic line-items table where each row has description, quantity, and unit price; quantity × unit_price auto-calculates the line total in real time; include add-row and remove-row buttons using useFieldArray. Show subtotal, tax amount (subtotal × tax_rate), and total at the bottom, all computed server-side on save. On save, generate a PDF using @react-pdf/renderer inside a Supabase Edge Function — NOT jsPDF. The PDF should include my company logo (accept a logo_url from the user's profile), invoice number, issue/due dates, line items table, and totals. Upload the PDF to Supabase Storage bucket 'invoices/' using the service_role key and store the signed URL (7-day expiry) in invoices.pdf_url. Add a Download PDF button and a Send Invoice button that calls a Resend Edge Function to email the PDF as an attachment using the RESEND_API_KEY secret. Invoice list page: show all invoices with status badges (draft = gray, sent = blue, paid = green, overdue = red). Mark overdue automatically when due_date < today and status = 'sent'. Include a manual Mark as Paid button. Show an empty state with a 'Create your first invoice' prompt when no invoices exist.

Where this path bites

  • Lovable's AI often defaults to jsPDF (client-side) rather than @react-pdf/renderer (server-side) — the prompt above specifies this explicitly but watch for it in the generated code
  • The Edge Function cold start on first PDF generation can take 2-3 seconds — acceptable for an invoice feature, but warn the user with a loading state
  • Custom fonts in the PDF require embedding them as base64 assets inside the Edge Function directory; Lovable won't do this unless prompted specifically

Third-party services you'll need

Invoice generation itself is free to run. You only pay for email delivery at volume and optionally for jurisdiction-aware tax calculation.

ServiceWhat it doesFree tierPaid from
ResendTransactional email delivery with PDF attachment3,000 emails/mo, 100/dayPro $20/mo: 50,000 emails/mo (approx)
Stripe TaxJurisdiction-aware VAT, GST, and US sales tax rate calculation by customer addressIncluded in Stripe Billing0.5-0.8% per transaction — no separate monthly fee (approx)
SupabasePostgreSQL database, Storage bucket for PDF files, Edge Functions for PDF generation and email dispatch500 MB DB, 1 GB StoragePro $25/mo: 8 GB DB, 100 GB Storage (approx)
@react-pdf/rendererServer-side PDF generation from JSX templates with embedded fontsOpen source, freeNo cost beyond compute

Swipe the table sideways to see pricing.

What it costs to run

Drag through the tiers to see how your monthly bill scales with users — no surprises later.

Estimated monthly running cost

$0/mo

Supabase free tier handles the DB and Storage. Resend free tier covers typical invoice email volume. No Stripe Tax cost unless live billing is active.

Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.

What breaks when AI tools build this

The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.

Client-side PDF has broken fonts

Symptom: AI tools default to jsPDF because it is the most-cited PDF library in training data. jsPDF cannot embed custom fonts — it renders non-ASCII characters as empty boxes or falls back to a generic typeface, making branded invoices look unprofessional or unreadable in some PDF viewers.

Fix: Explicitly request @react-pdf/renderer running inside a Supabase Edge Function or Next.js API route. Specify the font family (e.g. Inter) and instruct the AI to include the font as a base64-encoded asset co-located with the function. Verify by opening the generated PDF in Adobe Acrobat, not just Chrome.

Duplicate invoice numbers under concurrent inserts

Symptom: AI-generated code commonly reads MAX(invoice_number) + 1 inside the application layer before inserting the row. Two users (or two tabs) submitting invoices within milliseconds of each other both read the same MAX and produce the same number — a legal and accounting problem.

Fix: Use a PostgreSQL SEQUENCE and call nextval('invoice_number_seq') directly in the INSERT statement. The sequence is atomic and lock-free — concurrent inserts always get different values. The data model SQL on this page sets this up correctly.

PDF signed URL expires before the customer opens the email

Symptom: Supabase Storage signed URLs default to 60 seconds. The Edge Function generates the URL, emails it, and by the time the recipient opens the email (minutes or hours later) the link returns a 403 error.

Fix: Generate the signed URL with a 7-day expiry (604800 seconds) at email-send time. Alternatively, store the PDF in a private bucket using a non-guessable UUID path and generate a fresh signed URL on demand when the user clicks Download in your app.

Stripe webhook sets wrong invoice status

Symptom: AI often wires the invoice.payment_succeeded webhook event, which does not exist. The correct event for one-time Stripe charges is payment_intent.succeeded; for Stripe Billing subscriptions it is invoice.paid. Using the wrong event name means the webhook never fires and invoices stay in 'sent' status permanently.

Fix: Check your Stripe product type before prompting: one-time charges use payment_intent.succeeded, subscription invoices use invoice.paid. Verify in the Stripe Dashboard under Webhooks → Test events before going live.

RLS blocks invoice PDF upload

Symptom: Edge Functions default to using the anon key unless explicitly told otherwise. The anon key is subject to RLS policies on both the Storage bucket and the database. If the bucket's RLS policy requires the caller to be an authenticated user matching the invoice owner, the service-account upload fails with a 403.

Fix: Store SUPABASE_SERVICE_ROLE_KEY in Lovable Secrets (or Vercel environment variables) and use it only inside the Edge Function to create the Supabase client for Storage uploads and invoice row updates. Never expose the service_role key in client-side code.

Best practices

1

Always generate the invoice PDF server-side using @react-pdf/renderer — never trust client-side libraries for documents that carry legal weight

2

Use a PostgreSQL SEQUENCE for invoice numbers so they are guaranteed atomic and gap-free, regardless of traffic volume

3

Store PDFs in a private Supabase Storage bucket and serve them via signed URLs with a meaningful expiry (7 days minimum for emailed links)

4

Compute subtotal, tax, and total on the server at save time and store the results — never recompute from line items at display time, as line item prices or tax rates may have changed since the invoice was issued

5

Mark invoices overdue server-side (a nightly function or on-read check comparing due_date against now()) rather than relying on client timezone

6

Validate that the Stripe webhook event name matches your product type (one-time vs subscription) before going live — this is the most common silent failure

7

Show a clear empty state with a 'Create your first invoice' prompt and a single CTA — first-time users land here and need immediate guidance

8

Cache your company logo and branded assets inside the Edge Function directory rather than fetching them from an external URL on every PDF generation request

When You Need Custom Development

AI tools deliver a complete invoice generation feature — PDF, email, sequential numbers, status tracking — without custom development. The cases that genuinely require it are driven by legal mandates or enterprise-scale multi-tenancy:

  • Your target markets require legally-mandated e-invoice formats: ZUGFeRD (Germany), Factur-X (France), CFDI (Mexico), or FatturaPA (Italy) — these embed structured XML inside the PDF and require digital signatures that @react-pdf/renderer cannot produce alone
  • You need multi-currency invoices with real-time FX rates and automatic currency conversion at payment time, with audit trails per regulatory requirements
  • Two-way sync with an accounting platform (QuickBooks Online API, Xero API, FreshBooks API) is a hard requirement so invoices appear in the ledger automatically and payment status flows back without manual entry
  • You are building white-label multi-tenant invoicing where each tenant has their own logo, number sequence, tax configuration, and invoice template — requiring tenant isolation at the database and Storage level

RapidDev builds these features for production

Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.

Get a fixed-price quote

Frequently asked questions

Can I generate invoices as PDFs automatically when a payment is made?

Yes. Wire a Stripe webhook to your backend: when payment_intent.succeeded fires (one-time charge) or invoice.paid fires (subscription), call your PDF generation Edge Function, upload the result to Supabase Storage, and email the link to the customer via Resend. The entire flow runs without any manual trigger.

What is the best library for generating PDF invoices in a Next.js app?

@react-pdf/renderer (React-PDF) is the correct choice for server-side PDF generation in a Next.js API route or Supabase Edge Function. It lets you write your invoice layout as JSX, supports custom font embedding, and produces consistent output across all PDF viewers. Avoid jsPDF for invoices — it runs client-side and cannot embed fonts reliably.

How do I add my company logo and branding to invoices?

Store your logo as a base64-encoded PNG inside the Edge Function directory and reference it in the @react-pdf/renderer Image component. Fetch it once at function boot time, not on every request. For custom fonts, download the .ttf file, convert it to base64, and register it with Font.register() before rendering. This ensures the logo and fonts are always available without a network dependency.

How do I send invoices by email directly from my app?

Use the Resend SDK inside a Supabase Edge Function or Next.js API route. After the PDF is generated, read the file from Supabase Storage, encode it as base64, and pass it to resend.emails.send() as an attachment. Resend's free tier covers 3,000 emails/month, which is sufficient for most early-stage apps. Store your RESEND_API_KEY in Supabase Secrets or Vercel environment variables — never in client-side code.

How do I handle VAT or sales tax on invoices?

For a single-jurisdiction app a simple tax_rate percentage field on the invoice is sufficient — compute tax_amount = subtotal × tax_rate on the server at save time. For multi-jurisdiction businesses (EU VAT, US sales tax by state, Australian GST) integrate Stripe Tax API, which auto-detects the applicable rate by customer address. Stripe Tax is included in Stripe Billing at 0.5-0.8% per transaction with no additional monthly fee.

How do I create sequential invoice numbers that never have gaps?

Create a PostgreSQL SEQUENCE (CREATE SEQUENCE invoice_number_seq) and call nextval('invoice_number_seq') directly in your INSERT statement as part of the invoice_number default value. This is atomic — the database guarantees each INSERT gets a unique, incrementing value even under concurrent load. Never use MAX(invoice_number)+1 in application code; two simultaneous requests will read the same MAX and produce a duplicate.

Can I use Stripe to automatically mark invoices as paid?

Yes. Register a Stripe webhook endpoint in your Stripe Dashboard pointing to your backend. For one-time charges listen for payment_intent.succeeded; for subscription billing listen for invoice.paid. In your webhook handler, look up the invoice by the Stripe payment reference, set status to 'paid', and record paid_at. Test the webhook using Stripe's built-in test event tool before going live.

What is the difference between Stripe Invoicing and building a custom invoice system?

Stripe Invoicing creates and sends invoices entirely within Stripe — it is fast to set up but produces Stripe-branded documents with limited layout customisation. Building your own system gives you full control over branding, invoice layout, numbering format, storage, and email template. It also keeps invoice data in your own database for accounting exports and custom reporting. Most SaaS apps start with Stripe Invoicing for simplicity and migrate to a custom system when branding or accounting integration requirements appear.

RapidDev

Need this feature production-ready?

RapidDev builds invoice generation into real apps — auth, database, payments — at $13K–$25K.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

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.