Feature spec
IntermediateCategory
vertical-tools
Build with AI
4–8 hours with Lovable or V0
Custom build
1–3 weeks custom dev
Running cost
$0–25/mo up to 1K users
Works on
Everything it takes to ship Expense Tracking — parts, prompts, and real costs.
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.
What users consider table stakes in 2026
- Quick-add expense form accessible in one tap from the home screen — amount field focused immediately, not three levels deep
- Category picker showing default system categories and user-created custom categories with color dots
- Receipt photo upload from camera or gallery, stored securely and viewable from the expense row
- Monthly summary card showing total spent vs total budgeted per category with clear over/under status
- Category breakdown pie or bar chart that updates instantly when the date range or category filter changes
- Date range and category filter to drill into specific periods or spending areas without full-page reloads
- CSV export of the filtered expense list for tax and accounting purposes
- Recurring expense support so subscriptions and rent auto-insert each cycle without manual re-entry
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
UIreact-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.
Note: Specify in your prompt that amount must support decimals and reject negative values — AI tools sometimes generate integer-only amount fields.
Receipt upload layer
ServiceSupabase 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.
Note: The bucket must be private — never public — because receipts contain sensitive merchant and amount data. Display receipts via signed URLs with 1-hour expiry, not permanent public links.
Category breakdown chart
UIRecharts 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.
Note: Wrap Recharts PieChart in a conditional — it crashes with an empty data=[] prop when no expenses exist yet.
Expense list with filters
UIshadcn/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.
Note: Do not add WHERE category_id = null when no category is selected — this returns zero rows. Only filter when the user has actively selected a value.
Budget vs actual comparison
DataA 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.
Note: This comparison is the feature that converts casual users into daily active users — make it the most prominent element on the dashboard.
Multi-currency support
ServiceFrankfurter 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.
Note: Never sum amounts across currencies without converting first. This is the most common first-build bug — 100 EUR + 100 USD produce meaningless totals when treated as the same unit.
Recurring expense scheduler
BackendSupabase 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.
Note: pg_cron must be manually enabled in Supabase Dashboard → Database → Extensions before any migration that creates cron jobs takes effect. AI tools generate the correct migration SQL but cannot enable the extension.
The 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:
1create table public.categories (2 id uuid primary key default gen_random_uuid(),3 user_id uuid references auth.users(id) on delete cascade not null,4 name text not null,5 color varchar(7) not null default '#6366f1',6 icon text,7 is_default bool not null default false,8 created_at timestamptz not null default now()9);1011create table public.expenses (12 id uuid primary key default gen_random_uuid(),13 user_id uuid references auth.users(id) on delete cascade not null,14 amount decimal(12,2) not null check (amount > 0),15 currency varchar(3) not null default 'USD',16 amount_usd decimal(12,2),17 category_id uuid references public.categories(id) on delete set null,18 date date not null default current_date,19 note text,20 receipt_url text,21 is_recurring bool not null default false,22 recurrence_interval varchar(20),23 created_at timestamptz not null default now()24);2526create table public.budgets (27 id uuid primary key default gen_random_uuid(),28 user_id uuid references auth.users(id) on delete cascade not null,29 category_id uuid references public.categories(id) on delete cascade not null,30 month date not null,31 amount decimal(12,2) not null check (amount > 0),32 constraint budgets_user_category_month_unique unique (user_id, category_id, month)33);3435create table public.fx_rates (36 base_currency varchar(3) not null,37 target_currency varchar(3) not null,38 rate decimal(18,6) not null,39 fetched_at timestamptz not null default now(),40 primary key (base_currency, target_currency)41);4243-- Row Level Security44alter table public.categories enable row level security;45alter table public.expenses enable row level security;46alter table public.budgets enable row level security;4748create policy "Users manage own categories"49 on public.categories for all50 using (auth.uid() = user_id)51 with check (auth.uid() = user_id);5253create policy "Users manage own expenses"54 on public.expenses for all55 using (auth.uid() = user_id)56 with check (auth.uid() = user_id);5758create policy "Users manage own budgets"59 on public.budgets for all60 using (auth.uid() = user_id)61 with check (auth.uid() = user_id);6263-- Indexes for common queries64create index expenses_user_date_idx on public.expenses (user_id, date desc);65create index expenses_user_category_idx on public.expenses (user_id, category_id);6667-- Monthly summary view powering the pie chart and budget comparison68create view public.monthly_category_summary as69 select70 e.user_id,71 date_trunc('month', e.date)::date as month,72 e.category_id,73 c.name as category_name,74 c.color as category_color,75 sum(e.amount_usd) as total_usd,76 count(*) as expense_count77 from public.expenses e78 join public.categories c on c.id = e.category_id79 where e.amount_usd is not null80 group by e.user_id, date_trunc('month', e.date)::date, e.category_id, c.name, c.color;Heads up: 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 it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
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.
Step by step
- 1Bank 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
- 2OCR 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
- 3Multi-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
- 4Reimbursement payouts: Stripe Connect Express accounts for each employee; payout triggered via Stripe Payouts API after manager approval in the approval workflow
Where this path bites
- 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
Third-party services you'll need
Core expense tracking runs on Supabase alone. Additional services unlock bank import, OCR receipt parsing, and higher-reliability FX data:
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Supabase | Database (expenses, categories, budgets, FX cache), auth, and private Storage bucket for receipt photos | 500MB DB, 1GB storage, 50K monthly active users | $25/mo (Pro) — 8GB DB, 100GB storage |
| Frankfurter API | Daily FX exchange rates from the European Central Bank; no API key required; 33 currencies | Free, no documented rate limit | Free (open-source) |
| Open Exchange Rates | Higher-reliability FX rates with 170+ currencies and hourly updates — upgrade when Frankfurter currency coverage is insufficient | 1,000 requests/mo | From $12/mo (approx) |
| Mindee | OCR receipt parsing — extracts merchant name, total amount, date, and line items from a receipt photo | 250 pages/mo | From $25/mo (approx) |
| Plaid | Bank feed import — OAuth connection to user's bank account for automatic transaction sync instead of manual entry | Development mode (sandbox only, no real bank data) | $500/mo+ for production (approx) |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
Supabase free tier covers the database, auth, and receipt storage at this scale. Frankfurter FX API is free with no API key. Receipt photos at ~200KB each stay well under the 1GB free storage limit even with active users.
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
Currency totals are wildly wrong — mixing USD and EUR produces nonsense
Symptom: 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
Symptom: 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
Symptom: 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
Symptom: 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
When You Need Custom Development
Manual-entry expense tracking ships comfortably with AI tools. These scenarios require a custom build:
- App needs to import transactions automatically from bank accounts via Plaid or TrueLayer — this requires server-side OAuth token management, nightly transaction polling with pagination, and deduplication logic to avoid double-inserting the same transaction
- Receipts should be auto-parsed with OCR to pre-fill amount, merchant, and date fields — Mindee or Google Cloud Vision integration with field-mapping and user-correction UI adds 3–5 days of custom work beyond AI tool defaults
- Multi-user team expenses with an approval workflow (submitted → manager approves → finance exports for reimbursement) require organization-scoped RLS, role tables, and a multi-step notification and status system
- Reimbursement flow where employees submit expenses and the company pays them back requires Stripe Connect Express account onboarding for each employee, payout triggers after approval, and tax form handling
- Needs to export approved expenses automatically to an accounting system (QuickBooks Online, Xero) via their OAuth APIs in real time
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
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.
Need this feature production-ready?
RapidDev builds expense tracking into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.