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.
What you're building
Personal finance tracking helps individuals and small businesses understand where money goes. With income and expense categorization, budget tracking, and visual reports, users gain control over their financial health.
V0 generates the transaction forms, budget dashboards, and reporting charts from prompts. Supabase handles data storage with PostgreSQL triggers that automatically keep account balances and budget spent amounts in sync with every transaction change.
The architecture uses Next.js App Router with Server Components for dashboards and reports (zero client-side JavaScript for initial data), client components for interactive transaction forms, Server Actions for transaction CRUD with Zod validation, and Recharts for spending visualizations.
Final result
A complete finance tracker with multi-account management, categorized transactions, budget progress tracking, and visual monthly and yearly spending reports.
Tech stack
Prerequisites
- 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)
Build steps
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.
1// Paste this prompt into V0's AI chat:2// Build a finance tracker. Create a Supabase schema with:3// 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)4// 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)5// 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)6// 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))7// 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.8// 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.
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.
1import { createClient } from '@/lib/supabase/server'2import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'3import { Badge } from '@/components/ui/badge'4import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'5import Link from 'next/link'67export default async function DashboardPage() {8 const supabase = await createClient()9 const { data: { user } } = await supabase.auth.getUser()10 const now = new Date()11 const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1).toISOString()1213 const { data: accounts } = await supabase14 .from('accounts').select('*').eq('user_id', user?.id)1516 const { data: monthTx } = await supabase17 .from('transactions').select('amount_cents, type')18 .eq('user_id', user?.id).gte('date', startOfMonth.split('T')[0])1920 const { data: recent } = await supabase21 .from('transactions').select('*, categories(name, icon, color)')22 .eq('user_id', user?.id).order('date', { ascending: false }).limit(10)2324 const netWorth = accounts?.reduce((s, a) => s + Number(a.balance_cents), 0) ?? 025 const income = monthTx?.filter(t => t.type === 'income').reduce((s, t) => s + Number(t.amount_cents), 0) ?? 026 const expenses = monthTx?.filter(t => t.type === 'expense').reduce((s, t) => s + Number(t.amount_cents), 0) ?? 02728 return (29 <div className="container mx-auto py-8">30 <h1 className="text-3xl font-bold mb-6">Finance Dashboard</h1>31 <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">32 <Card><CardHeader><CardTitle className="text-sm">Net Worth</CardTitle></CardHeader>33 <CardContent><p className="text-2xl font-bold">${(netWorth / 100).toLocaleString()}</p></CardContent></Card>34 <Card><CardHeader><CardTitle className="text-sm">Income This Month</CardTitle></CardHeader>35 <CardContent><p className="text-2xl font-bold text-green-600">+${(income / 100).toLocaleString()}</p></CardContent></Card>36 <Card><CardHeader><CardTitle className="text-sm">Expenses This Month</CardTitle></CardHeader>37 <CardContent><p className="text-2xl font-bold text-red-600">-${(expenses / 100).toLocaleString()}</p></CardContent></Card>38 </div>39 <Table>40 <TableHeader><TableRow>41 <TableHead>Date</TableHead><TableHead>Description</TableHead>42 <TableHead>Category</TableHead><TableHead>Amount</TableHead>43 </TableRow></TableHeader>44 <TableBody>45 {recent?.map((tx) => (46 <TableRow key={tx.id}>47 <TableCell>{new Date(tx.date).toLocaleDateString()}</TableCell>48 <TableCell>{tx.description}</TableCell>49 <TableCell><Badge style={{ backgroundColor: tx.categories?.color }}>{tx.categories?.icon} {tx.categories?.name}</Badge></TableCell>50 <TableCell className={tx.type === 'income' ? 'text-green-600' : 'text-red-600'}>51 {tx.type === 'income' ? '+' : '-'}${(Number(tx.amount_cents) / 100).toFixed(2)}52 </TableCell>53 </TableRow>54 ))}55 </TableBody>56 </Table>57 </div>58 )59}Expected result: The dashboard shows net worth, monthly income/expenses, and recent transactions with color-coded categories.
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.
1// Paste this prompt into V0's AI chat:2// Build a transaction form as a 'use client' component.3// Requirements:4// - A Dialog triggered by a floating "+ Add Transaction" Button5// - Tabs to switch between Income and Expense (sets the type)6// - Input for amount with dollar sign prefix7// - Select for category (filtered by income/expense type)8// - Select for account9// - Calendar DatePicker for transaction date10// - Input for description (required)11// - Textarea for optional notes12// - Switch for "Recurring" toggle13// - Submit Button that calls a Server Action with Zod validation:14// - amount_cents must be > 015// - category_id and account_id required16// - date required, not in the future17// - description required, 3-100 chars18// - The Server Action inserts the transaction; the trigger auto-updates account balance and budget19// - Show success toast and close dialog after submission20// - Use Card inside the Dialog for clean form layoutPro 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.
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.
1// Paste this prompt into V0's AI chat:2// Build a budgets page at app/budgets/page.tsx.3// Requirements:4// - Server Component fetching budgets for the current month joined with categories5// - 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%)6// - Summary Card at top: total budget, total spent, remaining7// - "Set Budget" Button per category that opens a Dialog with Input for the monthly limit amount8// - Server Action to upsert the budget (create if not exists, update if exists for this month)9// - Show categories without budgets in a separate "Untracked" section with "Add Budget" Button10// - Filter by month using a month/year Select at the top11// - Use Badge for over-budget categories with a warning color12// - Layout: grid of budget cards, 2-3 columns on desktopExpected result: The budget page shows spending progress per category with color-coded bars. Over-budget categories are highlighted.
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.
1// Paste this prompt into V0's AI chat:2// Build a reports page at app/reports/page.tsx.3// Requirements:4// - Date range filter at top with month/year Select or DatePickerWithRange5// - Recharts BarChart: monthly income (green) vs expenses (red) side by side for the last 12 months6// - Recharts PieChart: expense breakdown by category with matching colors7// - Recharts LineChart: net savings trend over time (income - expenses per month)8// - Summary Cards: total income, total expenses, net savings, average monthly expense9// - Table showing top spending categories with total, percentage, and comparison to previous period10// - Wrap each chart in a 'use client' component, keep the page as Server Component for data fetching11// - Select to toggle between monthly and yearly views12// - Use Card to wrap each chart section with clear headingsPro 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
1'use server'23import { createClient } from '@/lib/supabase/server'4import { revalidatePath } from 'next/cache'5import { z } from 'zod'67const transactionSchema = z.object({8 amount_cents: z.number().int().positive(),9 type: z.enum(['income', 'expense', 'transfer']),10 category_id: z.string().uuid(),11 account_id: z.string().uuid(),12 description: z.string().min(3).max(100),13 date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),14 notes: z.string().max(500).optional(),15 is_recurring: z.boolean().default(false),16})1718export async function addTransaction(formData: FormData) {19 const supabase = await createClient()20 const { data: { user } } = await supabase.auth.getUser()21 if (!user) throw new Error('Unauthorized')2223 const parsed = transactionSchema.parse({24 amount_cents: Math.round(Number(formData.get('amount')) * 100),25 type: formData.get('type'),26 category_id: formData.get('category_id'),27 account_id: formData.get('account_id'),28 description: formData.get('description'),29 date: formData.get('date'),30 notes: formData.get('notes') || undefined,31 is_recurring: formData.get('is_recurring') === 'true',32 })3334 const { error } = await supabase.from('transactions').insert({35 ...parsed,36 user_id: user.id,37 })3839 if (error) throw new Error(error.message)40 revalidatePath('/')41 revalidatePath('/budgets')42}4344export async function deleteTransaction(id: string) {45 const supabase = await createClient()46 const { error } = await supabase47 .from('transactions')48 .delete()49 .eq('id', id)5051 if (error) throw new Error(error.message)52 revalidatePath('/')53 revalidatePath('/budgets')54}Customization ideas
Add bank account sync via Plaid
Integrate Plaid API to automatically import transactions from connected bank accounts, reducing manual entry.
Add recurring transaction automation
Use Vercel Cron to automatically create recurring transactions (rent, subscriptions) on their scheduled dates.
Add spending alerts
Send email notifications via Resend when spending exceeds 80% of a category budget or when large transactions are detected.
Add multi-currency support
Store transactions in their original currency and convert to base currency using exchange rate APIs for unified reporting.
Common pitfalls
Pitfall: Manually updating account balances instead of using database triggers
How to avoid: Use a PostgreSQL trigger function that fires after INSERT/UPDATE/DELETE on transactions to atomically adjust account balances and budget spent amounts.
Pitfall: Using floating-point numbers for money
How to avoid: Store all amounts in cents as bigint. Convert to dollars only for display: (amount_cents / 100).toFixed(2).
Pitfall: Fetching all transactions for chart rendering
How to avoid: 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.
AI prompts to try
Copy these prompts to build this project faster.
I'm building a finance tracker with Next.js and Supabase. I need a PostgreSQL trigger function that fires after INSERT, UPDATE, and DELETE on the transactions table to atomically update the related account's balance_cents and the matching budget's spent_cents. Show me the complete trigger function with all three cases handled.
Build the budget tracking page for a finance tracker. Show each expense category as a Card with icon, spent/limit amounts, and a Progress bar. Color the bar green under 80%, yellow between 80-100%, and red above 100%. Add a Set Budget Dialog with amount Input. Use a Server Action to upsert the budget row for the current month.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation