Courses/Invoice Generator Desktop App
Standalone Course~30 min14 challenges

Build an Invoice Generator: Desktop App with Wails

In this course, you will build a real, useful desktop application — an invoice generator. Create clients, generate invoices with line items, export professional PDF invoices, and track payment status. Everything runs offline with SQLite, no internet required.


What We're Building

An invoice generator is one of the most practical apps a freelancer or small business can use. Instead of paying $20/month for a SaaS tool, you build your own — and it runs entirely on your computer with no subscription:

  • Client management — store client names, emails, addresses, companies
  • Invoice creation — auto-numbered invoices with line items, tax calculation
  • PDF export — professional invoices ready to email to clients
  • Payment tracking — Draft, Sent, Paid, Overdue status workflow
  • Excel export — export all invoices for accounting and tax filing
  • Dashboard — total revenue, outstanding balance, overdue count

Everything is stored locally in SQLite. No server, no cloud, no account needed. Open the app and start invoicing.

1

Challenge: List Invoice Features

List 5 features an invoice generator needs beyond what's listed above. Think about: recurring invoices, multiple currencies, late fees, payment reminders, client portal. Which would you prioritize?

Scaffold the App

Use Grit's desktop scaffold command to create the project:

Terminal
grit new-desktop invoicer

This creates a Wails v2 project with Go backend, React + TypeScript frontend, local auth, SQLite database, and export capabilities (PDF + Excel). Start the development server:

Terminal
cd invoicer && grit start

The app opens as a native window. You should see the login page — create an account and explore the default dashboard. This is your starting point.

2

Challenge: Scaffold and Run

Scaffold the project and run grit start. Create an account and log in. Explore the default pages — dashboard, blog, contacts. These are the starter templates you will customize into an invoice app.

Design the Data Model

The invoice app needs three resources. Notice how they relate to each other: a Client has many Invoices, and an Invoice has many LineItems.

Terminal
# Resource 1: Client
grit generate resource Client \
  name:string email:string phone:string:optional \
  address:text:optional company:string:optional

# Resource 2: Invoice
grit generate resource Invoice \
  invoice_number:string:unique \
  client_id:belongs_to:Client \
  issue_date:date due_date:date status:string \
  subtotal:float tax:float total:float \
  notes:text:optional

# Resource 3: LineItem
grit generate resource LineItem \
  description:string quantity:int \
  unit_price:float total:float \
  invoice_id:belongs_to:Invoice

The relationships work like this:

  • Client — standalone entity. Has name, email, optional phone/address/company.
  • Invoice — belongs to a Client. Has dates, status, financial totals.
  • LineItem — belongs to an Invoice. Each line is one product/service with qty and price.
3

Challenge: Generate All 3 Resources

Generate all three resources. Check the Go models — are the belongs_to foreign keys correct? Can you see the relationships in the model structs?

Auto-Generate Invoice Numbers

Every invoice needs a unique, sequential number. Instead of making the user type one, auto-generate it. Add this method to the Invoice service:

internal/services/invoice_service.go
func (s *InvoiceService) NextNumber() string {
    var count int64
    s.db.Model(&models.Invoice{}).Count(&count)
    return fmt.Sprintf("INV-%03d", count+1)
}

This counts existing invoices and generates the next number: INV-001, INV-002, INV-003, and so on. The %03d format pads with leading zeros up to 3 digits.

Call this method when creating a new invoice. The Wails binding exposes it to the frontend, so you can pre-fill the invoice number field automatically.

For production apps with concurrent users, you'd want a database sequence or a transaction lock to avoid duplicate numbers. For a single-user desktop app, counting rows is perfectly safe.
4

Challenge: Add Auto-Numbering

Add the NextNumber() method to your invoice service. Create 3 invoices — are they numbered INV-001, INV-002, INV-003? Delete the second one and create another — what number does it get?

Build the Client List

The client list is your first custom page. It shows all clients in a searchable, sortable DataTable — the same component Grit's desktop scaffold provides:

Frontend — Client List Page
// The DataTable component handles:
// - Column sorting (click headers)
// - Search/filter
// - Pagination
// - Row selection
// - Actions (edit, delete)

const columns = [
  { key: 'name', label: 'Name' },
  { key: 'company', label: 'Company' },
  { key: 'email', label: 'Email' },
  { key: 'phone', label: 'Phone' },
]

// Fetch clients from the Go backend via Wails binding
const clients = await GetAllClients()

The client list is the foundation. Every invoice needs a client, so build this page first. Add a "New Client" button that opens the FormBuilder with fields for name, email, company, phone, and address.

5

Challenge: Create 5 Clients

Build the client list page and the create client form. Add 5 clients with different companies. Can you search by name? Sort by company? Edit a client's email?

Build the Invoice Form

The invoice form is the most complex page in the app. It has multiple steps and dynamic line items that calculate totals automatically:

Step 1: Invoice Details

  • Select client from dropdown (populated from your client list)
  • Invoice number (auto-filled, but editable)
  • Issue date (defaults to today)
  • Due date (defaults to 30 days from today)

Step 2: Line Items

  • Dynamic rows: description, quantity, unit price
  • Line total auto-calculated: quantity x unit price
  • "Add Line Item" button to add more rows
  • "Remove" button on each row

Step 3: Review

  • Subtotal (sum of all line totals)
  • Tax rate input (percentage, e.g., 10%)
  • Tax amount (subtotal x tax rate)
  • Grand total (subtotal + tax)
  • Optional notes field
Line Item Calculation Logic
// Each line item: total = quantity * unitPrice
const lineTotal = quantity * unitPrice

// Invoice totals
const subtotal = lineItems.reduce((sum, item) => sum + item.total, 0)
const taxAmount = subtotal * (taxRate / 100)
const grandTotal = subtotal + taxAmount
6

Challenge: Create an Invoice with 4 Line Items

Build the invoice form. Create an invoice with 4 line items (e.g., "Web Design — 40 hours at $75", "Logo Design — 1 at $500", etc.). Verify: Are the line totals correct? Is the subtotal the sum of all lines? Does the tax calculate properly?

Invoice Detail View

The invoice detail page shows a complete, formatted view of an invoice. This is what the user sees when they click on an invoice from the list:

  • Header — invoice number, status badge, issue date, due date
  • Client info — name, company, email, address
  • Line items table — description, quantity, unit price, line total
  • Totals section — subtotal, tax, grand total
  • Status badge — color-coded: Draft (gray), Sent (blue), Paid (green), Overdue (red)
  • Action buttons — Export PDF, Mark as Sent, Mark as Paid, Edit, Delete
Status Badge Colors
const statusColors = {
  draft:   'bg-gray-500/10 text-gray-400 border-gray-500/20',
  sent:    'bg-blue-500/10 text-blue-400 border-blue-500/20',
  paid:    'bg-green-500/10 text-green-400 border-green-500/20',
  overdue: 'bg-red-500/10 text-red-400 border-red-500/20',
}
7

Challenge: View an Invoice

Build the invoice detail page. Navigate to one of your invoices. Is all the information displayed correctly — client name, line items, totals? Does the status badge use the right color?

PDF Export

The killer feature of an invoice app: generating a professional PDF that you can email to clients. Grit's desktop scaffold includes an export service that generates PDFs from Go:

The PDF should include:

  • Company header — your business name, address, logo (optional)
  • Invoice number and dates — prominently displayed
  • Bill To section — client name, company, address
  • Line items table — with headers, alternating row colors
  • Totals — subtotal, tax, grand total (bold)
  • Payment terms"Payment due within 30 days"
  • Notes — any additional notes from the invoice
Export Button (Frontend)
// Call the Go export function via Wails binding
const handleExportPDF = async () => {
  try {
    const filePath = await ExportInvoicePDF(invoice.id)
    // filePath is where the PDF was saved
    alert("PDF saved to: " + filePath)
  } catch (error) {
    console.error("Export failed:", error)
  }
}
The Go backend handles PDF generation using a library like gofpdf or go-wkhtmltopdf. The Wails binding makes it callable from the React frontend as a simple async function. No REST API needed — it's a direct function call.
8

Challenge: Export a PDF Invoice

Add a "Export PDF" button to the invoice detail page. Generate a PDF for one of your invoices. Open the PDF — does it look professional? Is all the data correct: client info, line items, totals?

Payment Status Workflow

Invoices follow a status workflow. Each transition represents a real business action:

Status Workflow
Draft → Sent → Paid
  ↓              ↑
  └──→ Overdue ──┘

Draft:   Invoice created but not sent to client
Sent:    Invoice emailed/delivered to client
Paid:    Client has paid the invoice
Overdue: Past due date and not yet paid

The status transitions should be explicit actions — buttons on the invoice detail page:

  • "Mark as Sent" — visible when status is Draft
  • "Mark as Paid" — visible when status is Sent or Overdue
  • Auto-Overdue — a background check: if status is Sent and due_date has passed, mark as Overdue
internal/services/invoice_service.go
func (s *InvoiceService) UpdateStatus(id uint, status string) error {
    validTransitions := map[string][]string{
        "draft":   {"sent"},
        "sent":    {"paid", "overdue"},
        "overdue": {"paid"},
    }

    var invoice models.Invoice
    if err := s.db.First(&invoice, id).Error; err != nil {
        return err
    }

    allowed := validTransitions[invoice.Status]
    for _, s := range allowed {
        if s == status {
            return s.db.Model(&invoice).Update("status", status).Error
        }
    }
    return fmt.Errorf("cannot transition from %s to %s", invoice.Status, status)
}
9

Challenge: Test the Status Workflow

Create an invoice. It should start as Draft. Mark it as Sent — does the badge change to blue? Mark it as Paid — does the badge change to green? Try marking a Paid invoice as Draft — does the validation prevent it?

Dashboard

The dashboard gives you a financial overview at a glance:

  • Total Revenue — sum of all Paid invoices
  • Outstanding — sum of Sent invoices (money owed to you)
  • Overdue — count and total of overdue invoices (needs attention)
  • Recent Invoices — last 5 invoices with status, client, and amount
Go Service — Dashboard Stats
func (s *InvoiceService) DashboardStats() (*DashboardStats, error) {
    var stats DashboardStats

    // Total revenue (paid invoices)
    s.db.Model(&models.Invoice{}).
        Where("status = ?", "paid").
        Select("COALESCE(SUM(total), 0)").
        Scan(&stats.TotalRevenue)

    // Outstanding (sent invoices)
    s.db.Model(&models.Invoice{}).
        Where("status = ?", "sent").
        Select("COALESCE(SUM(total), 0)").
        Scan(&stats.Outstanding)

    // Overdue count
    s.db.Model(&models.Invoice{}).
        Where("status = ?", "overdue").
        Count(&stats.OverdueCount)

    return &stats, nil
}
10

Challenge: Verify Dashboard Accuracy

Create 10+ invoices across all statuses (3 Draft, 3 Sent, 2 Paid, 2 Overdue). Check the dashboard — does Total Revenue only count Paid invoices? Does Outstanding only count Sent? Is the Overdue count correct?

Excel Export

For accounting and tax purposes, you need to export all invoices to a spreadsheet. Grit's desktop scaffold includes Excel export via the excelize Go library:

Excel Export Columns
Invoice Number | Client | Issue Date | Due Date | Status | Subtotal | Tax | Total
INV-001        | Acme   | 2026-01-15 | 2026-02-14 | Paid | 3000.00 | 300.00 | 3300.00
INV-002        | Globex | 2026-01-20 | 2026-02-19 | Sent | 1500.00 | 150.00 | 1650.00
...

The export includes:

  • All invoices with key columns (number, client, dates, status, amounts)
  • Summary row at the bottom with total revenue and total outstanding
  • Formatted headers — bold, with column width auto-fit
11

Challenge: Export to Excel

Add an "Export All to Excel" button on the invoices list page. Export your invoices. Open the file in Excel, Google Sheets, or LibreOffice Calc. Are all the columns present? Is the data accurate?

Build for Production

When your invoice app is complete, compile it into a standalone native binary:

Terminal
grit compile

This produces a single executable file that includes:

  • The Go backend (compiled to native machine code)
  • The React frontend (bundled, minified, and embedded)
  • SQLite (embedded database, no external server needed)

The end user just double-clicks the file. No installation, no dependencies, no internet required. The SQLite database is created automatically on first launch in the app's data directory.

The compiled binary is typically 20-40 MB. You can reduce it with -ldflags "-s -w" to strip debug symbols, bringing it down to 15-25 MB. That's your entire app — backend, frontend, and database engine — in a single file.
12

Challenge: Build and Test the Binary

Run grit compile. Find the output binary. Run it outside of dev mode — does the app open? Create an invoice, export it as PDF. Does everything work the same as in development?

What You Learned

  • How to scaffold a desktop app with grit new-desktop
  • Designing related data models (Client, Invoice, LineItem)
  • Auto-generating sequential invoice numbers
  • Building a multi-step form with dynamic line items
  • Implementing a status workflow with valid transitions
  • Exporting professional PDF invoices
  • Building a financial dashboard with aggregated stats
  • Exporting data to Excel for accounting
  • Compiling into a standalone native binary
13

Challenge: Complete Invoicing Workflow

Build the complete workflow:

  1. Create 3 clients (different companies)
  2. Create 10 invoices: 2 Draft, 3 Sent, 3 Paid, 2 Overdue
  3. Each invoice should have 2-5 line items
  4. Export 2 invoices as PDF
  5. Export all invoices to Excel
  6. Check the dashboard — are the stats accurate?
14

Challenge: Share Your App

Compile the app with grit compile. Send the binary to a friend or test on a different computer. Does it run without installing Go, Node, or Wails? Does the SQLite database get created automatically on first launch? Create an invoice on the new machine and export it as PDF.