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

- Tool: App Features
- Last updated: July 2026

## 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.

## 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.

- **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.
- **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.
- **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.
- **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.
- **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.
- **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.
- **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'.

## 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.

```sql
-- Invoice number sequence (atomic, gap-free)
create sequence if not exists invoice_number_seq start 1;

-- Main invoices table
create table public.invoices (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  customer_name text not null,
  customer_email text not null,
  customer_address text,
  invoice_number text not null unique default ('INV-' || to_char(now(), 'YYYY') || '-' || lpad(nextval('invoice_number_seq')::text, 4, '0')),
  issue_date date not null default current_date,
  due_date date not null,
  status text not null default 'draft' check (status in ('draft', 'sent', 'paid', 'overdue')),
  subtotal numeric(12, 2) not null default 0,
  tax_rate numeric(5, 4) not null default 0,
  tax_amount numeric(12, 2) not null default 0,
  total numeric(12, 2) not null default 0,
  pdf_url text,
  paid_at timestamptz,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

-- Line items
create table public.invoice_items (
  id uuid primary key default gen_random_uuid(),
  invoice_id uuid references public.invoices(id) on delete cascade not null,
  description text not null,
  quantity numeric(10, 2) not null default 1,
  unit_price numeric(12, 2) not null default 0,
  line_total numeric(12, 2) generated always as (quantity * unit_price) stored
);

-- Row-level security
alter table public.invoices enable row level security;
alter table public.invoice_items enable row level security;

create policy "Users manage own invoices"
  on public.invoices for all
  using (auth.uid() = user_id)
  with check (auth.uid() = user_id);

create policy "Users manage own invoice items"
  on public.invoice_items for all
  using (
    exists (
      select 1 from public.invoices
      where invoices.id = invoice_items.invoice_id
        and invoices.user_id = auth.uid()
    )
  )
  with check (
    exists (
      select 1 from public.invoices
      where invoices.id = invoice_items.invoice_id
        and invoices.user_id = auth.uid()
    )
  );

-- Indexes
create index invoices_user_status_idx on public.invoices (user_id, status);
create index invoices_user_due_idx on public.invoices (user_id, due_date);
create index invoice_items_invoice_idx on public.invoice_items (invoice_id);

-- Auto-update updated_at
create or replace function update_updated_at()
returns trigger language plpgsql as $$
begin
  new.updated_at = now();
  return new;
end;
$$;

create trigger invoices_updated_at
  before update on public.invoices
  for each row execute function update_updated_at();
```

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 paths

### Lovable — fit 4/10, 4-6 hours

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.

1. Create a new Lovable project and enable Lovable Cloud so Supabase is provisioned automatically
2. Open 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. Paste the prompt below into Agent Mode and let it scaffold the invoice builder form, PDF Edge Function, Storage bucket, and Resend email action
4. Add your Resend API key in the Cloud tab under Secrets as RESEND_API_KEY — the Edge Function reads it via Deno.env.get()
5. Test by creating a draft invoice, clicking Send, and verifying the PDF arrives in your inbox with the correct line items and totals
6. Click Publish and share the HTTPS URL — PDF download requires the live domain, not the preview

Starter prompt:

```
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.
```

Limitations:

- 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

### V0 — fit 4/10, 5-8 hours

Excellent for the invoice builder UI and Next.js API routes for PDF generation. shadcn/ui Table renders line items cleanly and the generated TypeScript types are easy to extend.

1. Prompt v0 with the spec below to generate the invoice builder component, invoice list page, and API routes for PDF generation and email
2. Add Supabase and Resend environment variables in the v0 Vars panel: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, RESEND_API_KEY
3. Run the data model SQL from this page in your Supabase SQL editor to create the required tables and sequence
4. Publish to production on Vercel and test the PDF generation and email flow from the live URL
5. Wire the Stripe webhook for payment_intent.succeeded in the Vercel dashboard to auto-mark invoices as paid

Starter prompt:

```
Build a Next.js invoice generation system using Supabase, @react-pdf/renderer, and Resend. App Router with TypeScript and shadcn/ui. Use the 'invoices' and 'invoice_items' Supabase tables. Page 1: /invoices — server component listing all invoices for the signed-in user with status badges (draft/sent/paid/overdue), a Download PDF button, a Send Email button, and a Mark as Paid button. Empty state when no invoices exist. Page 2: /invoices/new — client component with a react-hook-form + zod invoice builder. Header fields: customer_name, customer_email, customer_address, issue_date, due_date, tax_rate (percentage). Line items section using useFieldArray: each row has description, quantity, unit_price; display computed line_total (quantity × unit_price) read-only; add/remove row buttons. Footer shows subtotal, tax amount, and total, all recomputed on every keystroke. API Route /api/invoices/pdf: accepts invoice_id, fetches invoice + items from Supabase using service_role key, renders a branded PDF with @react-pdf/renderer using the Inter font (embed as base64), uploads to Supabase Storage bucket 'invoices/' with a 7-day signed URL, saves pdf_url back to the invoice row, returns the signed URL. API Route /api/invoices/send: calls Resend with the PDF as a base64 attachment, updates status to 'sent'. Mark overdue server-side: status = 'sent' AND due_date < today. Handle concurrent inserts safely — invoice_number is set by a PostgreSQL sequence on the database side, do not generate it in application code.
```

Limitations:

- v0 does not auto-provision Supabase — you must run the SQL schema and set environment variables manually
- PDF generation in a Vercel serverless function is subject to the 50 MB bundle size limit — @react-pdf/renderer plus font assets can approach this; consider using a Supabase Edge Function for the PDF route instead
- Occasional shadcn registry mismatches in v0 require a follow-up prompt to fix broken imports

### Flutterflow — fit 3/10, 1-2 days

Suitable for building a mobile invoice viewer and status tracker backed by Supabase. PDF generation must be offloaded to a Supabase Edge Function since FlutterFlow has no native PDF renderer.

1. Connect your Supabase project in FlutterFlow under Settings → Supabase and import the invoices and invoice_items tables
2. Build an Invoice List page using a ListView widget bound to a Supabase query on the invoices table filtered by auth.uid()
3. Add an Invoice Detail page showing the line items in a DataTable widget and status in a styled Text/Badge widget
4. Add a Download PDF button: in the Action editor, choose Call API and point it to your Supabase Edge Function /functions/v1/generate-invoice-pdf passing the invoice_id; the function returns a signed URL
5. Open the signed URL in the device browser using the Launch URL action — FlutterFlow cannot render PDFs natively in-app

Limitations:

- PDF generation requires a separately deployed Supabase Edge Function — FlutterFlow itself cannot render PDFs
- The invoice builder form with dynamic add/remove line items requires careful use of Page State variables and repeated widgets; complex row calculations may need a Custom Action written in Dart
- Email sending must also be triggered from the Edge Function — FlutterFlow cannot call Resend or attach binary files directly

### Custom — fit 2/10, 1-2 weeks

Only justified for white-label multi-tenant invoicing, legally-mandated e-invoice formats, or deep two-way sync with accounting software.

1. Multi-tenant invoice number sequences: separate PostgreSQL schemas or sequence-per-tenant isolation so tenant A's INV-0001 never collides with tenant B's
2. Country-specific e-invoice formats: ZUGFeRD (Germany), Factur-X (France), CFDI (Mexico), FatturaPA (Italy) require XML embedding inside the PDF and digital signatures — @react-pdf/renderer alone is insufficient
3. Accounting integration: two-way sync with QuickBooks Online API, Xero API, or FreshBooks API so invoices appear in the ledger and payment status syncs back automatically
4. Custom PDF templates: white-label installs where each tenant uploads their own logo, colour palette, and footer text that renders correctly across fonts and paper sizes

Limitations:

- The base invoice feature (PDF, email, status tracking) does not justify a custom build — AI tools deliver it faster and cheaper
- Custom builds become necessary only when legal compliance, multi-tenancy, or accounting integrations are non-negotiable requirements

## Gotchas

- **Client-side PDF has broken fonts** — 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** — 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** — 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** — 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** — 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

- Always generate the invoice PDF server-side using @react-pdf/renderer — never trust client-side libraries for documents that carry legal weight
- Use a PostgreSQL SEQUENCE for invoice numbers so they are guaranteed atomic and gap-free, regardless of traffic volume
- Store PDFs in a private Supabase Storage bucket and serve them via signed URLs with a meaningful expiry (7 days minimum for emailed links)
- 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
- Mark invoices overdue server-side (a nightly function or on-read check comparing due_date against now()) rather than relying on client timezone
- 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
- 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
- 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

## 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.

---

Source: https://www.rapidevelopers.com/app-features/invoice-generation
© RapidDev — https://www.rapidevelopers.com/app-features/invoice-generation
