Build an expense tracking app with Lovable: step by step setup data models, secure sync and integrations to automate reporting and control budgets

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
Build it as a small React app that stores expenses in Supabase (recommended) or localStorage for quick iteration. Use Lovable Chat Mode to create files, the Secrets UI to add SUPABASE_URL and SUPABASE_ANON\_KEY, Preview to test, and Publish when ready — no terminal needed. If you need DB migrations or server functions, create them in Supabase or export to GitHub for CLI work.
Expense tracker with a list view, add/edit/delete expense form, and persistence to Supabase (with a localStorage fallback for quick Preview). All work done via Lovable Chat Mode edits, Secrets UI and Preview.
// Goal: Create the basic expense UI and localStorage persistence so Preview works immediately
// Create or update these files with the exact content described
create src/App.tsx
// Simple router to open the Expenses page
import React from "react";
import Expenses from "./pages/Expenses";
export default function App(){ return <Expenses/>; }
create src/pages/Expenses.tsx
// Page: lists expenses and opens ExpenseForm
import React, {useEffect, useState} from "react";
import ExpenseForm from "../components/ExpenseForm";
import ExpenseItem from "../components/ExpenseItem";
import {loadExpenses, saveExpense, deleteExpense, updateExpense} from "../lib/localApi";
export default function Expenses(){
const [items,setItems]=useState([]);
const [editing,setEditing]=useState(null);
useEffect(()=>{setItems(loadExpenses())},[]);
async function add(e){ await saveExpense(e); setItems(loadExpenses()); }
async function remove(id){ await deleteExpense(id); setItems(loadExpenses()); }
async function edit(updated){ await updateExpense(updated); setItems(loadExpenses()); setEditing(null); }
return (
<div>
<h1>Expenses</h1>
<ExpenseForm onSubmit={add} initial={editing}/>
<ul>{items.map(it=> <ExpenseItem key={it.id} item={it} onDelete={()=>remove(it.id)} onEdit={()=>setEditing(it)}/>)}</ul>
</div>
);
}
create src/components/ExpenseForm.tsx
// Form to add/edit. Keep minimal.
import React, {useState,useEffect} from "react";
export default function ExpenseForm({onSubmit, initial}){
const [desc,setDesc]=useState(""); const [amount,setAmount]=useState("");
useEffect(()=>{ if(initial){ setDesc(initial.description); setAmount(initial.amount); } },[initial]);
function submit(e){ e.preventDefault(); onSubmit({id: initial?.id || Date.now().toString(), description:desc, amount:parseFloat(amount), date: new Date().toISOString()}); setDesc(""); setAmount(""); }
return (
<form onSubmit={submit}>
<input value={desc} onChange={e=>setDesc(e.target.value)} placeholder="Description"/>
<input value={amount} onChange={e=>setAmount(e.target.value)} placeholder="Amount"/>
<button type="submit">Save</button>
</form>
);
}
create src/components/ExpenseItem.tsx
// Row with edit/delete actions
import React from "react";
export default function ExpenseItem({item,onDelete,onEdit}){
return (<li>
{item.description} — ${item.amount}
<button onClick={onEdit}>Edit</button>
<button onClick={onDelete}>Delete</button>
</li>);
}
create src/lib/localApi.ts
// Simple localStorage API for Preview
export function loadExpenses(){ return JSON.parse(localStorage.getItem("expenses")||"[]"); }
export function saveExpense(exp){ const list=loadExpenses(); list.unshift(exp); localStorage.setItem("expenses",JSON.stringify(list)); }
export function deleteExpense(id){ const list=loadExpenses().filter(i=>i.id!==id); localStorage.setItem("expenses",JSON.stringify(list)); }
export function updateExpense(updated){ const list=loadExpenses().map(i=> i.id===updated.id?updated:i); localStorage.setItem("expenses",JSON.stringify(list)); }
// Goal: Add Supabase client and API wrapper that uses Secrets when present
// Create/modify these files. Do NOT add secrets here — set them in Lovable Secrets UI.
create src/lib/supabaseClient.ts
// Create a client that reads from environment variables (Lovable Secrets)
import { createClient } from "@supabase/supabase-js";
// // These env values will be provided via Lovable Secrets at runtime
const SUPABASE_URL = process.env.SUPABASE_URL || "";
const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || "";
export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
modify src/lib/localApi.ts
// Update to try Supabase first when SUPABASE_URL & SUPABASE_ANON_KEY exist
import { supabase } from "./supabaseClient";
// keep local functions as fallback
export async function loadExpenses(){
if(process.env.SUPABASE_URL && process.env.SUPABASE_ANON_KEY){
const { data, error } = await supabase.from("expenses").select("*").order("created_at",{ascending:false});
if(error) throw error; return data;
}
return JSON.parse(localStorage.getItem("expenses")||"[]");
}
export async function saveExpense(exp){
if(process.env.SUPABASE_URL && process.env.SUPABASE_ANON_KEY){
const { error } = await supabase.from("expenses").insert([{ id: exp.id, description: exp.description, amount: exp.amount, date: exp.date }]);
if(error) throw error; return;
}
const list=JSON.parse(localStorage.getItem("expenses")||"[]"); list.unshift(exp); localStorage.setItem("expenses",JSON.stringify(list));
}
export async function deleteExpense(id){
if(process.env.SUPABASE_URL && process.env.SUPABASE_ANON_KEY){
const { error } = await supabase.from("expenses").delete().eq("id", id);
if(error) throw error; return;
}
const list=JSON.parse(localStorage.getItem("expenses")||"[]").filter(i=>i.id!==id); localStorage.setItem("expenses",JSON.stringify(list));
}
export async function updateExpense(updated){
if(process.env.SUPABASE_URL && process.env.SUPABASE_ANON_KEY){
const { error } = await supabase.from("expenses").update({description:updated.description, amount:updated.amount}).eq("id", updated.id);
if(error) throw error; return;
}
const list=JSON.parse(localStorage.getItem("expenses")||"[]").map(i=> i.id===updated.id?updated:i); localStorage.setItem("expenses",JSON.stringify(list));
}
// Goal: Add Supabase credentials using Lovable Secrets UI
// Steps for you (not pasting to chat):
// 1) Open Lovable Cloud > Secrets
// 2) Create SUPABASE_URL with your Supabase project URL
// 3) Create SUPABASE_ANON_KEY with the anon/public key
// 4) In Preview/Publish they will be available as process.env.SUPABASE_URL and SUPABASE_ANON_KEY
// Goal: Create the 'expenses' table in Supabase (Supabase UI)
// This is outside Lovable: open your Supabase project and run SQL or use the table editor:
//
// create table expenses (
// id text primary key,
// description text,
// amount numeric,
// date timestamptz,
// created_at timestamptz default now()
// );
//
// After creating the table the app will persist to Supabase when Secrets are present.
This prompt helps an AI assistant understand your setup and guide to build the feature
This prompt helps an AI assistant understand your setup and guide to build the feature
This prompt helps an AI assistant understand your setup and guide to build the feature

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
Build it as a secure, small-surface web app backed by a real DB (Supabase), use Lovable’s chat-first editor to generate and iterate code, keep secrets in Lovable’s Secrets UI, test in Preview, and export to GitHub for any local CLI or complex migration work. Focus on schema and auth first, validate all AI-generated code, enforce Row-Level Security in Supabase, and use Lovable-native actions (Chat edits, diffs/patches, Preview, Publish, Secrets, GitHub sync) rather than assuming you can run a terminal inside Lovable.
Start small: define the minimal data model (users, expenses, categories, receipts) and auth flows before asking generators to scaffold UI or endpoints.
Work within Lovable constraints: there’s no terminal, so use Chat Mode to edit files, Preview to run the app, Secrets UI to store env vars, and GitHub sync for anything requiring CLI (migrations, local testing).
// POST /api/expenses.js
// Expects body: { amount, currency, category_id, description }
// Uses SUPABASE_SERVICE_ROLE from Lovable Secrets (server-side)
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.SUPABASE_URL
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE
const sb = createClient(supabaseUrl, supabaseServiceKey)
export default async function handler(req, res) {
// Reject non-POST
if (req.method !== 'POST') return res.status(405).end()
const { user_id, amount, currency, category_id, description } = req.body
// Basic server-side validation
if (!user_id || !amount || isNaN(amount)) return res.status(400).json({ error: 'invalid input' })
// Insert expense as server (ensure user_id is valid)
const { data, error } = await sb.from('expenses').insert([
{ user_id, amount, currency: currency || 'USD', category_id, description }
]).select()
if (error) return res.status(500).json({ error: error.message })
return res.status(201).json(data[0])
}
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.