# How to Build Finance tracker with V0

- Tool: v0
- Difficulty: Intermediate
- Compatibility: V0 Premium or higher
- Last updated: April 2026

## TL;DR

Build a personal finance tracker with V0 using Next.js, Supabase for transaction data, and Recharts for spending analytics. You'll create income and expense logging, category budgets with progress bars, multi-account balances, and monthly trend reports — all in about 1-2 hours without touching a terminal.

## Before you start

- A V0 account (Premium plan for chart iterations)
- A Supabase project (free tier works — connect via V0's Connect panel)
- A list of income and expense categories for your use case
- Your account types (checking, savings, credit card, cash)

## Step-by-step guide

### 1. Set up the project and financial database schema

Open V0 and create a new project. Connect Supabase via the Connect panel. Create the schema for accounts, categories, transactions, and budgets with a trigger for automatic balance sync.

```
// Paste this prompt into V0's AI chat:
// Build a finance tracker. Create a Supabase schema with:
// 1. accounts: id (uuid PK), user_id (uuid FK to auth.users), name (text), type (text check in 'checking','savings','credit_card','cash'), balance_cents (bigint default 0), currency (text default 'usd'), created_at (timestamptz)
// 2. categories: id (uuid PK), user_id (uuid FK to auth.users), name (text), type (text check in 'income','expense'), icon (text), color (text), budget_cents (int)
// 3. transactions: id (uuid PK), user_id (uuid FK to auth.users), account_id (uuid FK to accounts), category_id (uuid FK to categories), amount_cents (bigint), type (text check in 'income','expense','transfer'), description (text), date (date), notes (text), is_recurring (boolean default false), created_at (timestamptz)
// 4. budgets: id (uuid PK), user_id (uuid FK to auth.users), category_id (uuid FK to categories), month (date), limit_cents (int), spent_cents (int default 0), unique(user_id, category_id, month))
// Create a PostgreSQL trigger function on_transaction_change() that fires after INSERT/UPDATE/DELETE on transactions to atomically adjust accounts.balance_cents and budgets.spent_cents.
// Add RLS with auth.uid() = user_id on all tables.
```

> Pro tip: The trigger function handles INSERT, UPDATE, and DELETE cases, adjusting balances atomically so accounts and budgets never drift out of sync with transactions.

**Expected result:** Database schema created with automatic balance and budget sync via PostgreSQL triggers.

### 2. Build the finance dashboard

Create the main dashboard showing account balances, monthly income vs expenses, and recent transactions. This is a Server Component for zero client-side JS on initial load.

```
import { createClient } from '@/lib/supabase/server'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import Link from 'next/link'

export default async function DashboardPage() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  const now = new Date()
  const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1).toISOString()

  const { data: accounts } = await supabase
    .from('accounts').select('*').eq('user_id', user?.id)

  const { data: monthTx } = await supabase
    .from('transactions').select('amount_cents, type')
    .eq('user_id', user?.id).gte('date', startOfMonth.split('T')[0])

  const { data: recent } = await supabase
    .from('transactions').select('*, categories(name, icon, color)')
    .eq('user_id', user?.id).order('date', { ascending: false }).limit(10)

  const netWorth = accounts?.reduce((s, a) => s + Number(a.balance_cents), 0) ?? 0
  const income = monthTx?.filter(t => t.type === 'income').reduce((s, t) => s + Number(t.amount_cents), 0) ?? 0
  const expenses = monthTx?.filter(t => t.type === 'expense').reduce((s, t) => s + Number(t.amount_cents), 0) ?? 0

  return (
    <div className="container mx-auto py-8">
      <h1 className="text-3xl font-bold mb-6">Finance Dashboard</h1>
      <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
        <Card><CardHeader><CardTitle className="text-sm">Net Worth</CardTitle></CardHeader>
          <CardContent><p className="text-2xl font-bold">${(netWorth / 100).toLocaleString()}</p></CardContent></Card>
        <Card><CardHeader><CardTitle className="text-sm">Income This Month</CardTitle></CardHeader>
          <CardContent><p className="text-2xl font-bold text-green-600">+${(income / 100).toLocaleString()}</p></CardContent></Card>
        <Card><CardHeader><CardTitle className="text-sm">Expenses This Month</CardTitle></CardHeader>
          <CardContent><p className="text-2xl font-bold text-red-600">-${(expenses / 100).toLocaleString()}</p></CardContent></Card>
      </div>
      <Table>
        <TableHeader><TableRow>
          <TableHead>Date</TableHead><TableHead>Description</TableHead>
          <TableHead>Category</TableHead><TableHead>Amount</TableHead>
        </TableRow></TableHeader>
        <TableBody>
          {recent?.map((tx) => (
            <TableRow key={tx.id}>
              <TableCell>{new Date(tx.date).toLocaleDateString()}</TableCell>
              <TableCell>{tx.description}</TableCell>
              <TableCell><Badge style={{ backgroundColor: tx.categories?.color }}>{tx.categories?.icon} {tx.categories?.name}</Badge></TableCell>
              <TableCell className={tx.type === 'income' ? 'text-green-600' : 'text-red-600'}>
                {tx.type === 'income' ? '+' : '-'}${(Number(tx.amount_cents) / 100).toFixed(2)}
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  )
}
```

**Expected result:** The dashboard shows net worth, monthly income/expenses, and recent transactions with color-coded categories.

### 3. Build the transaction form with validation

Create the add transaction dialog with amount input, category and account selection, date picker, and Zod validation through a Server Action.

```
// Paste this prompt into V0's AI chat:
// Build a transaction form as a 'use client' component.
// Requirements:
// - A Dialog triggered by a floating "+ Add Transaction" Button
// - Tabs to switch between Income and Expense (sets the type)
// - Input for amount with dollar sign prefix
// - Select for category (filtered by income/expense type)
// - Select for account
// - Calendar DatePicker for transaction date
// - Input for description (required)
// - Textarea for optional notes
// - Switch for "Recurring" toggle
// - Submit Button that calls a Server Action with Zod validation:
//   - amount_cents must be > 0
//   - category_id and account_id required
//   - date required, not in the future
//   - description required, 3-100 chars
// - The Server Action inserts the transaction; the trigger auto-updates account balance and budget
// - Show success toast and close dialog after submission
// - Use Card inside the Dialog for clean form layout
```

> Pro tip: Use Design Mode (Option+D) to adjust the transaction form layout, button sizes, and color scheme without spending V0 credits.

**Expected result:** A floating Add button opens a transaction form. After submission, the balance and budget update automatically via the database trigger.

### 4. Create the budget tracking page

Build the budget page showing spending progress per category with limits and utilization bars. Users can set monthly budget limits for each expense category.

```
// Paste this prompt into V0's AI chat:
// Build a budgets page at app/budgets/page.tsx.
// Requirements:
// - Server Component fetching budgets for the current month joined with categories
// - Each category budget as a Card showing: category icon + name, spent vs limit in dollars, Progress bar (green under 80%, yellow 80-100%, red over 100%)
// - Summary Card at top: total budget, total spent, remaining
// - "Set Budget" Button per category that opens a Dialog with Input for the monthly limit amount
// - Server Action to upsert the budget (create if not exists, update if exists for this month)
// - Show categories without budgets in a separate "Untracked" section with "Add Budget" Button
// - Filter by month using a month/year Select at the top
// - Use Badge for over-budget categories with a warning color
// - Layout: grid of budget cards, 2-3 columns on desktop
```

**Expected result:** The budget page shows spending progress per category with color-coded bars. Over-budget categories are highlighted.

### 5. Build monthly and yearly reports with charts

Create the reports page with interactive charts showing spending trends, category breakdowns, and income vs expense comparisons over time.

```
// Paste this prompt into V0's AI chat:
// Build a reports page at app/reports/page.tsx.
// Requirements:
// - Date range filter at top with month/year Select or DatePickerWithRange
// - Recharts BarChart: monthly income (green) vs expenses (red) side by side for the last 12 months
// - Recharts PieChart: expense breakdown by category with matching colors
// - Recharts LineChart: net savings trend over time (income - expenses per month)
// - Summary Cards: total income, total expenses, net savings, average monthly expense
// - Table showing top spending categories with total, percentage, and comparison to previous period
// - Wrap each chart in a 'use client' component, keep the page as Server Component for data fetching
// - Select to toggle between monthly and yearly views
// - Use Card to wrap each chart section with clear headings
```

> Pro tip: All data fetching happens in Server Components — no NEXT_PUBLIC_ keys needed since Supabase queries run server-side. This means zero client-side key exposure.

**Expected result:** The reports page shows bar charts for monthly trends, pie charts for category breakdown, and line charts for savings over time.

## Complete code example

File: `app/actions/transactions.ts`

```typescript
'use server'

import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
import { z } from 'zod'

const transactionSchema = z.object({
  amount_cents: z.number().int().positive(),
  type: z.enum(['income', 'expense', 'transfer']),
  category_id: z.string().uuid(),
  account_id: z.string().uuid(),
  description: z.string().min(3).max(100),
  date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
  notes: z.string().max(500).optional(),
  is_recurring: z.boolean().default(false),
})

export async function addTransaction(formData: FormData) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) throw new Error('Unauthorized')

  const parsed = transactionSchema.parse({
    amount_cents: Math.round(Number(formData.get('amount')) * 100),
    type: formData.get('type'),
    category_id: formData.get('category_id'),
    account_id: formData.get('account_id'),
    description: formData.get('description'),
    date: formData.get('date'),
    notes: formData.get('notes') || undefined,
    is_recurring: formData.get('is_recurring') === 'true',
  })

  const { error } = await supabase.from('transactions').insert({
    ...parsed,
    user_id: user.id,
  })

  if (error) throw new Error(error.message)
  revalidatePath('/')
  revalidatePath('/budgets')
}

export async function deleteTransaction(id: string) {
  const supabase = await createClient()
  const { error } = await supabase
    .from('transactions')
    .delete()
    .eq('id', id)

  if (error) throw new Error(error.message)
  revalidatePath('/')
  revalidatePath('/budgets')
}
```

## Common mistakes

- **Manually updating account balances instead of using database triggers** — Application-level balance updates can fail or be skipped, causing balances to drift from actual transaction totals. Fix: Use a PostgreSQL trigger function that fires after INSERT/UPDATE/DELETE on transactions to atomically adjust account balances and budget spent amounts.
- **Using floating-point numbers for money** — $10.10 + $10.20 in floating-point may not equal $20.30, causing penny discrepancies in reports. Fix: Store all amounts in cents as bigint. Convert to dollars only for display: (amount_cents / 100).toFixed(2).
- **Fetching all transactions for chart rendering** — Loading thousands of transactions to compute monthly totals is slow and memory-intensive. Fix: Use Supabase aggregate queries or database views that pre-compute monthly totals, fetching only the summary data needed for charts.

## Best practices

- Store monetary amounts in cents as bigint to avoid floating-point rounding errors.
- Use PostgreSQL triggers to keep account balances and budget spent amounts atomically synchronized with transactions.
- Use Server Components for dashboards and reports — all data fetching runs server-side with zero client-side key exposure.
- Use Design Mode (Option+D) to adjust chart colors, card layouts, and dashboard grids without spending V0 credits.
- Enable RLS with auth.uid() = user_id policies on all tables so users can only see their own financial data.
- Validate all transaction inputs with Zod in Server Actions — enforce positive amounts, valid dates, and required categories.

## Frequently asked questions

### How do account balances stay in sync with transactions?

A PostgreSQL trigger function fires automatically after every INSERT, UPDATE, or DELETE on the transactions table. It adjusts the related account's balance_cents and the matching budget's spent_cents atomically within the database.

### Can I track multiple accounts?

Yes. The accounts table supports checking, savings, credit card, and cash account types. Each transaction is linked to a specific account, and the dashboard shows individual and total balances.

### How do budgets work?

Each expense category can have a monthly budget limit. When transactions are added, the trigger updates the spent_cents. The budgets page shows progress bars with color coding: green under 80%, yellow approaching limit, red over budget.

### What V0 plan do I need?

Premium ($20/month) is recommended for the chart components and form iterations. Free tier works for the basic transaction list but may need manual coding for Recharts integration.

### How do I deploy?

Publish via V0's Share menu. All env vars are server-side only (SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY in Vars tab, no NEXT_PUBLIC_ prefix needed since all data fetching is in Server Components).

### Can RapidDev help build a custom finance tracker?

Yes. RapidDev has built 600+ apps including financial management platforms with bank sync, multi-currency support, and investment tracking. Book a free consultation to discuss your requirements.

---

Source: https://www.rapidevelopers.com/how-to-build-v0/finance-tracker
© RapidDev — https://www.rapidevelopers.com/how-to-build-v0/finance-tracker
