# How to Add Expense Tracking to Your App (Copy-Paste Prompts Included)

- Tool: App Features
- Last updated: July 2026

## TL;DR

Expense tracking needs a quick-add form, receipt storage, a category breakdown chart, and multi-currency support. With Lovable or V0 you can ship a working feature in 4–8 hours. Running cost is $0–25/mo for small teams — Supabase free tier covers the database and receipt storage up to ~1K users. Costs rise when receipt photos accumulate (~200KB each) and FX rates need a paid API.

## What an Expense Tracking Feature Actually Is

An expense tracker records money spent: amount, category, date, and optionally a receipt photo. It then surfaces that data as charts and monthly summaries that help users understand where money goes. The core product decisions are how far to automate — from pure manual entry (4 hours to ship) up to bank feed import and OCR receipt parsing (weeks of custom work). Most founders need the middle ground: a form with categories, a category pie chart, a monthly budget comparison, and a CSV export. Multi-currency support is the most common feature that breaks first builds because the naive implementation stores amounts without a base currency, making aggregations across currencies meaningless.

## Anatomy of the Feature

Seven components. The currency conversion layer and the recurring job scheduler are where AI tools most commonly produce broken first drafts — be explicit about both in your prompt.

- **Quick-add expense form** (ui): react-hook-form + zod (web) or Flutter Form widget (mobile). Amount field uses react-currency-input-field for locale-aware decimal input with currency symbol. Category select shows predefined + user-created categories with color dots. shadcn/ui DatePicker defaults to today. Zod validation: amount must be a positive decimal, category required, date required.
- **Receipt upload layer** (service): Supabase Storage private bucket receives the receipt photo via a presigned upload URL generated by a Supabase Edge Function. The Edge Function returns the storage object path which is saved to the receipt_url column on the expense row. Thumbnails are served via Supabase Storage transform URL (?width=200) or imgix for on-the-fly resizing.
- **Category breakdown chart** (ui): Recharts PieChart with a legend (web) or fl_chart PieChart (Flutter). Data grouped by category for the current month — either via a Postgres view that sums by category and month, or client-side reduce over the filtered expense list. Each slice color comes from the categories.color column.
- **Expense list with filters** (ui): shadcn/ui DataTable (web) or Flutter ListView with filter chips (mobile). Filters: date range (from/to), multi-select category, and optional amount range. Build the Supabase query dynamically — only add a WHERE clause for each filter that has a value selected. Sorted by date descending by default. Pagination or virtual scroll for users with hundreds of entries.
- **Budget vs actual comparison** (data): A Postgres view sums expenses by category and month, then joins against the budgets table. The result feeds a Recharts BarChart with two bars per category (budget vs spent). Green when under budget, red when over. Store budget amounts in a budgets table keyed to (user_id, category_id, month) so users can set different budgets per month without affecting historical data.
- **Multi-currency support** (service): Frankfurter API (free, European Central Bank data, no API key needed) or Open Exchange Rates for higher reliability with 170+ currencies. Rates cached in a Supabase fx_rates table for 24 hours to avoid excessive API calls. On INSERT, an Edge Function converts the entered amount to the user's base currency (default: USD) and stores original_amount, original_currency, and amount_usd as separate columns.
- **Recurring expense scheduler** (backend): Supabase Edge Function triggered by the pg_cron extension. Queries expenses where is_recurring=true and inserts a copy on their defined schedule (monthly, weekly, annually). The cron job runs at midnight UTC on the 1st of each month for monthly recurring expenses.

## Data model

Five tables covering expenses, categories, budgets, FX rate cache, and a monthly summary view. Run this in the Supabase SQL editor — all RLS policies are included:

```sql
create table public.categories (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  name text not null,
  color varchar(7) not null default '#6366f1',
  icon text,
  is_default bool not null default false,
  created_at timestamptz not null default now()
);

create table public.expenses (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  amount decimal(12,2) not null check (amount > 0),
  currency varchar(3) not null default 'USD',
  amount_usd decimal(12,2),
  category_id uuid references public.categories(id) on delete set null,
  date date not null default current_date,
  note text,
  receipt_url text,
  is_recurring bool not null default false,
  recurrence_interval varchar(20),
  created_at timestamptz not null default now()
);

create table public.budgets (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  category_id uuid references public.categories(id) on delete cascade not null,
  month date not null,
  amount decimal(12,2) not null check (amount > 0),
  constraint budgets_user_category_month_unique unique (user_id, category_id, month)
);

create table public.fx_rates (
  base_currency varchar(3) not null,
  target_currency varchar(3) not null,
  rate decimal(18,6) not null,
  fetched_at timestamptz not null default now(),
  primary key (base_currency, target_currency)
);

-- Row Level Security
alter table public.categories enable row level security;
alter table public.expenses enable row level security;
alter table public.budgets enable row level security;

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

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

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

-- Indexes for common queries
create index expenses_user_date_idx on public.expenses (user_id, date desc);
create index expenses_user_category_idx on public.expenses (user_id, category_id);

-- Monthly summary view powering the pie chart and budget comparison
create view public.monthly_category_summary as
  select
    e.user_id,
    date_trunc('month', e.date)::date as month,
    e.category_id,
    c.name as category_name,
    c.color as category_color,
    sum(e.amount_usd) as total_usd,
    count(*) as expense_count
  from public.expenses e
  join public.categories c on c.id = e.category_id
  where e.amount_usd is not null
  group by e.user_id, date_trunc('month', e.date)::date, e.category_id, c.name, c.color;
```

The monthly_category_summary view powers both the pie chart and the budget comparison BarChart without client-side aggregation. The fx_rates table acts as a 24-hour cache — your Edge Function checks if the rate was fetched in the last 24 hours before calling the Frankfurter API.

## Build paths

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

Lovable handles the Supabase schema, react-hook-form expense entry, Recharts breakdown chart, and auth in one session. Best path for founders who want a full working feature without manually setting up the database.

1. Create a new Lovable project and connect Lovable Cloud to provision Supabase auth and the database automatically
2. Paste the prompt below — Agent Mode will scaffold the expense form, category chart, filter list, and budget comparison
3. In Lovable Cloud tab → Secrets, add FRANKFURTER_API_URL=https://api.frankfurter.app (no API key needed) or your Open Exchange Rates key if using that service
4. In Supabase Dashboard → Database → Extensions, search for pg_cron and enable it before testing recurring expense insertion
5. Publish the app and verify receipt upload, category chart, and currency conversion on the live HTTPS URL with a real Supabase connection

Starter prompt:

```
Build a full expense tracking feature using Supabase. Create these tables with RLS: categories (id uuid pk, user_id references auth.users, name text, color varchar(7) default '#6366f1', icon text, is_default bool default false); expenses (id uuid pk, user_id references auth.users, amount decimal(12,2) check amount > 0, currency varchar(3) default 'USD', amount_usd decimal(12,2), category_id uuid references categories, date date default current_date, note text, receipt_url text, is_recurring bool default false, recurrence_interval varchar(20)); budgets (id uuid pk, user_id references auth.users, category_id uuid, month date, amount decimal). All RLS policies must restrict all operations to auth.uid() = user_id. UI on the main /expenses page: at the top, a quick-add expense form using react-hook-form with an amount field (react-currency-input-field, decimal, positive only), a category picker showing color dots for default and custom categories, a date picker defaulting to today, an optional note field, and a receipt upload button that stores to a private Supabase Storage bucket and saves the path to receipt_url. For multi-currency: on form submit, call a Supabase Edge Function that fetches the USD rate from api.frankfurter.app, caches it in an fx_rates table (base_currency, target_currency, rate, fetched_at) for 24 hours, and stores both original amount/currency and amount_usd on the expense row. Below the form: a Recharts PieChart showing current-month spending by category grouped using a Postgres view — wrap the chart in a conditional and only render when data.length > 0, otherwise show an empty-state illustration. A budget comparison section with a Recharts BarChart (two bars per category: budget vs spent, green under / red over). An expense list with a shadcn/ui DataTable, date range filter (from/to DateRangePicker), multi-select category filter, and a CSV export button. Build the Supabase filter query dynamically — only add WHERE clauses for filters that have values selected. Include a Monthly Summary card showing total spent vs total budgeted. Recurring expenses: is_recurring toggle on the form with a recurrence_interval select (monthly, weekly, annually); note in a comment that pg_cron must be enabled in Supabase Dashboard before the recurring job fires.
```

Limitations:

- pg_cron for recurring expense insertion requires manual activation in Supabase Dashboard — Lovable AI generates the migration but cannot enable the extension
- Receipt thumbnails via Supabase Storage transform URL sometimes need a follow-up prompt to wire the signed URL display correctly
- The FX rate caching Edge Function may need a second prompt to handle the 24-hour staleness check and fx_rates upsert correctly

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

Best for the reporting and dashboard view — V0 generates polished shadcn/ui DataTable with sorting and filtering, and Server Components make the monthly category chart fast. Choose this when expense tracking is part of a larger Next.js product.

1. Prompt V0 with the spec below to generate the expense form, DataTable, Recharts chart, and budget comparison components
2. In the V0 Vars panel, add NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY from your Supabase project settings
3. Run the SQL schema from this page in the Supabase SQL editor to create all tables, RLS policies, and the monthly_category_summary view
4. For receipt storage, either set up Vercel Blob (add BLOB_READ_WRITE_TOKEN in Vercel Dashboard) or point to a private Supabase Storage bucket via an API route
5. Add FRANKFURTER_BASE_URL=https://api.frankfurter.app to Vercel env vars — the FX rate fetch runs server-side in a Next.js API route, never in the browser
6. Deploy to Vercel and verify receipt upload, currency conversion, budget comparison, and CSV export on the live URL

Starter prompt:

```
Build an expense tracking dashboard in Next.js using Supabase. Main Server Component page at /expenses fetches current-month expenses for the signed-in user. Include a shadcn/ui DataTable with columns: date (formatted locale), amount (formatted with currency symbol), category (color dot + name), note, receipt (thumbnail link). Add column sorting and a filter bar with shadcn/ui DateRangePicker (from/to) and a multi-select Combobox for category — build the Supabase query dynamically so only active filters add WHERE clauses; never add WHERE category_id = null when no category is selected. CSV export button triggers a Next.js API route at /api/expenses/export that returns the filtered list as a CSV download with headers: date, amount, currency, amount_usd, category, note, receipt_url. Include a New Expense Sheet (shadcn/ui Sheet) triggered by a button: react-hook-form form with react-currency-input-field amount (decimal, positive only, default currency USD), category select with color indicators for default and user categories, shadcn/ui DatePicker defaulting to today, note textarea, is_recurring toggle with recurrence_interval select. Receipt upload: POST to /api/upload which stores to Vercel Blob and returns the URL, then saves receipt_url to the Supabase expense row. For FX: add /api/fx-rate Next.js API route that fetches USD rates from api.frankfurter.app and caches in Supabase fx_rates table for 24 hours; always store amount_usd alongside original amount/currency. Recharts PieChart for current-month spending by category — only render when data.length > 0. Recharts BarChart for budget vs actual (green under budget, red over) fetching from the budgets and monthly_category_summary tables. Monthly summary card showing total spent vs total budgeted. Handle SSR: wrap any localStorage access in useEffect; all filter state in URL search params via useRouter.
```

Limitations:

- No auto-provisioned database — Neon or Supabase must be set up separately and tables created before the app works end-to-end
- Receipt upload requires either Vercel Blob or Supabase Storage — both need manual env var setup in Vercel Dashboard
- The pg_cron recurring expense job is not included by default — add it as a follow-up prompt after the base feature is working and the extension is enabled in Supabase

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

Good for a native mobile expense logger where camera receipt capture via image_picker is the primary input method. Firebase Firestore or Supabase handles persistence. Best when mobile is the priority and chart customization requirements are minimal.

1. Create a FlutterFlow project connected to Firebase or Supabase; enable email/password auth in FlutterFlow Auth settings
2. Build the expense entry page: use FlutterFlow Form widget with TextField for amount, DropdownButton for category, DatePicker for date, and image_picker for receipt photo
3. Wire a custom Dart HTTP action to call api.frankfurter.app for FX rate conversion when currency is not the base currency
4. Add fl_chart PieChart as a custom widget for the category breakdown — FlutterFlow's built-in charts have insufficient color and legend customization for a financial dashboard
5. Test receipt capture on a real device via the FlutterFlow mobile preview app — image_picker does not work in web preview mode

Limitations:

- fl_chart PieChart for category breakdown must be added as a custom widget — FlutterFlow's built-in charts cannot be colored per-category from a Firestore/Supabase color field
- Dynamic filter queries (date range + multi-select category) require custom Dart code in a custom action, not visual workflow blocks
- pg_cron for recurring expenses is not available in a Firebase project — use Cloud Scheduler + Cloud Functions as an alternative, which requires custom code
- Multi-currency FX rate fetching requires a custom Dart HTTP action and caching logic since FlutterFlow has no built-in API caching

### Custom — fit 5/10, 1–3 weeks

Required when the feature moves beyond manual entry — bank feed import via Plaid or TrueLayer, OCR receipt parsing via Mindee, multi-user team approval workflows, or reimbursement payouts via Stripe Connect.

1. Bank feed import: embed Plaid Link or TrueLayer OAuth flow in a Next.js page; exchange the public_token server-side via an API route; store the encrypted access_token in Supabase; poll /transactions nightly via a Supabase Edge Function with deduplication
2. OCR receipt parsing: send uploaded receipt to Mindee API from a Supabase Edge Function; map extracted merchant name, amount, and date back to the expense form as pre-filled default values that users can correct
3. Multi-user team expenses: add an organization_id column to all tables; update RLS policies to allow team members to SELECT all org expenses and managers to UPDATE approval status; build a notification system for pending approvals
4. Reimbursement payouts: Stripe Connect Express accounts for each employee; payout triggered via Stripe Payouts API after manager approval in the approval workflow

Limitations:

- Plaid production access requires business verification and typically costs $500/mo minimum — budget accordingly before committing to bank feed import
- OCR accuracy varies by receipt quality — always allow users to review and correct pre-filled fields before saving an OCR-parsed expense

## Gotchas

- **Currency totals are wildly wrong — mixing USD and EUR produces nonsense** — The most common first-build bug: the AI stores amount exactly as entered without converting to a base currency. When the app then sums expenses across currencies, 100 EUR + 100 USD + 100 GBP are all treated as the same numeric unit, producing aggregations that are financially meaningless. This only becomes visible once a user adds their first expense in a second currency. Fix: Always convert to a base currency (USD or the user's chosen home currency) at INSERT time using the FX rate for that day. Store original_amount, original_currency, and amount_usd as separate columns. All charts and monthly summaries must aggregate on amount_usd only. Specify both the storage schema and the conversion step explicitly in your prompt — the AI defaults to storing the raw entered value.
- **Receipt photos accessible to any user who knows the storage URL** — Supabase Storage defaults to public buckets in many AI-generated builds. A public bucket means any person with the storage path can view any receipt — including those of other users — with no authentication check. This is a privacy violation that often goes unnoticed in development because the developer only sees their own receipts. Fix: Set the Supabase Storage bucket to private. Display receipts by generating signed URLs with a 1-hour expiry from a Supabase Edge Function that first checks auth.uid() before signing. Never store or display a permanent public URL for a private financial document.
- **Recurring expenses never insert — pg_cron job registered but silent** — Lovable and V0 generate correct SQL migration code for pg_cron scheduled jobs, but the pg_cron extension itself must be manually enabled in Supabase Dashboard before any cron migration takes effect. Without the extension enabled, the migration runs without error but the job never actually registers in cron.job — so no recurring rows are ever inserted. Fix: In Supabase Dashboard, navigate to Database → Extensions, search pg_cron, and enable it. Then verify the job appears by querying SELECT * FROM cron.job in the SQL editor. If you already ran the migration before enabling the extension, run it again after enabling.
- **Expense list crashes on empty filter results** — When the date range or category filter returns zero matching expenses, JavaScript's .reduce() without a default initializer throws 'reduce of empty array with no initial value'. Simultaneously, Recharts PieChart renders a blank white box with console errors when passed data=[]. Both failures happen together whenever a user applies a filter that matches nothing — a common scenario on first use. Fix: Pass 0 as the second argument to every .reduce() call. Wrap the Recharts PieChart in a conditional that only renders when data.length > 0, with an empty-state illustration and a 'clear filters' link below it. Specify both fixes in your prompt — AI tools skip them by default.

## Best practices

- Store both the original currency and the base-currency amount at INSERT time — never try to re-fetch historical FX rates for past transactions, as rates change daily and retrospective conversion is inaccurate
- Cache FX rates in a Supabase fx_rates table for 24 hours per currency pair — daily rates are accurate enough for expense tracking and prevent 1,000 users each triggering separate API calls when loading the dashboard
- Use a private Supabase Storage bucket for receipts and generate signed URLs with 1-hour expiry — treat receipt photos as sensitive financial documents, not public assets
- Build category filter queries dynamically — only add a WHERE clause for filters that have active values, or an empty category_id = null condition silently returns zero rows
- Seed default categories (Food, Travel, Office, Subscriptions, Other) on first sign-in via a Supabase trigger or database function — a blank category picker on first use is the primary activation blocker
- Show the Recharts PieChart only when data.length > 0 and display an illustrated empty state that prompts adding the first expense — PieChart crashes on empty arrays and the crash surface is not immediately obvious
- Add a CSV export from the currently filtered view, not a full account dump — users preparing quarterly tax reports want exactly the filtered period, formatted with their local date format and currency symbol
- Design recurring expense recurrence_interval as a structured select (monthly, weekly, annually) rather than a free-text field — this makes pg_cron schedule generation deterministic and avoids scheduling logic bugs

## Frequently asked questions

### How do I handle expenses in multiple currencies?

Store the original amount and currency exactly as entered, then convert to a base currency (USD is the safe default) at INSERT time using the FX rate for that date from Frankfurter API — it's free, requires no API key, and covers 33 currencies. Cache the rate in a Supabase fx_rates table for 24 hours. All charts and monthly totals aggregate on the base-currency column only. Never sum raw amounts across currencies — the result is financially meaningless.

### Can I automatically import bank transactions instead of entering them manually?

Yes, via Plaid (US, Canada, UK) or TrueLayer (Europe, UK). Both require users to authenticate their bank through a secure OAuth flow — the connection runs server-side, never in the browser. New transactions sync nightly or on demand. Plaid production access requires business verification and typically costs around $500/mo minimum. This is a custom development project, not an AI tool prompt, and typically adds 1–2 weeks to the build.

### How do I categorize expenses automatically without the user selecting a category each time?

Two approaches: rule-based (map merchant names or transaction descriptions to categories using a Supabase lookup table — 'Netflix' always maps to 'Subscriptions') or AI-based (send merchant name and description to Claude or GPT via an API route that returns a suggested category). The rule-based approach is faster, cheaper, and more predictable for the 20 merchants that typically represent 80% of any user's spending.

### Can I set monthly budget limits and get notified when spending is close to the limit?

Yes — store monthly budget amounts in a budgets table keyed to (user_id, category_id, month). After each INSERT, a Supabase trigger or Edge Function compares the running monthly total to the budget for that category. When spending crosses 80% of the limit, trigger a notification via Resend email or OneSignal push notification. The query is a simple sum of expenses for the current month filtered to that category.

### How do I export expenses for tax purposes?

A CSV export covers most tax requirements — include date, amount, currency, base-currency amount, category, merchant or note, and receipt URL. Trigger the export from a Next.js API route or Supabase Edge Function that runs the same filtered Supabase query as the expense list and streams back a CSV file download. For PDF, render the filtered list using jsPDF or Puppeteer server-side and return a formatted expense report.

### Can I track recurring subscriptions separately from one-off expenses?

Set is_recurring=true and recurrence_interval='monthly' on subscription expenses. Use this flag to build a dedicated Subscriptions view that shows only recurring expenses grouped by next renewal date. pg_cron in Supabase handles automatic monthly re-insertion — but remember to enable the pg_cron extension manually in Supabase Dashboard → Database → Extensions before your cron job will fire. The migration SQL generates correctly without the extension being enabled, but the job silently does nothing.

### How do I share expense tracking with a partner or business team?

Add an organization_id column to all tables and update RLS policies to allow SELECT for all members of the same organization, stored in an org_members table (org_id, user_id, role). Read-only sharing is straightforward. For a full approval workflow with role-based write permissions — where managers can approve and finance can export but employees can only submit — plan for 2–4 weeks of custom development beyond a standard AI tool build.

### What is the best way to handle receipt photos uploaded on mobile?

On mobile, use image_picker (Flutter) or the browser File input with the capture='camera' attribute (web PWA) to open the camera directly from the expense form. Compress the photo client-side to approximately 200KB before upload using browser-image-compression (web) or the Flutter image package to keep storage costs manageable as users accumulate months of receipts. Store in a private Supabase Storage bucket and display via a signed URL with a 1-hour expiry — never via a permanent public URL.

---

Source: https://www.rapidevelopers.com/app-features/expense-tracking
© RapidDev — https://www.rapidevelopers.com/app-features/expense-tracking
